From 1582c3a8ef8926bae55d614dee3375a4fc1ef316 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 11:53:56 +0500 Subject: [PATCH 001/223] Updated on 2026-08-14 --- .../state/confirm/SendNotificationFactory.kt | 13 ++--- .../presentation/viewmodel/SendViewModel.kt | 56 ++++--------------- 2 files changed, 18 insertions(+), 51 deletions(-) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 16c1315e2d..7d19a4e8f8 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -41,7 +41,7 @@ import java.math.BigDecimal @Suppress("LongParameterList", "LargeClass") internal class SendNotificationFactory( private val cryptoCurrencyStatusProvider: Provider, - private val coinCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val currencyChecksRepository: CurrencyChecksRepository, @@ -307,6 +307,7 @@ internal class SendNotificationFactory( private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false val change = when (cryptoCurrencyStatus.currency) { is CryptoCurrency.Coin -> { @@ -314,7 +315,7 @@ internal class SendNotificationFactory( balance - (feeAmount + receivedAmount) } is CryptoCurrency.Token -> { - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val balance = feeCurrencyStatus.value.amount ?: BigDecimal.ZERO balance - feeAmount } } @@ -351,16 +352,14 @@ internal class SendNotificationFactory( val feeValue = fee?.amount?.value ?: BigDecimal.ZERO val userWalletId = userWalletProvider().walletId val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return val warning = getBalanceNotEnoughForFeeWarningUseCase( fee = feeValue, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, - coinStatus = coinCryptoCurrencyStatusProvider(), - ).fold( - ifLeft = { null }, - ifRight = { it }, - ) ?: return + coinStatus = feeCurrencyStatus, + ).getOrNull() ?: return val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName() when (warning) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index f580eaa140..3d37fc0db7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -30,16 +30,14 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase -import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -71,7 +69,6 @@ internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, @@ -172,7 +169,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, currentStateProvider = Provider { uiState }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, @@ -203,7 +200,6 @@ internal class SendViewModel @Inject constructor( private var isAmountSubtractAvailable: Boolean = false private var isUtxoConsolidationAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false - private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null @@ -281,33 +277,18 @@ internal class SendViewModel @Inject constructor( } private suspend fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - val maybeCurrencyStatus = getCurrencyStatus( + getCurrencyStatus( isSingleWalletWithToken = isSingleWalletWithToken, isMultiCurrency = isMultiCurrency, + ).fold( + ifRight = { cryptoCurrencyStatus -> + onDataLoaded( + currencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = getFeeCurrencyStatusSync(cryptoCurrencyStatus, isMultiCurrency), + ) + }, + ifLeft = { showErrorAlert() }, ) - val maybeCoinStatus = if (cryptoCurrency is CryptoCurrency.Coin) { - maybeCurrencyStatus - } else { - getCoinCurrencyStatusUpdates(isSingleWalletWithToken) - } - - if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { - val currencyStatus = maybeCurrencyStatus.getOrElse { - showErrorAlert() - return Timber.e("Currency status is unreachable") - } - val coinStatus = maybeCoinStatus.getOrElse { - showErrorAlert() - return Timber.e("Coin status is unreachable") - } - onDataLoaded( - currencyStatus = currencyStatus, - coinCurrencyStatus = coinStatus, - feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), - ) - } else { - showErrorAlert() - } } private fun getTapHelpPreviewAvailability() { @@ -316,14 +297,6 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase - .invokeSync( - userWalletId = userWalletId, - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = isSingleWalletWithToken, - ) - private suspend fun getCurrencyStatus( isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean, @@ -365,13 +338,8 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded( - currencyStatus: CryptoCurrencyStatus, - coinCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus?, - ) { + private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus?) { cryptoCurrencyStatus = currencyStatus - coinCryptoCurrencyStatus = coinCurrencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus subscribeOnQRScannerResult() when { From 611d980d5ea85a34516e871da2c586f1937a3aeb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 16:41:33 +0500 Subject: [PATCH 002/223] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 6 ++ .../di/domain/WalletManagersFacadeModule.kt | 3 + .../tap/proxy/TransactionManagerImpl.kt | 81 ------------------- .../DefaultWalletManagersFacade.kt | 9 ++- .../transaction/usecase/EstimateFeeUseCase.kt | 5 +- .../usecase/ValidateTransactionUseCase.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 71 +++++++--------- .../swap/domain/di/SwapDomainModule.kt | 12 +-- .../tangem/lib/crypto/TransactionManager.kt | 22 ----- 9 files changed, 54 insertions(+), 157 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 9635e67a29..849209811f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -64,6 +64,12 @@ internal object TransactionDomainModule { return CreateTransactionUseCase(transactionRepository) } + @Provides + @Singleton + fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase { + return EstimateFeeUseCase(walletManagersFacade) + } + @Provides @Singleton fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { 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 ac937e5a47..98c6c20536 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 @@ -6,6 +6,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.walletmanager.DefaultWalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -23,12 +24,14 @@ internal object WalletManagersFacadeModule { userWalletsStore: UserWalletsStore, assetLoader: AssetLoader, blockchainSDKFactory: BlockchainSDKFactory, + dispatchers: CoroutineDispatcherProvider, ): WalletManagersFacade { return DefaultWalletManagersFacade( walletManagersStore = walletManagersStore, userWalletsStore = userWalletsStore, assetLoader = assetLoader, blockchainSDKFactory = blockchainSDKFactory, + dispatchers = dispatchers, ) } } \ 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 a04a551fc2..d6d69b827d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -1,19 +1,10 @@ package com.tangem.tap.proxy -import androidx.core.text.isDigitsOnly import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.Message -import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras -import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras -import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager -import com.tangem.blockchain.blockchains.stellar.StellarMemo -import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras -import com.tangem.blockchain.blockchains.ton.TonTransactionExtras -import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee @@ -65,31 +56,6 @@ class TransactionManagerImpl( ) } - override suspend fun sendTransaction( - txData: SwapTxData, - isSwap: Boolean, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult { - val blockchain = requireNotNull(Blockchain.fromNetworkId(txData.networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - walletManager.update() - val amount = if (isSwap) { - createAmountForSwap(txData.amountToSend, txData.currencyToSend, blockchain) - } else { - createAmount(txData.amountToSend, txData.currencyToSend, blockchain) - } - return sendTransactionInternal( - walletManager = walletManager, - amount = amount, - blockchain = blockchain, - feeAmount = txData.feeAmount, - gasLimit = txData.gasLimit, - destinationAddress = txData.destinationAddress, - dataToSign = txData.dataToSign, - ) - } - @Suppress("LongParameterList") private suspend fun sendTransactionInternal( walletManager: WalletManager, @@ -125,39 +91,11 @@ class TransactionManagerImpl( } } - override fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { - val blockchain = Blockchain.fromNetworkId(networkId) - if (memo == null) return null - return when (blockchain) { - Blockchain.Stellar -> { - val xlmMemo = if (memo.isNotEmpty() && memo.isDigitsOnly()) { - StellarMemo.Id(memo.toBigInteger()) - } else { - StellarMemo.Text(memo) - } - StellarTransactionExtras(xlmMemo) - } - Blockchain.Binance -> BinanceTransactionExtras(memo) - Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } - Blockchain.Cosmos -> CosmosTransactionExtras(memo) - Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionExtras(memo) - Blockchain.Algorand -> AlgorandTransactionExtras(memo) - else -> null - } - } - override suspend fun updateWalletManager(networkId: String, derivationPath: String?) { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } getActualWalletManager(blockchain, derivationPath).update() } - override fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val gasPriceValue = requireNotNull(gasPrice.toLongOrNull()) { "gasprice should be Long" } - return (gasPriceValue * estimatedGas).toBigDecimal().movePointLeft(blockchain.decimals()) - } - @Throws(IllegalStateException::class) override suspend fun getFee( networkId: String, @@ -512,25 +450,6 @@ class TransactionManagerImpl( } } - private fun createAmountForSwap(amount: BigDecimal, currency: Currency?, blockchain: Blockchain): Amount { - return when (currency) { - is Currency.NativeToken, - null, - -> { - Amount(value = amount, blockchain = blockchain) - } - is Currency.NonNativeToken -> { - // 1. when creates swap amount for NonNativeToken, amount should be ZERO - // 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress - Amount( - currencySymbol = currency.symbol, - value = BigDecimal.ZERO, - decimals = currency.decimalCount, - ) - } - } - } - private fun convertNonNativeToken(token: Currency.NonNativeToken): Token { return Token( name = token.name, 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 6c5f931d5e..de352faaab 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 @@ -36,20 +36,23 @@ import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.walletmanager.utils.WalletManagerFactory import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import java.util.EnumSet -@Suppress("LargeClass", "TooManyFunctions", "LongParameterList") +@Suppress("LargeClass", "TooManyFunctions") // 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 assetLoader: AssetLoader, + private val dispatchers: CoroutineDispatcherProvider, blockchainSDKFactory: BlockchainSDKFactory, ) : WalletManagersFacade { @@ -467,7 +470,7 @@ class DefaultWalletManagersFacade( amount: Amount, userWalletId: UserWalletId, network: Network, - ): Result? { + ): Result? = withContext(dispatchers.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, @@ -477,7 +480,7 @@ class DefaultWalletManagersFacade( val destination = estimationFeeAddressFactory.makeAddress(blockchain) - return (walletManager as? TransactionSender)?.estimateFee( + (walletManager as? TransactionSender)?.estimateFee( amount = amount, destination = destination, ) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt index a33088d612..541f04b900 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -12,10 +12,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import java.math.BigDecimal /** @@ -23,7 +21,6 @@ import java.math.BigDecimal */ class EstimateFeeUseCase( private val walletManagersFacade: WalletManagersFacade, - private val dispatcher: CoroutineDispatcherProvider, ) { suspend operator fun invoke( amount: BigDecimal, @@ -43,7 +40,7 @@ class EstimateFeeUseCase( null -> GetFeeError.UnknownError.left() } emit(maybeFee) - }.flowOn(dispatcher.io) + } } private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt index 56c89954a4..37a6a4f1c6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -16,7 +16,7 @@ class ValidateTransactionUseCase( @Suppress("LongParameterList") suspend operator fun invoke( amount: Amount, - fee: Fee, + fee: Fee?, memo: String?, destination: String, userWalletId: UserWalletId, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 9babfe5a73..a18cb2399c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -24,13 +24,12 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount -import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.CreateTransactionUseCase import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -46,7 +45,6 @@ import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -63,24 +61,19 @@ internal class SwapInteractorImpl @Inject constructor( private val allowPermissionsHandler: AllowPermissionsHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, - private val walletManagersFacade: WalletManagersFacade, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val quotesRepository: QuotesRepository, - private val dispatcher: CoroutineDispatcherProvider, private val swapTransactionRepository: SwapTransactionRepository, private val currencyChecksRepository: CurrencyChecksRepository, private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, private val demoConfig: DemoConfig, - private val transactionRepository: TransactionRepository, + private val validateTransactionUseCase: ValidateTransactionUseCase, + private val estimateFeeUseCase: EstimateFeeUseCase, ) : SwapInteractor { - private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { - EstimateFeeUseCase(walletManagersFacade, dispatcher) - } - private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) { GetSelectedAppCurrencyUseCase(appCurrencyRepository) } @@ -475,34 +468,33 @@ internal class SwapInteractorImpl @Inject constructor( userWalletId: UserWalletId, minAdaValue: BigDecimal?, ) { - transactionRepository.validateTransaction( + validateTransactionUseCase( amount = amount.value.convertToAmount(fromToken), fee = null, memo = null, destination = getTokenAddress(fromToken), userWalletId = userWalletId, network = fromToken.network, - ) - .fold( - onFailure = { - addCardanoTransactionValidationError( - warnings = warnings, - error = it as? BlockchainSdkError.Cardano ?: return@fold, - fromToken = fromToken, - userWalletId = userWalletId, + ).fold( + ifLeft = { + addCardanoTransactionValidationError( + warnings = warnings, + error = it as? BlockchainSdkError.Cardano ?: return@fold, + fromToken = fromToken, + userWalletId = userWalletId, + ) + }, + ifRight = { + minAdaValue?.let { + warnings.add( + Warning.Cardano.MinAdaValueCharged( + tokenName = fromToken.name, + minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), + ), ) - }, - onSuccess = { - minAdaValue?.let { - warnings.add( - Warning.Cardano.MinAdaValueCharged( - tokenName = fromToken.name, - minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), - ), - ) - } - }, - ) + } + }, + ) } private suspend fun addCardanoTransactionValidationError( @@ -706,28 +698,27 @@ internal class SwapInteractorImpl @Inject constructor( val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError - val txExtras = transactionManager.getMemoExtras( - currencyToSend.currency.network.backendId, - exchangeDataCex.txExtraId, - ) val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError - if (txExtras == null && exchangeDataCex.txExtraId != null && !demoConfig.isDemoCardId(cardId)) { - return SwapTransactionState.UnknownError - } + if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.UnknownError + val txData = createTransactionUseCase( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction( fee = txFee, blockchain = Blockchain.fromId(currencyToSend.currency.network.id.value), ), - memo = null, + memo = exchangeDataCex.txExtraId, destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, ).getOrElse { Timber.e(it) return SwapTransactionState.UnknownError - }.copy(extras = txExtras) + } + + if (txData.extras == null && exchangeDataCex.txExtraId != null) { + return SwapTransactionState.UnknownError + } val result = sendTransactionUseCase( txData = txData, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 1a6a6e8b22..daa4bcc969 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -12,7 +12,9 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.CreateTransactionUseCase +import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -46,11 +48,10 @@ class SwapDomainModule { swapTransactionRepository: SwapTransactionRepository, appCurrencyRepository: AppCurrencyRepository, currencyChecksRepository: CurrencyChecksRepository, - walletManagersFacade: WalletManagersFacade, - coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, currenciesRepository: CurrenciesRepository, - transactionRepository: TransactionRepository, + validateTransactionUseCase: ValidateTransactionUseCase, + estimateFeeUseCase: EstimateFeeUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -62,15 +63,14 @@ class SwapDomainModule { sendTransactionUseCase = sendTransactionUseCase, createTransactionUseCase = createTransactionUseCase, quotesRepository = quotesRepository, - walletManagersFacade = walletManagersFacade, - dispatcher = coroutineDispatcherProvider, swapTransactionRepository = swapTransactionRepository, appCurrencyRepository = appCurrencyRepository, currencyChecksRepository = currencyChecksRepository, currenciesRepository = currenciesRepository, initialToCurrencyResolver = initialToCurrencyResolver, demoConfig = DemoConfig(), - transactionRepository = transactionRepository, + validateTransactionUseCase = validateTransactionUseCase, + estimateFeeUseCase = estimateFeeUseCase, ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 6b69d96fb8..6a3ab8f374 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,6 +1,5 @@ package com.tangem.lib.crypto -import com.tangem.blockchain.common.TransactionExtras import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import java.math.BigDecimal @@ -14,22 +13,6 @@ interface TransactionManager { analyticsData: AnalyticsData, ): SendTxResult - /** - * Send transaction - * - * @param txData data to build a tx - * @param derivationPath for select right walletManager - * @param analyticsData data for send analytics event - * @return result of transaction - */ - @Throws(IllegalStateException::class) - suspend fun sendTransaction( - txData: SwapTxData, - isSwap: Boolean, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult - /** * Get fee * @@ -57,8 +40,6 @@ interface TransactionManager { @Throws(IllegalStateException::class) suspend fun updateWalletManager(networkId: String, derivationPath: String?) - fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal - /** * In app blockchain id, actual in blockchain sdk, not the same as networkId * @@ -69,7 +50,4 @@ interface TransactionManager { @Throws(IllegalStateException::class) fun getExplorerTransactionLink(networkId: String, txAddress: String): String - - // TODO: move to another place to use as in Send feature - fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? } \ No newline at end of file From 9c5490bcedf3aa56634a957b32440dffdd1781e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 20:25:24 +0500 Subject: [PATCH 003/223] Updated on 2026-08-14 --- app/build.gradle.kts | 5 +++++ app/src/main/AndroidManifest.xml | 1 - .../src/main/java/com/tangem/core/ui/res/TangemColors.kt | 1 - gradle/dependencies.toml | 7 +++---- .../extension/LibraryExtensionConfigurations.kt | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8615f2502..417f9ea050 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,6 +14,11 @@ android { testOptions { animationsDisabled = true } + packaging { + jniLibs { + useLegacyPackaging = true + } + } } configurations.all { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d52735ce7..1cd7e794eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,7 +44,6 @@ android:largeHeap="@bool/largeHeap" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher" - android:extractNativeLibs="true" android:supportsRtl="true" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:allowBackup, android:fullBackupContent, android:label"> diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 451207be18..5ff9fc7e8d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -83,7 +83,6 @@ class TangemColors internal constructor( warning: Color, attention: Color, accent: Color = TangemColorPalette.Azure, - constant: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) private set diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5063455aba..8a1c580d07 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -4,8 +4,8 @@ [versions] # region Classpath androidGradlePlugin = "8.2.2" -firebaseCrashlytics = "2.9.4" -googleServices = "4.3.10" +firebaseCrashlytics = "3.0.1" +googleServices = "4.4.1" kotlin = "1.9.22" # endregion Classpath @@ -44,7 +44,7 @@ coil = "2.1.0" compose-shimmer = "1.0.3" coroutine = "1.7.2" desugarJdkLibs = "1.1.5" -firebase = "26.0.0" +firebase = "33.1.0" googleMaterialComponent = "1.6.1" googlePlayCore = "1.10.3" googlePlayCoreKtx = "1.8.1" @@ -130,7 +130,6 @@ room = { id = "androidx.room", version.ref = "room" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } -gradle-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebaseCrashlytics" } # end region Classpath # region AndroidX diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt index 053151f9f1..f82cfcf71e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt @@ -39,7 +39,7 @@ private fun LibraryExtension.configureBuildTypes() { } private fun LibraryExtension.configurePackagingOptions() { - packagingOptions { + packaging { resources { excludes += "lib/x86_64/darwin/libscrypt.dylib" excludes += "lib/x86_64/freebsd/libscrypt.so" From a2792223a37e9b1304f895e0ae5c7b8c11cfaa80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 20:57:05 +0300 Subject: [PATCH 004/223] Updated on 2026-08-14 --- common/ui-charts/.gitignore | 1 + common/ui-charts/build.gradle.kts | 24 ++ .../tangem/common/ui/charts/MarketChart.kt | 8 + .../ui/charts/layer/MarketChartLayer.kt | 269 ++++++++++++++++++ .../common/ui/charts/marker/ChartMarker.kt | 135 +++++++++ .../common/ui/charts/preview/PreviewData.kt | 77 +++++ gradle/dependencies.toml | 6 + .../extension/BaseExtensionConfigurations.kt | 1 + settings.gradle.kts | 13 + 9 files changed, 534 insertions(+) create mode 100644 common/ui-charts/.gitignore create mode 100644 common/ui-charts/build.gradle.kts create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt diff --git a/common/ui-charts/.gitignore b/common/ui-charts/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/common/ui-charts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts new file mode 100644 index 0000000000..62c6c0de87 --- /dev/null +++ b/common/ui-charts/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.ui.charts" +} + +dependencies { + /** Project - Core */ + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.tangem.vico.core) + implementation(deps.tangem.vico.compose) + implementation(deps.tangem.vico.compose.m3) + + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt new file mode 100644 index 0000000000..31f0f55f2b --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -0,0 +1,8 @@ +package com.tangem.common.ui.charts + +import androidx.compose.runtime.Composable + +@Composable +fun MarketChart() { + TODO() +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt new file mode 100644 index 0000000000..239f0f0665 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt @@ -0,0 +1,269 @@ +package com.tangem.common.ui.charts.layer + +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.fullWidth +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLineSpec +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState +import com.patrykandpatrick.vico.compose.common.shader.BrushShader +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import com.patrykandpatrick.vico.core.common.shader.ColorShader +import com.patrykandpatrick.vico.core.common.shader.DynamicShader +import com.tangem.common.ui.charts.preview.getPreviewBigCartData +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Creates and remembers a LineCartesianLayer for a chart with specific characteristics. + * + * @param lineColor The color of the main line in the chart. + * @param backgroundLineColor The color of the line's background. + * @param secondLineColor The color of the line for the second part of the chart. + * @param backgroundSecondLineColor The color of the line's background for the second part of the chart. + * @param startDrawingAnimation A mutable state that triggers the start of the drawing animation when set to true. + * @param axisValueOverrider An AxisValueOverrider that provides custom values for the axis. + * @param secondColorOnTheRightSide A boolean that determines if the second color should be on the right side of the chart. Default is false. + * @param markerFraction A float between 0.0 and 1.0 that represents the fraction of the chart where the marker is located. Default is null. + * + * @return A LineCartesianLayer that represents a layer in a chart with the specified characteristics. + */ +@Suppress("LongParameterList") +@Composable +fun rememberMarketChartLayer( + lineColor: Color, + backgroundLineColor: Color, + secondLineColor: Color, + backgroundSecondLineColor: Color, + startDrawingAnimation: MutableState, + axisValueOverrider: AxisValueOverrider, + secondColorOnTheRightSide: Boolean = false, + @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, +): LineCartesianLayer { + var animationFraction: Float? by remember { mutableStateOf(null) } + + LaunchedEffect(startDrawingAnimation.value) { + if (startDrawingAnimation.value) { + animate( + initialValue = 0f, + targetValue = 1f, + animationSpec = tween(easing = LinearEasing, durationMillis = 1000), + ) { start, _ -> + if (start == 1f) { + animationFraction = null + startDrawingAnimation.value = false + } else { + animationFraction = start + } + } + } + } + + return rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = backgroundLineColor, + secondLineColor = secondLineColor, + backgroundSecondLineColor = backgroundSecondLineColor, + axisValueOverrider = axisValueOverrider, + secondColorOnTheRightSide = secondColorOnTheRightSide, + markerFraction = markerFraction, + animationFraction = animationFraction, + ) +} + +@Composable +private fun rememberRawMarketChartLayer( + lineColor: Color, + backgroundLineColor: Color, + secondLineColor: Color, + backgroundSecondLineColor: Color, + axisValueOverrider: AxisValueOverrider, + secondColorOnTheRightSide: Boolean = false, + @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, + @FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null, +): LineCartesianLayer { + val backgroundColorLineGradient = listOf(backgroundLineColor, Color.Transparent) + val backgroundSecondLineColorGradient = listOf(backgroundSecondLineColor, Color.Transparent) + + val markerSet = markerFraction != null + val animationRunning = animationFraction != null && animationFraction != 1f + + val layerColors = remember(animationRunning, markerSet, secondColorOnTheRightSide) { + when { + !animationRunning && markerSet && secondColorOnTheRightSide -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = secondLineColor, + backLineColorRight = backgroundSecondLineColorGradient, + ) + } + !animationRunning && markerSet && !secondColorOnTheRightSide -> { + LayerColors( + lineColor = secondLineColor, + backLineColor = backgroundSecondLineColorGradient, + lineColorRight = lineColor, + backLineColorRight = backgroundColorLineGradient, + ) + } + animationRunning -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = Color.Transparent, + backLineColorRight = listOf(Color.Transparent, Color.Transparent), + ) + } + else -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + ) + } + } + } + + return rememberLayer( + fractionValue = animationFraction ?: markerFraction, + axisValueOverrider = axisValueOverrider, + layerColors = layerColors, + ) +} + +private data class LayerColors( + val lineColor: Color, + val backLineColor: List, + val lineColorRight: Color? = null, + val backLineColorRight: List? = null, +) + +@Composable +private fun rememberLayer( + fractionValue: Float?, + axisValueOverrider: AxisValueOverrider, + layerColors: LayerColors, +): LineCartesianLayer { + return rememberLineCartesianLayer( + listOf( + if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) { + rememberLineSpec( + shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) }, + backgroundShader = remember(layerColors.backLineColor) { + BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor)) + }, + ) + } else { + rememberSplitLineSpec( + shader = remember(layerColors.lineColor, layerColors.lineColorRight, fractionValue) { + DynamicShader.Companion.horizontalGradient( + colors = intArrayOf(layerColors.lineColor.toArgb(), layerColors.lineColorRight.toArgb()), + positions = floatArrayOf(fractionValue, fractionValue), + ) + }, + backgroundShaderFirst = remember(layerColors.backLineColor) { + BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor)) + }, + backgroundShaderSecond = remember(layerColors.backLineColorRight) { + BrushShader(brush = Brush.verticalGradient(layerColors.backLineColorRight)) + }, + xSplitFraction = fractionValue, + ) + }, + ), + axisValueOverrider = axisValueOverrider, + ) +} + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LayerChartPreview() { + val xListKey = ExtraStore.Key>() + val model = getPreviewBigCartData(xListKey) + + TangemThemePreview { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(48.dp), + ) { + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = Color.Blue, + backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + secondColorOnTheRightSide = true, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = Color.Blue, + backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = true, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = Color.Blue, + backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + secondLineColor = Color.Blue, + backgroundSecondLineColor = Color.Blue.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = true, + animationFraction = 0.7f, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt new file mode 100644 index 0000000000..4675085e2a --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt @@ -0,0 +1,135 @@ +package com.tangem.common.ui.charts.marker + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.fullWidth +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState +import com.patrykandpatrick.vico.compose.common.component.rememberLayeredComponent +import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent +import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent +import com.patrykandpatrick.vico.compose.common.of +import com.patrykandpatrick.vico.compose.common.shader.color +import com.patrykandpatrick.vico.compose.common.shape.dashed +import com.patrykandpatrick.vico.core.cartesian.* +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker +import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.component.TextComponent +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import com.patrykandpatrick.vico.core.common.shader.DynamicShader +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.common.ui.charts.preview.getPreviewBigCartData +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * @param color The color of the indicator and guideline. + * @param innerCircleColor The color of the inner circle of the indicator. + * + * @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect. + */ +@Composable +internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): CartesianMarker { + val indicatorFrontComponent = rememberShapeComponent( + shape = Shape.Pill, + color = innerCircleColor, + ) + val indicatorCenterComponent = rememberShapeComponent( + shape = Shape.Pill, + color = color, + ) + val indicatorRearComponent = rememberShapeComponent( + shape = Shape.Pill, + color = color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA), + ) + val indicator = rememberLayeredComponent( + rear = indicatorRearComponent, + front = rememberLayeredComponent( + rear = indicatorCenterComponent, + front = indicatorFrontComponent, + padding = indicatorPadding, + ), + padding = indicatorPadding, + ) + val guideline = rememberUnboundedLineComponent( + color = color, + verticalAddDrawSpace = TangemTheme.dimens.spacing24, + shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) }, + ) + return remember(indicator, guideline) { + object : DefaultCartesianMarker( + label = TextComponent.build { textSizeSp = 0f }, + indicator = indicator, + indicatorSizeDp = INDICATOR_SIZE_DP, + guideline = guideline, + ) { + override fun getInsets( + context: CartesianMeasureContext, + outInsets: Insets, + horizontalDimensions: HorizontalDimensions, + ) { + with(context) { + super.getInsets(context, outInsets, horizontalDimensions) + val baseShadowInsetDp = + CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP + outInsets.top += (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels + outInsets.bottom += (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels + } + } + } + } +} + +private val indicatorPadding = Dimensions.of(3.dp) +private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f +private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f +private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f +private const val INDICATOR_SIZE_DP = 16f +private const val INDICATOR_REAR_COLOR_ALPHA = .24f + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemChartMarkerPreview() { + val marker = rememberTangemChartMarker(Color.Red, Color.White) + val xListKey = ExtraStore.Key>() + val model = getPreviewBigCartData(xListKey) + val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f + val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it } + + TangemThemePreview { + Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberLineCartesianLayer( + listOf(rememberLineSpec(shader = DynamicShader.color(Color.Blue))), + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + ), + persistentMarkers = mapOf(center to marker), + ), + model = model, + marker = marker, + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + ) + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt new file mode 100644 index 0000000000..0b90226888 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt @@ -0,0 +1,77 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.common.ui.charts.preview + +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import com.patrykandpatrick.vico.core.common.data.MutableExtraStore + +internal fun getPreviewBigCartData(xListKey: ExtraStore.Key>): CartesianChartModel { + val y = listOf( + 59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917, + 63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680, + 63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183, + 62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065, + 62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158, + 61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372, + 62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540, + 61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838, + 65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845, + 67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273, + 66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720, + 69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714, + 69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517, + 68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821, + 68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630, + 68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567, + 68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121, + 67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709, + ) + val timestamps: List = listOf( + 1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000, + 1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000, + 1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000, + 1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000, + 1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000, + 1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000, + 1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000, + 1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000, + 1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000, + 1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000, + 1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000, + 1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000, + 1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000, + 1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000, + 1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000, + 1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000, + 1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000, + 1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000, + 1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000, + 1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000, + 1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000, + 1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000, + 1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000, + 1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000, + 1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000, + 1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000, + 1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000, + 1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000, + 1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000, + 1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000, + 1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000, + ) + + val x = List(y.size) { index -> index } + + return CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }).copy( + extraStore = MutableExtraStore().apply { + set(xListKey, timestamps) + }, + ) +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 8a1c580d07..3992a40b54 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -91,6 +91,8 @@ tangemBlockchainSdk = "develop-654" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-360" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ +tangemVico = "2.0.0-alpha.21-tangem13" +#tangemVico = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem # region Tools @@ -180,6 +182,10 @@ firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" tangem-blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } tangem-card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } tangem-card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } + +tangem-vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } +tangem-vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } +tangem-vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } # endregion Tangem # region Detekt diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index c47360c0fc..67de36d6df 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -20,6 +20,7 @@ internal fun BaseExtension.configureCompilerOptions() { internal fun BaseExtension.configureCompose(project: Project) { val useCompose = with(project.path) { contains(":ui") || + contains(":common:ui-charts") || contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] diff --git a/settings.gradle.kts b/settings.gradle.kts index 9f9643c29f..0b65a0a105 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,6 +39,7 @@ dependencyResolutionManagement { includeModule("com.tangem", "blockchain") includeModule("com.tangem", "wallet-core-proto") includeModule("com.tangem", "wallet-core") + includeModule("com.tangem", "vico") } } maven { @@ -80,6 +81,17 @@ dependencyResolutionManagement { includeModule("com.tangem", "wallet-core") } } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/vico") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { + includeGroupAndSubgroups("com.tangem.vico") + } + } jcenter { // unable to replace with mavenCentral() due to rekotlin content { includeModule("org.rekotlin", "rekotlin") @@ -100,6 +112,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") include(":common") +include(":common:ui-charts") // region Core modules include(":core:analytics") From c211d221ea49cc438bc8ae64f3ebcd809d92b83e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 12:09:55 +0400 Subject: [PATCH 005/223] Updated on 2026-08-14 --- .../tangem/data/visa/DefaultVisaRepository.kt | 36 +++----- .../com/tangem/data/visa/config/VisaConfig.kt | 25 ++++++ .../tangem/data/visa/config/VisaLibLoader.kt | 86 +++++++++++++++++++ .../data/visa/di/ImplementedVisaDataModule.kt | 35 ++------ .../tangem/data/visa/utils/CurrencyUtils.kt | 4 +- .../com/tangem/data/visa/utils/VisaConfig.kt | 16 ---- .../tangem/data/visa/utils/VisaConstants.kt | 44 ++++++++++ .../data/visa/utils/VisaCurrencyFactory.kt | 4 +- .../SetBalancesAndLimitsTransformer.kt | 31 +++---- .../TypedWalletStateTransformer.kt | 22 +++++ .../transformers/WalletStateTransformer.kt | 12 +-- .../lib/visa/VisaContractInfoProvider.kt | 22 ++--- .../com/tangem/lib/visa/api/VisaApiBuilder.kt | 19 +++- .../utils/{VisaConfig.kt => Constants.kt} | 8 +- 14 files changed, 251 insertions(+), 113 deletions(-) create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt delete mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt create mode 100644 data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt rename libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/{VisaConfig.kt => Constants.kt} (55%) diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index a817781b53..e558b2def0 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -10,10 +10,8 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.utils.VisaConfig -import com.tangem.data.visa.utils.VisaCurrencyFactory -import com.tangem.data.visa.utils.VisaTxDetailsFactory -import com.tangem.data.visa.utils.VisaTxHistoryPagingSource +import com.tangem.data.visa.config.VisaLibLoader +import com.tangem.data.visa.utils.* import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -24,8 +22,6 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.visa.repository.VisaRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApi import com.tangem.lib.visa.model.VisaTxHistoryResponse import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -35,9 +31,8 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal internal class DefaultVisaRepository( - private val visaContractInfoProvider: VisaContractInfoProvider, + private val visaLibLoader: VisaLibLoader, private val tangemTechApi: TangemTechApi, - private val visaApi: VisaApi, private val cacheRegistry: CacheRegistry, private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, @@ -76,9 +71,11 @@ internal class DefaultVisaRepository( } private suspend fun fetchVisaCurrency(address: String) { + val contractInfoProvider = visaLibLoader.getOrCreateProvider() + parZip( dispatchers.io, - { visaContractInfoProvider.getContractInfo(address) }, + { contractInfoProvider.getContractInfo(address) }, { getFiatRate() }, { contractInfo, fiatRate -> fetchedCurrencies.update { value -> @@ -97,6 +94,7 @@ internal class DefaultVisaRepository( ): Flow> { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet) + val api = visaLibLoader.getOrCreateApi() val pager = Pager( config = PagingConfig( pageSize = pageSize, @@ -110,7 +108,7 @@ internal class DefaultVisaRepository( isRefresh = isRefresh, ), cacheRegistry = cacheRegistry, - visaApi = visaApi, + visaApi = api, fetchedItems = fetchedHistoryItems, dispatchers = dispatchers, ) @@ -137,7 +135,7 @@ internal class DefaultVisaRepository( } private suspend fun makeAddress(userWalletId: UserWalletId): String { - if (IS_DEMO_MODE_ENABLED) return DEMO_ADDRESS + if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress() val userWallet = findVisaUserWallet(userWalletId) val walletAddresses = makeWalletAddresses(userWallet) @@ -149,13 +147,13 @@ internal class DefaultVisaRepository( } private suspend fun getFiatRate(): BigDecimal? { - val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase() + val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase() val quotes = tangemTechApi.getQuotes( currencyId = fiatCurrencyId, - coinIds = VisaConfig.TOKEN_ID, + coinIds = VisaConstants.TOKEN_ID, ).getOrThrow() - return quotes.quotes[VisaConfig.TOKEN_ID]?.price + return quotes.quotes[VisaConstants.TOKEN_ID]?.price } private fun makeWalletAddresses(userWallet: UserWallet): Set
{ @@ -165,7 +163,7 @@ internal class DefaultVisaRepository( } private fun getCardPubKey(userWallet: UserWallet): String { - if (IS_DEMO_MODE_ENABLED) return DEMO_PUBLIC_KEY + if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey() val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 @@ -189,12 +187,4 @@ internal class DefaultVisaRepository( private fun getVisaCurrencyKey(address: String): String { return "visa_currency_$address" } - - private companion object { - // Must be `false` in production - const val IS_DEMO_MODE_ENABLED = false - - const val DEMO_ADDRESS = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" - const val DEMO_PUBLIC_KEY = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" - } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt new file mode 100644 index 0000000000..8c5dba275a --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt @@ -0,0 +1,25 @@ +package com.tangem.data.visa.config + +import com.squareup.moshi.Json + +internal data class VisaConfig( + @Json(name = "testnet") + val testnet: Addresses, + @Json(name = "mainnet") + val mainnet: Addresses, + @Json(name = "txHistoryAPIAdditionalHeaders") + val header: Header, +) { + + data class Addresses( + @Json(name = "paymentAccountRegistry") + val paymentAccountRegistry: String, + @Json(name = "bridgeProcessor") + val bridgeProcessor: String, + ) + + data class Header( + @Json(name = "x-asn") + val xAsn: String, + ) +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt new file mode 100644 index 0000000000..e5062cf662 --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt @@ -0,0 +1,86 @@ +package com.tangem.data.visa.config + +import com.squareup.moshi.Moshi +import com.tangem.data.visa.BuildConfig +import com.tangem.data.visa.utils.VisaConstants +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApi +import com.tangem.lib.visa.api.VisaApiBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import javax.inject.Inject + +internal class VisaLibLoader @Inject constructor( + private val assetLoader: AssetLoader, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val createMutex = Mutex() + + private var config: VisaConfig? = null + + private var provider: VisaContractInfoProvider? = null + private var api: VisaApi? = null + + suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider() + + suspend fun getOrCreateApi(): VisaApi = api ?: createApi() + + private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock { + val config = getOrLoadConfig() + + provider = VisaContractInfoProvider.Builder( + useTestnetRpc = VisaConstants.USE_TEST_ENV, + bridgeProcessorAddress = if (VisaConstants.USE_TEST_ENV) { + config.testnet.bridgeProcessor + } else { + config.mainnet.bridgeProcessor + }, + paymentAccountRegistryAddress = if (VisaConstants.USE_TEST_ENV) { + config.testnet.paymentAccountRegistry + } else { + config.mainnet.paymentAccountRegistry + }, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + dispatchers = dispatchers, + ).build() + + return requireNotNull(provider) { + "Visa provider is not created" + } + } + + private suspend fun createApi(): VisaApi = createMutex.withLock { + val config = getOrLoadConfig() + + api = VisaApiBuilder( + useDevApi = VisaConstants.USE_TEST_ENV, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + moshi = moshi, + headers = mapOf( + X_ASN_HEADER_NAME to config.header.xAsn, + ), + ).build() + + return requireNotNull(api) { + "Visa API is not created" + } + } + + private suspend fun getOrLoadConfig(): VisaConfig { + config = assetLoader.load(VISA_CONFIG_FILE_NAME) + + return requireNotNull(config) { + "Visa config is not found" + } + } + + companion object { + private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config" + private const val X_ASN_HEADER_NAME = "x-asn" + } +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt index a3022a444a..60d487838e 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -1,15 +1,11 @@ package com.tangem.data.visa.di -import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.BuildConfig import com.tangem.data.visa.DefaultVisaRepository +import com.tangem.data.visa.config.VisaLibLoader import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.visa.repository.VisaRepository -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApiBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,29 +21,16 @@ internal object ImplementedVisaDataModule { @Singleton @ImplementedVisaRepository fun provideVisaRepository( - @NetworkMoshi moshi: Moshi, + visaLibLoader: VisaLibLoader, tangemTechApi: TangemTechApi, cacheRegistry: CacheRegistry, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, - ): VisaRepository { - val contractInfoProvider = VisaContractInfoProvider.Builder( - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - dispatchers = dispatchers, - ).build() - val visaApi = VisaApiBuilder( - useDevApi = true, - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - moshi = moshi, - ).build() - - return DefaultVisaRepository( - contractInfoProvider, - tangemTechApi, - visaApi, - cacheRegistry, - userWalletsStore, - dispatchers, - ) - } + ): VisaRepository = DefaultVisaRepository( + visaLibLoader, + tangemTechApi, + cacheRegistry, + userWalletsStore, + dispatchers, + ) } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt index b664ad4320..bcc9aeb311 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt @@ -7,9 +7,9 @@ import java.util.Currency internal fun findCurrencyByNumericCode(code: Int): Currency { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code } - ?: Currency.getInstance(VisaConfig.fiatCurrency.code) + ?: Currency.getInstance(VisaConstants.fiatCurrency.code) } else { Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}") - Currency.getInstance(VisaConfig.fiatCurrency.code) + Currency.getInstance(VisaConstants.fiatCurrency.code) } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt deleted file mode 100644 index 6b3e3f3f8b..0000000000 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.data.visa.utils - -import com.tangem.domain.appcurrency.model.AppCurrency - -internal object VisaConfig { - - const val NETWORK_NAME = "Polygon PoS" - - const val TOKEN_ID = "tether" - - val fiatCurrency = AppCurrency( - code = "EUR", - name = "Euro", - symbol = "€", - ) -} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt new file mode 100644 index 0000000000..27cd336588 --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt @@ -0,0 +1,44 @@ +package com.tangem.data.visa.utils + +import com.tangem.domain.appcurrency.model.AppCurrency + +internal object VisaConstants { + + const val NETWORK_NAME = "Polygon PoS" + + const val TOKEN_ID = "tether" + + val fiatCurrency = AppCurrency( + code = "EUR", + name = "Euro", + symbol = "€", + ) + + /* + * Must be `false` in production + * Don't forget to change CardTypesResolver.isVisaWallet + * */ + const val IS_DEMO_MODE_ENABLED = false + + const val USE_TEST_ENV = true + + const val DEMO_TESTNET_ADDRESS = "0x51d034eb1563d0d2e66379ef37756d3c14936c44" + const val DEMO_TESTNET_PUBLIC_KEY = "03FA1122B809079F79C4E0F657FE11337FEC88C3FB3C6341B2CE2E4F5D9241DD86" + + const val DEMO_MAINNET_ADDRESS = "0x927e3ef2b3d85bacf9e520379f64f6627d323fcd" + const val DEMO_MAINNET_PUBLIC_KEY = "02AC61CD57B8011BEE8BB489FB744845CC113AD379132C56015EE70528B6A88E92" +} + +internal fun getDemoAddress(): String { + return if (VisaConstants.USE_TEST_ENV) { + VisaConstants.DEMO_TESTNET_ADDRESS + } else { + VisaConstants.DEMO_MAINNET_ADDRESS + } +} + +internal fun getDemoPublicKey(): String { + return if (VisaConstants.USE_TEST_ENV) { + VisaConstants.DEMO_TESTNET_PUBLIC_KEY + } else VisaConstants.DEMO_MAINNET_PUBLIC_KEY +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 3c9f55e37f..6398dcec84 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -21,10 +21,10 @@ internal class VisaCurrencyFactory { return VisaCurrency( symbol = contractInfo.token.symbol, - networkName = VisaConfig.NETWORK_NAME, + networkName = VisaConstants.NETWORK_NAME, decimals = contractInfo.token.decimals, fiatRate = fiatRate, - fiatCurrency = VisaConfig.fiatCurrency, + fiatCurrency = VisaConstants.fiatCurrency, balances = with(contractInfo) { VisaCurrency.Balances( total = balances.total, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index ea53efb4c6..d42d27f165 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -19,24 +19,25 @@ internal class SetBalancesAndLimitsTransformer( private val userWallet: UserWallet, private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntents, -) : WalletStateTransformer(userWallet.walletId) { +) : TypedWalletStateTransformer( + userWalletId = userWallet.walletId, + targetStateClass = WalletState.Visa.Content::class, +) { - override fun transform(prevState: WalletState): WalletState { - return prevState.transformWhenInState { state -> - val visaCurrency = maybeVisaCurrency.getOrElse { - return state.copy( - walletCardState = getErrorWalletCardState(state.walletCardState), - depositButtonState = state.depositButtonState.copy(isEnabled = false), - balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, - ) - } - - state.copy( - walletCardState = getContentWalletCardState(state.walletCardState, visaCurrency), - depositButtonState = state.depositButtonState.copy(isEnabled = true), - balancesAndLimitBlockState = getContentBlockState(visaCurrency), + override fun transformTyped(prevState: WalletState.Visa.Content): WalletState { + val visaCurrency = maybeVisaCurrency.getOrElse { + return prevState.copy( + walletCardState = getErrorWalletCardState(prevState.walletCardState), + depositButtonState = prevState.depositButtonState.copy(isEnabled = false), + balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } + + return prevState.copy( + walletCardState = getContentWalletCardState(prevState.walletCardState, visaCurrency), + depositButtonState = prevState.depositButtonState.copy(isEnabled = true), + balancesAndLimitBlockState = getContentBlockState(visaCurrency), + ) } private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt new file mode 100644 index 0000000000..2c72901066 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlin.reflect.KClass + +internal abstract class TypedWalletStateTransformer( + userWalletId: UserWalletId, + protected val targetStateClass: KClass, +) : WalletStateTransformer(userWalletId) { + + abstract fun transformTyped(prevState: S): WalletState + + @Suppress("UNCHECKED_CAST") + final override fun transform(prevState: WalletState): WalletState { + return if (prevState::class == targetStateClass) { + transformTyped(prevState as S) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index f02bd16531..bcbd84cf28 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -4,7 +4,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber internal abstract class WalletStateTransformer( protected val userWalletId: UserWalletId, @@ -12,7 +11,7 @@ internal abstract class WalletStateTransformer( abstract fun transform(prevState: WalletState): WalletState - override fun transform(prevState: WalletScreenState): WalletScreenState { + final override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = prevState.wallets .map { state -> @@ -21,13 +20,4 @@ internal abstract class WalletStateTransformer( .toImmutableList(), ) } - - protected inline fun WalletState.transformWhenInState( - transform: (state: S) -> WalletState, - ): WalletState = if (this is S) { - transform(this) - } else { - Timber.w("Impossible to transform ${this::class.simpleName} because current is ${S::class.simpleName}") - this - } } \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index 7ae3b7e0dc..4696b698fb 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -3,8 +3,8 @@ package com.tangem.lib.visa import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import com.tangem.lib.visa.model.VisaContractInfo -import com.tangem.lib.visa.utils.VisaConfig -import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG +import com.tangem.lib.visa.utils.Constants +import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG import com.tangem.lib.visa.utils.toHexString import com.tangem.utils.coroutines.CoroutineDispatcherProvider import okhttp3.OkHttpClient @@ -24,16 +24,16 @@ interface VisaContractInfoProvider { suspend fun getContractInfo(walletAddress: String): VisaContractInfo class Builder( + private val useTestnetRpc: Boolean, + private val bridgeProcessorAddress: String, + private val paymentAccountRegistryAddress: String, private val isNetworkLoggingEnabled: Boolean, private val dispatchers: CoroutineDispatcherProvider, - private val baseUrl: String = VisaConfig.BASE_RPC_URL, - private val bridgeProcessorAddress: String = VisaConfig.BRIDGE_PROCESSOR_CONTRACT_ADDRESS, - private val paymentAccountRegistryAddress: String = VisaConfig.PAYMENT_ACCOUNT_REGISTRY_ADDRESS, - private val chainId: Long = VisaConfig.CHAIN_ID, - private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, - private val decimals: Int = VisaConfig.DECIMALS, - private val gasLimit: Long = VisaConfig.GAS_LIMIT, - private val privateKey: String = ByteArray(VisaConfig.PRIVATE_KEY_LENGTH).toHexString(), + private val chainId: Long = Constants.CHAIN_ID, + private val decimals: Int = Constants.DECIMALS, + private val gasLimit: Long = Constants.GAS_LIMIT, + private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS, + private val privateKey: String = ByteArray(Constants.PRIVATE_KEY_LENGTH).toHexString(), ) { fun build(): VisaContractInfoProvider { @@ -52,6 +52,8 @@ interface VisaContractInfoProvider { } private fun createWeb3J(): Web3j { + val baseUrl: String = if (useTestnetRpc) Constants.TESTNET_RPC_URL else Constants.MAINNET_RPC_URL + val httpClient = OkHttpClient.Builder().apply { connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS) readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt index 50f200249b..ef1a99dd5f 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt @@ -5,8 +5,8 @@ import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory -import com.tangem.lib.visa.utils.VisaConfig -import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG +import com.tangem.lib.visa.utils.Constants +import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG import okhttp3.Interceptor import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -17,7 +17,8 @@ class VisaApiBuilder( private val useDevApi: Boolean, private val isNetworkLoggingEnabled: Boolean, private val moshi: Moshi, - private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, + private val headers: Map, + private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS, ) { fun build(): VisaApi { @@ -35,13 +36,23 @@ class VisaApiBuilder( if (isNetworkLoggingEnabled) { addInterceptor(createNetworkLoggingInterceptor()) } + + if (headers.isNotEmpty()) { + addInterceptor { chain -> + val request = chain.request().newBuilder().apply { + headers.forEach { (key, value) -> addHeader(key, value) } + }.build() + + chain.proceed(request) + } + } } return builder.build() } private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit { - val baseUrl = if (useDevApi) VisaConfig.VISA_API_DEV_URL else VisaConfig.VISA_API_PROD_URL + val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.VISA_API_PROD_URL return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt similarity index 55% rename from libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt rename to libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt index e3d0a9dab5..06e365e4ec 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt @@ -1,10 +1,10 @@ package com.tangem.lib.visa.utils -internal object VisaConfig { +internal object Constants { + + const val MAINNET_RPC_URL = "https://polygon-rpc.com/" + const val TESTNET_RPC_URL = "https://rpc-amoy.polygon.technology/" - const val BASE_RPC_URL = "https://polygon-mumbai.g.alchemy.com/v2/_1qqjXgBC_IikaXChnna8KTcV2eMMIQG/" - const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0xe32ecbbc1ec17fa9c160569cd613ad568ca50279" - const val PAYMENT_ACCOUNT_REGISTRY_ADDRESS = "0x3f4ae01073d1a9d5a92315fe118e57d1cdec7c44" const val CHAIN_ID = 80_001L const val DECIMALS = 9 const val GAS_LIMIT = 500_000_000L From 27a864a0a9b265f1238e9476a5c881656a40c312 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 14:41:05 +0200 Subject: [PATCH 006/223] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 13 +- .../tangem/tap/features/main/MainViewModel.kt | 11 ++ .../auth/DefaultStakeKitAuthProvider.kt | 2 +- core/datasource/build.gradle.kts | 1 + .../common/adapter/UnknownEnumMoshiAdapter.kt | 27 +++- .../datasource/api/stakekit/StakeKitApi.kt | 12 +- .../models/response/model/AddressArgument.kt | 4 +- .../response/model/StakingActionType.kt | 2 + .../stakekit/models/response/model/Yield.kt | 6 +- .../response/model/YieldBalanceWrapper.kt | 2 + .../models/response/model/YieldBalances.kt | 138 ------------------ ...eModule.kt => ExpressAssetsStoreModule.kt} | 10 +- .../com/tangem/datasource/di/MoshiModule.kt | 8 +- .../datasource/di/StakingTokensStoreModule.kt | 21 +++ ...sStore.kt => DefaultExpressAssetsStore.kt} | 4 +- .../local/token/DefaultStakingTokensStore.kt | 21 +++ .../{AssetsStore.kt => ExpressAssetsStore.kt} | 2 +- .../local/token/StakingTokensStore.kt | 10 ++ .../tangem/datasource/utils/RequestHeader.kt | 1 + .../data/staking/DefaultStakingRepository.kt | 16 ++ .../converters/StakingTokenConverter.kt | 22 +++ .../data/staking/di/StakingDataModule.kt | 6 + .../tangem/data/tokens/di/TokensDataModule.kt | 10 +- .../repository/DefaultCurrenciesRepository.kt | 6 +- .../DefaultMarketCryptoCurrencyRepository.kt | 6 +- .../staking/FetchStakingTokensUseCase.kt | 15 ++ .../domain/staking/model/StakingToken.kt | 9 ++ .../staking/model/StakingTokenWithYield.kt | 6 + .../staking/repositories/StakingRepository.kt | 2 + features/wallet/impl/build.gradle.kts | 1 + .../wallet/viewmodels/WalletViewModel.kt | 2 +- 31 files changed, 217 insertions(+), 179 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt rename core/datasource/src/main/java/com/tangem/datasource/di/{AssetsStoreModule.kt => ExpressAssetsStoreModule.kt} (52%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt rename core/datasource/src/main/java/com/tangem/datasource/local/token/{DefaultAssetsStore.kt => DefaultExpressAssetsStore.kt} (89%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt rename core/datasource/src/main/java/com/tangem/datasource/local/token/{AssetsStore.kt => ExpressAssetsStore.kt} (90%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 54bfe9d2b7..532e167dbd 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.settings.* import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -14,6 +15,14 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object StakingDomainModule { + @Provides + @Singleton + fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { + return GetStakingAvailabilityUseCase( + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase { @@ -24,8 +33,8 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { - return GetStakingAvailabilityUseCase( + fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase { + return FetchStakingTokensUseCase( stakingRepository = stakingRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 53a3565731..23e161aaa8 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase +import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -44,6 +45,7 @@ internal class MainViewModel @Inject constructor( private val sendFeatureToggles: SendFeatureToggles, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, + private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -69,6 +71,7 @@ internal class MainViewModel @Inject constructor( observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() + fetchStakingTokens() viewModelScope.launch(dispatchers.main) { deleteDeprecatedLogsUseCase() @@ -115,6 +118,14 @@ internal class MainViewModel @Inject constructor( } } + private fun fetchStakingTokens() { + viewModelScope.launch(dispatchers.main) { + fetchStakingTokensUseCase() + .onLeft { Timber.e("Unable to fetch the staking tokens list") } + .onRight { Timber.d("Staking token list was fetched successfully") } + } + } + private fun updateSendFeatureToggle() { viewModelScope.launch(dispatchers.main) { sendFeatureToggles.fetchNewSendEnabled() diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt index 7e953fdfd5..a5ef219c9b 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -8,6 +8,6 @@ internal class DefaultStakeKitAuthProvider( ) : StakeKitAuthProvider { override fun getApiKey(): String { - return configManager.config.express?.apiKey ?: error("No StakeKit api key provided") + return configManager.config.stakeKitApiKey ?: error("No StakeKit api key provided") } } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 4deb2329d1..b706481bfe 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.staking) // TODO staking create staking models module /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index f61191a104..633fd20cd2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -2,12 +2,35 @@ package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import com.squareup.moshi.adapters.EnumJsonAdapter +import com.tangem.datasource.api.stakekit.models.response.model.StakingActionType +import com.tangem.datasource.api.stakekit.models.response.model.Token +import com.tangem.datasource.api.stakekit.models.response.model.Yield +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper /** * Object to create a adapter for enum types with support for unknown enum values. */ object UnknownEnumMoshiAdapter { - fun > create(enumType: Class, defaultValue: T): JsonAdapter = - EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue) + @Suppress("UNCHECKED_CAST") + fun > create(enumType: Class>, defaultValue: Enum<*>): JsonAdapter> { + return EnumJsonAdapter.create(enumType as Class).withUnknownFallback(defaultValue as T) + } +} + +fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { + val map = mapOf( + Token.NetworkType::class.java to Token.NetworkType.UNKNOWN, + StakingActionType::class.java to StakingActionType.UNKNOWN, + Yield.RewardType::class.java to Yield.RewardType.UNKNOWN, + YieldBalanceWrapper.Balance.BalanceType::class.java to YieldBalanceWrapper.Balance.BalanceType.UNKNOWN, + ) + + return apply { + map.forEach { entry -> + val enumClass = entry.key + val unknownValue = entry.value + add(enumClass, UnknownEnumMoshiAdapter.create(enumClass, unknownValue)) + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index c7d564c69a..4aae3792c7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -18,12 +18,12 @@ interface StakeKitApi { @GET("yields/enabled") suspend fun getMultipleYields( - @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean, - @Query("type") type: YieldType, - @Query("revenueOption") revenueOption: RevenueOption, - @Query("page") page: Int, - @Query("network") network: String, - @Query("limit") limit: Int, + @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null, + @Query("type") type: YieldType? = null, + @Query("revenueOption") revenueOption: RevenueOption? = null, + @Query("page") page: Int? = null, + @Query("network") network: String? = null, + @Query("limit") limit: Int? = null, ): ApiResponse @GET("yields/{integrationId}") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt index 301b7b5805..80c6347e26 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt @@ -10,7 +10,7 @@ data class AddressArgument( @Json(name = "network") val network: String? = null, @Json(name = "minimum") - val minimum: Int? = null, + val minimum: Double? = null, @Json(name = "maximum") - val maximum: Int? = null, + val maximum: Double? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt index 5e60da84d9..2d71d2a50a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt @@ -47,4 +47,6 @@ enum class StakingActionType { @Json(name = "MIGRATE") MIGRATE, + + UNKNOWN, } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt index c122022402..bdbea0868c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt @@ -139,8 +139,10 @@ data class Yield( enum class RewardType { @Json(name = "apy") - APY, // auto + APY, // compound rate @Json(name = "apr") - APR, // manual + APR, // simple rate, + + UNKNOWN, } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt index 01f0e26c16..adb7968856 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt @@ -61,6 +61,8 @@ data class YieldBalanceWrapper( @Json(name = "unlocking") UNLOCKING, + + UNKNOWN, } @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt deleted file mode 100644 index 98637444a4..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.datasource.api.stakekit.models.response.model - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.joda.time.DateTime -import java.math.BigDecimal - -@JsonClass(generateAdapter = true) -data class YieldBalances( - @Json(name = "balances") - val balances: List, - @Json(name = "integrationId") - val integrationId: String, -) { - - @JsonClass(generateAdapter = true) - data class Balance( - @Json(name = "groupId") - val groupId: String, - @Json(name = "type") - val type: BalanceType, - @Json(name = "amount") - val amount: BigDecimal, - @Json(name = "date") - val date: DateTime?, - @Json(name = "pricePerShare") - val pricePerShare: BigDecimal, - @Json(name = "pendingActions") - val pendingActions: List, - @Json(name = "token") - val token: Token, - @Json(name = "validatorAddress") - val validatorAddress: String?, - @Json(name = "validatorAddresses") - val validatorAddresses: List?, - @Json(name = "providerId") - val providerId: String?, - ) { - - enum class BalanceType { - @Json(name = "available") - AVAILABLE, - - @Json(name = "staked") - STAKED, - - @Json(name = "unstaking") - UNSTAKING, - - @Json(name = "unstaked") - UNSTAKED, - - @Json(name = "preparing") - PREPARING, - - @Json(name = "rewards") - REWARDS, - - @Json(name = "locked") - LOCKED, - - @Json(name = "unlocking") - UNLOCKING, - } - - @JsonClass(generateAdapter = true) - data class PendingAction( - @Json(name = "type") - val type: StakingActionType, - @Json(name = "passthrough") - val passthrough: String, - @Json(name = "args") - val args: PendingActionArgs?, - ) { - @JsonClass(generateAdapter = true) - data class PendingActionArgs( - @Json(name = "amount") - val amount: Amount?, - @Json(name = "duration") - val duration: Duration?, - @Json(name = "validatorAddress") - val validatorAddress: Required?, - @Json(name = "validatorAddresses") - val validatorAddresses: Required?, - @Json(name = "nfts") - val nfts: List?, - @Json(name = "tronResource") - val tronResource: TronResource?, - @Json(name = "signatureVerification") - val signatureVerification: Required?, - ) { - @JsonClass(generateAdapter = true) - data class Amount( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: BigDecimal?, - @Json(name = "maximum") - val maximum: BigDecimal?, - ) - - @JsonClass(generateAdapter = true) - data class Duration( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: Int?, - @Json(name = "maximum") - val maximum: Int?, - ) - - @JsonClass(generateAdapter = true) - data class Nft( - @Json(name = "baycId") - val baycId: Required?, - @Json(name = "maycId") - val maycId: Required?, - @Json(name = "bakcId") - val bakcId: Required?, - ) - - @JsonClass(generateAdapter = true) - data class TronResource( - @Json(name = "required") - val required: Boolean, - @Json(name = "options") - val options: List, - ) - } - } - - @JsonClass(generateAdapter = true) - data class Required( - @Json(name = "required") - val required: Boolean, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt index 43b8e93f5c..cf51b2b006 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.token.DefaultAssetsStore -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.DefaultExpressAssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,11 +11,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object AssetsStoreModule { +internal object ExpressAssetsStoreModule { @Provides @Singleton - fun provideAssetsStore(): AssetsStore { - return DefaultAssetsStore(dataStore = RuntimeDataStore()) + fun provideExpressAssetsStore(): ExpressAssetsStore { + return DefaultExpressAssetsStore(dataStore = RuntimeDataStore()) } } \ 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 ddd3386e58..e04042a8b4 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 @@ -4,11 +4,10 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter +import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.api.common.adapter.BigDecimalAdapter import com.tangem.datasource.api.common.adapter.DateTimeAdapter import com.tangem.datasource.api.common.adapter.LocalDateAdapter -import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter -import com.tangem.datasource.api.stakekit.models.response.model.Token import com.tangem.datasource.config.models.ProviderModel import dagger.Module import dagger.Provides @@ -35,10 +34,7 @@ class MoshiModule { .add(LocalDateAdapter()) .add(DateTimeAdapter()) .add(KotlinJsonAdapterFactory()) - .add( - Token.NetworkType::class.java, - UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN), - ) + .addStakeKitEnumFallbackAdapters() .build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt new file mode 100644 index 0000000000..50b1bacb2b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultStakingTokensStore +import com.tangem.datasource.local.token.StakingTokensStore +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 StakingTokensStoreModule { + + @Provides + @Singleton + fun provideStakingTokensStore(): StakingTokensStore { + return DefaultStakingTokensStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt similarity index 89% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt index 9177ec70b7..c67abc06e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt @@ -4,9 +4,9 @@ import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.domain.wallets.models.UserWalletId -internal class DefaultAssetsStore( +internal class DefaultExpressAssetsStore( private val dataStore: StringKeyDataStore>, -) : AssetsStore { +) : ExpressAssetsStore { override suspend fun getSyncOrNull(userWalletId: UserWalletId): List? { return dataStore.getSyncOrNull(userWalletId.stringValue) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt new file mode 100644 index 0000000000..a74afc914e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.staking.model.StakingTokenWithYield + +internal class DefaultStakingTokensStore( + private val dataStore: StringKeyDataStore>, +) : StakingTokensStore { + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_TOKENS_KEY) + } + + override suspend fun store(items: List) { + dataStore.store(STAKING_TOKENS_KEY, items) + } + + companion object { + private const val STAKING_TOKENS_KEY = "STAKING_TOKENS_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt index 11d6101843..353a15c7e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.express.models.response.Asset import com.tangem.domain.wallets.models.UserWalletId -interface AssetsStore { +interface ExpressAssetsStore { suspend fun getSyncOrNull(userWalletId: UserWalletId): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt new file mode 100644 index 0000000000..586674756b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.token + +import com.tangem.domain.staking.model.StakingTokenWithYield + +interface StakingTokensStore { + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index c9fa81608f..215b56e9f8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -35,5 +35,6 @@ sealed class RequestHeader(vararg pairs: Pair String>) { class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader( "X-API-KEY" to { stakeKitAuthProvider.getApiKey() }, + "accept" to { "application/json" }, ) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 26f738de00..0c5b731554 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,18 +1,26 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain +import com.tangem.data.staking.converters.StakingTokenConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingTokensStore import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingFeatureToggles: StakingFeatureToggles, + private val stakingTokenStore: StakingTokensStore, + private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { + private val tokenWithYieldConverter = StakingTokenConverter() + override fun getStakingAvailability(blockchainId: String): StakingAvailability { if (!stakingFeatureToggles.isStakingEnabled) { return StakingAvailability.Unavailable @@ -33,6 +41,14 @@ internal class DefaultStakingRepository( ) } + override suspend fun fetchEnabledTokens() { + withContext(dispatchers.io) { + val stakingTokensWithYields = stakeKitApi.getTokens().getOrThrow() + + stakingTokenStore.store(stakingTokensWithYields.map { tokenWithYieldConverter.convert(it) }) + } + } + companion object { private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt new file mode 100644 index 0000000000..3fa4fe1a33 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield +import com.tangem.domain.staking.model.StakingToken +import com.tangem.domain.staking.model.StakingTokenWithYield +import com.tangem.utils.converter.Converter + +class StakingTokenConverter : Converter { + + override fun convert(value: TokenWithYield): StakingTokenWithYield { + return StakingTokenWithYield( + token = StakingToken( + name = value.token.name, + symbol = value.token.symbol, + decimals = value.token.decimals, + contractAddress = value.token.address, + coinGeckoId = value.token.coinGeckoId, + ), + availableYieldIds = value.availableYieldIds, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 0028f190b2..7663df6823 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -2,8 +2,10 @@ package com.tangem.data.staking.di import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingTokensStore import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,10 +21,14 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingFeatureToggles: StakingFeatureToggles, + stakingTokenStore: StakingTokensStore, + dispatchers: CoroutineDispatcherProvider, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingFeatureToggles = stakingFeatureToggles, + stakingTokenStore = stakingTokenStore, + dispatchers = dispatchers, ) } } \ 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 062692cd06..5705476197 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 @@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* @@ -31,7 +31,7 @@ internal object TokensDataModule { userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, - assetsStore: AssetsStore, + expressAssetsStore: ExpressAssetsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { @@ -41,7 +41,7 @@ internal object TokensDataModule { userTokensStore = userTokensStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - assetsStore = assetsStore, + expressAssetsStore = expressAssetsStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) @@ -87,8 +87,8 @@ internal object TokensDataModule { @Provides @Singleton - fun provideDefaultMarketCoinsRepository(assetsStore: AssetsStore): MarketCryptoCurrencyRepository { - return DefaultMarketCryptoCurrencyRepository(assetsStore) + fun provideDefaultMarketCoinsRepository(expressAssetsStore: ExpressAssetsStore): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(expressAssetsStore) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index cf29c3b4a3..ff644fd95c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -15,7 +15,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider @@ -46,7 +46,7 @@ internal class DefaultCurrenciesRepository( private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, - private val assetsStore: AssetsStore, + private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -493,7 +493,7 @@ internal class DefaultCurrenciesRepository( ), ) - assetsStore.store(userWalletId, response.getOrThrow()) + expressAssetsStore.store(userWalletId, response.getOrThrow()) } } catch (e: Throwable) { Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index a210d96273..a3c7834ed9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -1,13 +1,13 @@ package com.tangem.data.tokens.repository import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId class DefaultMarketCryptoCurrencyRepository( - private val assetsStore: AssetsStore, + private val expressAssetsStore: ExpressAssetsStore, ) : MarketCryptoCurrencyRepository { override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { @@ -17,7 +17,7 @@ class DefaultMarketCryptoCurrencyRepository( private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE - return assetsStore.getSyncOrNull(userWalletId)?.find { + return expressAssetsStore.getSyncOrNull(userWalletId)?.find { it.network == cryptoCurrency.network.backendId && it.contractAddress.equals(contractAddress, ignoreCase = true) }?.exchangeAvailable ?: false diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt new file mode 100644 index 0000000000..55c8d1b3fa --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for getting enabled tokens + */ +class FetchStakingTokensUseCase( + private val stakingRepository: StakingRepository, +) { + suspend operator fun invoke(): Either { + return Either.catch { stakingRepository.fetchEnabledTokens() } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt new file mode 100644 index 0000000000..5da29043d1 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.staking.model + +data class StakingToken( + val name: String, + val symbol: String, + val decimals: Int, + val contractAddress: String?, + val coinGeckoId: String?, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt new file mode 100644 index 0000000000..833cb6ae49 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.model + +data class StakingTokenWithYield( + val token: StakingToken, + val availableYieldIds: List, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 763377ac5e..5e84852870 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -8,4 +8,6 @@ interface StakingRepository { fun getStakingAvailability(blockchainId: String): StakingAvailability suspend fun getEntryInfo(integrationId: String): StakingEntryInfo + + suspend fun fetchEnabledTokens() } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index dad4976a7d..6f499d7be1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) implementation(projects.domain.visa) + implementation(projects.domain.staking) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) 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 8507f48bbc..6d6961b577 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 @@ -55,12 +55,12 @@ internal class WalletViewModel @Inject constructor( private val canUseBiometryUseCase: CanUseBiometryUseCase, private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, + analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState From 0e29df4ce9730aaa4d2d2b858debf8ba05a5a545 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 17:23:23 +0200 Subject: [PATCH 007/223] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 + .../tangem/tap/features/main/MainViewModel.kt | 2 +- .../common/adapter/UnknownEnumMoshiAdapter.kt | 17 +- .../datasource/api/stakekit/StakeKitApi.kt | 14 +- .../models/response/EnabledYieldsResponse.kt | 4 +- ...dressArgument.kt => AddressArgumentDTO.kt} | 2 +- ...gActionType.kt => StakingActionTypeDTO.kt} | 2 +- .../response/model/{Token.kt => TokenDTO.kt} | 6 +- ...TokenWithYield.kt => TokenWithYieldDTO.kt} | 4 +- ...ceWrapper.kt => YieldBalanceWrapperDTO.kt} | 10 +- .../response/model/{Yield.kt => YieldDTO.kt} | 50 ++--- .../datasource/di/StakingTokensStoreModule.kt | 8 +- .../local/token/DefaultStakingYieldsStore.kt | 21 ++ .../local/token/StakingYieldsStore.kt | 10 + core/res/src/main/res/values-ru/strings.xml | 11 +- core/res/src/main/res/values/strings.xml | 20 +- data/staking/build.gradle.kts | 2 + .../data/staking/DefaultStakingRepository.kt | 124 +++++++---- .../converters/StakingTokenConverter.kt | 6 +- .../data/staking/converters/YieldConverter.kt | 206 ++++++++++++++++++ .../data/staking/di/StakingDataModule.kt | 6 +- domain/staking/build.gradle.kts | 9 +- .../staking/FetchStakingTokensUseCase.kt | 10 +- .../staking/GetStakingAvailabilityUseCase.kt | 7 +- .../staking/error/StakingTokensError.kt | 6 + .../staking/model/StakingAvailability.kt | 2 + .../com/tangem/domain/staking/model/Yield.kt | 172 +++++++++++++++ .../staking/repositories/StakingRepository.kt | 10 +- domain/tokens/build.gradle.kts | 1 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 22 ++ .../model/ScenarioUnavailabilityReason.kt | 3 + .../domain/tokens/model/TokenActionsState.kt | 2 + .../tokendetails/TokenDetailsPreviewData.kt | 4 +- .../tokendetails/state/TokenDetailsState.kt | 2 +- .../components/TokenDetailsActionButton.kt | 15 ++ .../TokenDetailsActionButtonsConverter.kt | 6 + .../TokenDetailsSkeletonStateConverter.kt | 5 +- .../state/factory/TokenDetailsStateFactory.kt | 14 +- .../tokendetails/ui/TokenDetailsScreen.kt | 2 +- .../viewmodels/TokenDetailsClickIntents.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 13 +- .../wallet/state/model/WalletManageButton.kt | 13 ++ .../SetRefreshStateTransformer.kt | 1 + .../MultiWalletCurrencyActionsConverter.kt | 5 + .../WalletCurrencyActionsClickIntents.kt | 14 ++ 45 files changed, 731 insertions(+), 137 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{AddressArgument.kt => AddressArgumentDTO.kt} (92%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{StakingActionType.kt => StakingActionTypeDTO.kt} (95%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{Token.kt => TokenDTO.kt} (97%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{TokenWithYield.kt => TokenWithYieldDTO.kt} (75%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{YieldBalanceWrapper.kt => YieldBalanceWrapperDTO.kt} (96%) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{Yield.kt => YieldDTO.kt} (81%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.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 40ba3c5084..ca10e45669 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 @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -190,6 +191,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -199,6 +201,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 23e161aaa8..07650632f0 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -121,7 +121,7 @@ internal class MainViewModel @Inject constructor( private fun fetchStakingTokens() { viewModelScope.launch(dispatchers.main) { fetchStakingTokensUseCase() - .onLeft { Timber.e("Unable to fetch the staking tokens list") } + .onLeft { Timber.e(it, "Unable to fetch the staking tokens list") } .onRight { Timber.d("Staking token list was fetched successfully") } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index 633fd20cd2..1d8fc5ffc1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -2,10 +2,10 @@ package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import com.squareup.moshi.adapters.EnumJsonAdapter -import com.tangem.datasource.api.stakekit.models.response.model.StakingActionType -import com.tangem.datasource.api.stakekit.models.response.model.Token -import com.tangem.datasource.api.stakekit.models.response.model.Yield -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper +import com.tangem.datasource.api.stakekit.models.response.model.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO /** * Object to create a adapter for enum types with support for unknown enum values. @@ -20,10 +20,11 @@ object UnknownEnumMoshiAdapter { fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { val map = mapOf( - Token.NetworkType::class.java to Token.NetworkType.UNKNOWN, - StakingActionType::class.java to StakingActionType.UNKNOWN, - Yield.RewardType::class.java to Yield.RewardType.UNKNOWN, - YieldBalanceWrapper.Balance.BalanceType::class.java to YieldBalanceWrapper.Balance.BalanceType.UNKNOWN, + TokenDTO.NetworkTypeDTO::class.java to TokenDTO.NetworkTypeDTO.UNKNOWN, + StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, + YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN, + YieldBalanceWrapperDTO.BalanceDTO.BalanceType::class.java to + YieldBalanceWrapperDTO.BalanceDTO.BalanceType.UNKNOWN, ) return apply { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 4aae3792c7..6f1df5b3f4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -5,9 +5,9 @@ import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody import com.tangem.datasource.api.stakekit.models.request.RevenueOption import com.tangem.datasource.api.stakekit.models.request.YieldType import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse -import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield -import com.tangem.datasource.api.stakekit.models.response.model.Yield -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Path @@ -30,19 +30,19 @@ interface StakeKitApi { suspend fun getSingleYield( @Path("integrationId") integrationId: String, @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false, - ): ApiResponse + ): ApiResponse @GET("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, - ): ApiResponse> + ): ApiResponse> @GET("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( @Path("integrationId") integrationId: String, @Body body: YieldBalanceRequestBody, - ): ApiResponse + ): ApiResponse @GET("tokens") - suspend fun getTokens(): ApiResponse> + suspend fun getTokens(): ApiResponse> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt index c4a958c435..15605bc27c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt @@ -2,12 +2,12 @@ package com.tangem.datasource.api.stakekit.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.datasource.api.stakekit.models.response.model.Yield +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO @JsonClass(generateAdapter = true) data class EnabledYieldsResponse( @Json(name = "data") - val data: List, + val data: List, @Json(name = "hasNextPage") val hasNextPage: Boolean, @Json(name = "limit") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt similarity index 92% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt index 80c6347e26..7a4f33b288 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class AddressArgument( +data class AddressArgumentDTO( @Json(name = "required") val required: Boolean, @Json(name = "network") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt similarity index 95% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt index 2d71d2a50a..5f310f04a8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json -enum class StakingActionType { +enum class StakingActionTypeDTO { @Json(name = "STAKE") STAKE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt similarity index 97% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt index 88d5d12b59..6321b54ecb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt @@ -4,9 +4,9 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class Token( +data class TokenDTO( @Json(name = "name") val name: String, - @Json(name = "network") val network: NetworkType, + @Json(name = "network") val network: NetworkTypeDTO, @Json(name = "symbol") val symbol: String, @Json(name = "decimals") val decimals: Int, @Json(name = "address") val address: String?, @@ -14,7 +14,7 @@ data class Token( @Json(name = "logoURI") val logoURI: String?, @Json(name = "isPoints") val isPoints: Boolean?, ) { - enum class NetworkType { + enum class NetworkTypeDTO { @Json(name = "avalanche-c") AVALANCHE_C, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt similarity index 75% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt index 2bc30185af..15182ef389 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class TokenWithYield( - @Json(name = "token") val token: Token, +data class TokenWithYieldDTO( + @Json(name = "token") val token: TokenDTO, @Json(name = "availableYields") val availableYieldIds: List, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt similarity index 96% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index adb7968856..c64bdd591d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -6,15 +6,15 @@ import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class YieldBalanceWrapper( +data class YieldBalanceWrapperDTO( @Json(name = "balances") - val balances: List, + val balances: List, @Json(name = "integrationId") val integrationId: String?, ) { @JsonClass(generateAdapter = true) - data class Balance( + data class BalanceDTO( @Json(name = "groupId") val groupId: String, @Json(name = "type") @@ -28,7 +28,7 @@ data class YieldBalanceWrapper( @Json(name = "pendingActions") val pendingActions: List, @Json(name = "token") - val token: Token, + val tokenDTO: TokenDTO, @Json(name = "validatorAddress") val validatorAddress: String?, @Json(name = "validatorAddresses") @@ -68,7 +68,7 @@ data class YieldBalanceWrapper( @JsonClass(generateAdapter = true) data class PendingAction( @Json(name = "type") - val type: StakingActionType, + val type: StakingActionTypeDTO, @Json(name = "passthrough") val passthrough: String, @Json(name = "args") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt similarity index 81% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index bdbea0868c..3e0c439e09 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -5,33 +5,33 @@ import com.squareup.moshi.JsonClass import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class Yield( +data class YieldDTO( @Json(name = "id") val id: String, @Json(name = "token") - val token: Token, + val token: TokenDTO, @Json(name = "tokens") - val tokens: List, + val tokens: List, @Json(name = "args") - val args: Args, + val args: ArgsDTO, @Json(name = "status") - val status: Status, + val status: StatusDTO, @Json(name = "apy") val apy: BigDecimal, @Json(name = "rewardRate") val rewardRate: Double, @Json(name = "rewardType") - val rewardType: RewardType, + val rewardType: RewardTypeDTO, @Json(name = "metadata") - val metadata: Metadata, + val metadata: MetadataDTO, @Json(name = "validators") - val validators: List, + val validators: List, @Json(name = "isAvailable") val isAvailable: Boolean, ) { @JsonClass(generateAdapter = true) - data class Status( + data class StatusDTO( @Json(name = "enter") val enter: Boolean, @Json(name = "exit") @@ -39,7 +39,7 @@ data class Yield( ) @JsonClass(generateAdapter = true) - data class Args( + data class ArgsDTO( @Json(name = "enter") val enter: Enter, @Json(name = "exit") @@ -50,20 +50,20 @@ data class Yield( @Json(name = "addresses") val addresses: Addresses, @Json(name = "args") - val args: Map, + val args: Map, ) { @JsonClass(generateAdapter = true) data class Addresses( @Json(name = "address") - val address: AddressArgument, + val address: AddressArgumentDTO, @Json(name = "additionalAddresses") - val additionalAddresses: Map? = null, + val additionalAddresses: Map? = null, ) } } @JsonClass(generateAdapter = true) - data class Validator( + data class ValidatorDTO( @Json(name = "address") val address: String, @Json(name = "status") @@ -87,7 +87,7 @@ data class Yield( ) @JsonClass(generateAdapter = true) - data class Metadata( + data class MetadataDTO( @Json(name = "name") val name: String, @Json(name = "logoURI") @@ -97,19 +97,19 @@ data class Yield( @Json(name = "documentation") val documentation: String?, @Json(name = "gasFeeToken") - val gasFeeToken: Token, + val gasFeeTokenDTO: TokenDTO, @Json(name = "token") - val token: Token, + val tokenDTO: TokenDTO, @Json(name = "tokens") - val tokens: List, + val tokensDTO: List, @Json(name = "type") val type: String, @Json(name = "rewardSchedule") val rewardSchedule: String, @Json(name = "cooldownPeriod") - val cooldownPeriod: Period, + val cooldownPeriod: PeriodDTO, @Json(name = "warmupPeriod") - val warmupPeriod: Period, + val warmupPeriod: PeriodDTO, @Json(name = "rewardClaiming") val rewardClaiming: String, @Json(name = "defaultValidator") @@ -119,25 +119,25 @@ data class Yield( @Json(name = "supportsMultipleValidators") val supportsMultipleValidators: Boolean, @Json(name = "revshare") - val revshare: Enabled, + val revshare: EnabledDTO, @Json(name = "fee") - val fee: Enabled, + val fee: EnabledDTO, ) { @JsonClass(generateAdapter = true) - data class Period( + data class PeriodDTO( @Json(name = "days") val days: Int, ) @JsonClass(generateAdapter = true) - data class Enabled( + data class EnabledDTO( @Json(name = "enabled") val enabled: Boolean, ) } - enum class RewardType { + enum class RewardTypeDTO { @Json(name = "apy") APY, // compound rate @Json(name = "apr") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt index 50b1bacb2b..45e280e949 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.token.DefaultStakingTokensStore -import com.tangem.datasource.local.token.StakingTokensStore +import com.tangem.datasource.local.token.DefaultStakingYieldsStore +import com.tangem.datasource.local.token.StakingYieldsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +15,7 @@ internal object StakingTokensStoreModule { @Provides @Singleton - fun provideStakingTokensStore(): StakingTokensStore { - return DefaultStakingTokensStore(dataStore = RuntimeDataStore()) + fun provideStakingTokensStore(): StakingYieldsStore { + return DefaultStakingYieldsStore(dataStore = RuntimeDataStore()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt new file mode 100644 index 0000000000..1cdb6c381c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore + +internal class DefaultStakingYieldsStore( + private val dataStore: StringKeyDataStore>, +) : StakingYieldsStore { + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_YIELDS_KEY) + } + + override suspend fun store(items: List) { + dataStore.store(STAKING_YIELDS_KEY, items) + } + + companion object { + private const val STAKING_YIELDS_KEY = "STAKING_YIELDS_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt new file mode 100644 index 0000000000..61f46cf15d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO + +interface StakingYieldsStore { + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 1135d892dc..67531e72a0 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -117,6 +117,7 @@ Отклонить Перезагрузить Переименовать + Сохранить Сохранить изменения Искать Поиск токенов @@ -128,6 +129,7 @@ Поделиться Подписать Подписать и отправить + Стейкинг Начать Отправить Успешно @@ -169,6 +171,7 @@ Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить Остерегайтесь мошеннических токенов, они могут ничего не стоить Токены могут быть созданы кем угодно + Купить кошелек Tangem Чат Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа. @@ -186,6 +189,7 @@ Скрывать балансы жестом переворота Эмитент Подписано + Отправить отзыв Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -538,6 +542,8 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + Доступно + Стейкинг %s Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -579,9 +585,10 @@ У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. - Продажа средств станет доступной после завершения транзакции(-ий) в сети %s + Продажа средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции(-ий) в сети %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. + В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. @@ -668,7 +675,7 @@ Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. Ошибка активации - По решению разработчиков сети BNB, стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять свои активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении чтобы перевести их в cеть BNB Smart Chain. + По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. Отключение сети BNB Beacon Chain Можно лучше Нравится diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8bbaa40db7..130168cb3a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -116,6 +116,7 @@ Reject Reload Rename + Save Save changes Search Search tokens @@ -127,6 +128,8 @@ Share Sign Sign and send + Stake + Staking Start Submit Success @@ -168,6 +171,7 @@ Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. Be aware of adding scam tokens, they can cost nothing Note that tokens can be created by anyone + Buy Tangem Wallet Chat Access code You will have to submit the correct access code before scanning the card @@ -185,6 +189,7 @@ Flip-to-Hide Balances Issuer Signed + Send feedback Details Check your internet connection or switch to a different network Terms of service @@ -531,6 +536,18 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name + Available + Average Reward Rate + %s est. profit + Minimum Requirement + No rewards to claim + On stake + Reward claiming + Reward schedule + Staking %s + Unbonding Period + Warmup period + Rewards Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -575,6 +592,7 @@ Selling funds will be available once the pending transaction(s) in network %s is complete Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. + Staking %s is not available at the moment. Please check our updates. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -661,7 +679,7 @@ Use %s or scan a card to unlock access to your wallet Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. Activation error - According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service to transfer them to the BNB Smart Chain network. + According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. BNB Beacon Chain will shut down Could be better Like it diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 83a8cccc87..3c16b7c878 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.domain.tokens.models) implementation(projects.domain.staking) implementation(projects.features.staking.api) @@ -29,6 +30,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(projects.libs.blockchainSdk) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 0c5b731554..be421d5a75 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,13 +1,16 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain -import com.tangem.data.staking.converters.StakingTokenConverter +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.data.staking.converters.YieldConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.local.token.StakingTokensStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -15,65 +18,90 @@ import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingFeatureToggles: StakingFeatureToggles, - private val stakingTokenStore: StakingTokensStore, + private val stakingYieldsStore: StakingYieldsStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { - private val tokenWithYieldConverter = StakingTokenConverter() + private val yieldConverter = YieldConverter() + + override suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability { + val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable - override fun getStakingAvailability(blockchainId: String): StakingAvailability { if (!stakingFeatureToggles.isStakingEnabled) { return StakingAvailability.Unavailable } + return withContext(dispatchers.io) { + val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable - return integrationIdMap[Blockchain.fromId(blockchainId)]?.let { - StakingAvailability.Available(it) - } ?: StakingAvailability.Unavailable - } + val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) + val isSupported = isStakingSupported(rawCurrencyId) - override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { - val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() - - return StakingEntryInfo( - interestRate = yield.apy, - periodInDays = yield.metadata.cooldownPeriod.days, - tokenSymbol = yield.token.symbol, - ) - } - - override suspend fun fetchEnabledTokens() { - withContext(dispatchers.io) { - val stakingTokensWithYields = stakeKitApi.getTokens().getOrThrow() - - stakingTokenStore.store(stakingTokensWithYields.map { tokenWithYieldConverter.convert(it) }) + when { + prefetchedYield != null && isSupported -> { + StakingAvailability.Available(prefetchedYield.id) + } + prefetchedYield == null && isSupported -> { + StakingAvailability.TemporaryDisabled + } + else -> StakingAvailability.Unavailable + } } } - companion object { - private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" - private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" - private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - private const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking" - private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" - private const val TRON_INTEGRATION_ID = "tron-trx-native-staking" - private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - private const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking" - private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" - private const val NEAR_INTEGRATION_ID = "near-near-native-staking" - private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { + return yields + .find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } + } - private val integrationIdMap = mapOf( - Blockchain.Solana to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos to COSMOS_INTEGRATION_ID, - Blockchain.Polkadot to POLKADOT_INTEGRATION_ID, - Blockchain.Polygon to ETHEREUM_INTEGRATION_ID, - Blockchain.Avalanche to AVALANCHE_INTEGRATION_ID, - Blockchain.Tron to TRON_INTEGRATION_ID, - Blockchain.Cronos to CRONOS_INTEGRATION_ID, - Blockchain.Binance to BINANCE_INTEGRATION_ID, - Blockchain.Kava to KAVA_INTEGRATION_ID, - Blockchain.Near to NEAR_INTEGRATION_ID, - Blockchain.Tezos to TEZOS_INTEGRATION_ID, + override fun isStakingSupported(currencyId: String): Boolean { + return integrationIds.contains(currencyId) + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { + return withContext(dispatchers.io) { + val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() + + StakingEntryInfo( + interestRate = yield.apy, + periodInDays = yield.metadata.cooldownPeriod.days, + tokenSymbol = yield.token.symbol, + ) + } + } + + override suspend fun fetchEnabledYields() { + if (!stakingFeatureToggles.isStakingEnabled) { + return + } + + withContext(dispatchers.io) { + val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + + stakingYieldsStore.store(stakingTokensWithYields.data) + } + } + + private suspend fun getEnabledYields(): List? { + val yields = stakingYieldsStore.getSyncOrNull() ?: return null + return yields.map { yieldConverter.convert(it) } + } + + companion object { + private val integrationIds = setOf( + Blockchain.Solana.toCoinId(), + Blockchain.Cosmos.toCoinId(), + Blockchain.Polkadot.toCoinId(), + Blockchain.Polygon.toCoinId(), + Blockchain.Avalanche.toCoinId(), + Blockchain.Tron.toCoinId(), + Blockchain.Cronos.toCoinId(), + Blockchain.Binance.toCoinId(), + Blockchain.Kava.toCoinId(), + Blockchain.Near.toCoinId(), + Blockchain.Tezos.toCoinId(), ) } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt index 3fa4fe1a33..06cbb55869 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt @@ -1,13 +1,13 @@ package com.tangem.data.staking.converters -import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO import com.tangem.domain.staking.model.StakingToken import com.tangem.domain.staking.model.StakingTokenWithYield import com.tangem.utils.converter.Converter -class StakingTokenConverter : Converter { +class StakingTokenConverter : Converter { - override fun convert(value: TokenWithYield): StakingTokenWithYield { + override fun convert(value: TokenWithYieldDTO): StakingTokenWithYield { return StakingTokenWithYield( token = StakingToken( name = value.token.name, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt new file mode 100644 index 0000000000..d7300234f0 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -0,0 +1,206 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.domain.staking.model.* +import com.tangem.utils.converter.Converter + +class YieldConverter : Converter { + + override fun convert(value: YieldDTO): Yield { + return Yield( + id = value.id, + token = convertToken(value.token), + tokens = value.tokens.map { convertToken(it) }, + args = convertArgs(value.args), + status = convertStatus(value.status), + apy = value.apy, + rewardRate = value.rewardRate, + rewardType = convertRewardType(value.rewardType), + metadata = convertMetadata(value.metadata), + validators = value.validators.map { convertValidator(it) }, + isAvailable = value.isAvailable, + ) + } + + private fun convertToken(tokenDTO: TokenDTO): Token { + return Token( + name = tokenDTO.name, + network = convertNetworkType(tokenDTO.network), + symbol = tokenDTO.symbol, + decimals = tokenDTO.decimals, + address = tokenDTO.address, + coinGeckoId = tokenDTO.coinGeckoId, + logoURI = tokenDTO.logoURI, + isPoints = tokenDTO.isPoints, + ) + } + + @Suppress("LongMethod", "CyclomaticComplexMethod") + private fun convertNetworkType(networkTypeDTO: TokenDTO.NetworkTypeDTO): Token.NetworkType { + return when (networkTypeDTO) { + TokenDTO.NetworkTypeDTO.AVALANCHE_C -> Token.NetworkType.AVALANCHE_C + TokenDTO.NetworkTypeDTO.AVALANCHE_ATOMIC -> Token.NetworkType.AVALANCHE_ATOMIC + TokenDTO.NetworkTypeDTO.AVALANCHE_P -> Token.NetworkType.AVALANCHE_P + TokenDTO.NetworkTypeDTO.ARBITRUM -> Token.NetworkType.ARBITRUM + TokenDTO.NetworkTypeDTO.BINANCE -> Token.NetworkType.BINANCE + TokenDTO.NetworkTypeDTO.CELO -> Token.NetworkType.CELO + TokenDTO.NetworkTypeDTO.ETHEREUM -> Token.NetworkType.ETHEREUM + TokenDTO.NetworkTypeDTO.ETHEREUM_GOERLI -> Token.NetworkType.ETHEREUM_GOERLI + TokenDTO.NetworkTypeDTO.ETHEREUM_HOLESKY -> Token.NetworkType.ETHEREUM_HOLESKY + TokenDTO.NetworkTypeDTO.FANTOM -> Token.NetworkType.FANTOM + TokenDTO.NetworkTypeDTO.HARMONY -> Token.NetworkType.HARMONY + TokenDTO.NetworkTypeDTO.OPTIMISM -> Token.NetworkType.OPTIMISM + TokenDTO.NetworkTypeDTO.POLYGON -> Token.NetworkType.POLYGON + TokenDTO.NetworkTypeDTO.GNOSIS -> Token.NetworkType.GNOSIS + TokenDTO.NetworkTypeDTO.MOONRIVER -> Token.NetworkType.MOONRIVER + TokenDTO.NetworkTypeDTO.OKC -> Token.NetworkType.OKC + TokenDTO.NetworkTypeDTO.ZKSYNC -> Token.NetworkType.ZKSYNC + TokenDTO.NetworkTypeDTO.VICTION -> Token.NetworkType.VICTION + TokenDTO.NetworkTypeDTO.AGORIC -> Token.NetworkType.AGORIC + TokenDTO.NetworkTypeDTO.AKASH -> Token.NetworkType.AKASH + TokenDTO.NetworkTypeDTO.AXELAR -> Token.NetworkType.AXELAR + TokenDTO.NetworkTypeDTO.BAND_PROTOCOL -> Token.NetworkType.BAND_PROTOCOL + TokenDTO.NetworkTypeDTO.BITSONG -> Token.NetworkType.BITSONG + TokenDTO.NetworkTypeDTO.CANTO -> Token.NetworkType.CANTO + TokenDTO.NetworkTypeDTO.CHIHUAHUA -> Token.NetworkType.CHIHUAHUA + TokenDTO.NetworkTypeDTO.COMDEX -> Token.NetworkType.COMDEX + TokenDTO.NetworkTypeDTO.COREUM -> Token.NetworkType.COREUM + TokenDTO.NetworkTypeDTO.COSMOS -> Token.NetworkType.COSMOS + TokenDTO.NetworkTypeDTO.CRESCENT -> Token.NetworkType.CRESCENT + TokenDTO.NetworkTypeDTO.CRONOS -> Token.NetworkType.CRONOS + TokenDTO.NetworkTypeDTO.CUDOS -> Token.NetworkType.CUDOS + TokenDTO.NetworkTypeDTO.DESMOS -> Token.NetworkType.DESMOS + TokenDTO.NetworkTypeDTO.DYDX -> Token.NetworkType.DYDX + TokenDTO.NetworkTypeDTO.EVMOS -> Token.NetworkType.EVMOS + TokenDTO.NetworkTypeDTO.FETCH_AI -> Token.NetworkType.FETCH_AI + TokenDTO.NetworkTypeDTO.GRAVITY_BRIDGE -> Token.NetworkType.GRAVITY_BRIDGE + TokenDTO.NetworkTypeDTO.INJECTIVE -> Token.NetworkType.INJECTIVE + TokenDTO.NetworkTypeDTO.IRISNET -> Token.NetworkType.IRISNET + TokenDTO.NetworkTypeDTO.JUNO -> Token.NetworkType.JUNO + TokenDTO.NetworkTypeDTO.KAVA -> Token.NetworkType.KAVA + TokenDTO.NetworkTypeDTO.KI_NETWORK -> Token.NetworkType.KI_NETWORK + TokenDTO.NetworkTypeDTO.MARS_PROTOCOL -> Token.NetworkType.MARS_PROTOCOL + TokenDTO.NetworkTypeDTO.NYM -> Token.NetworkType.NYM + TokenDTO.NetworkTypeDTO.OKEX_CHAIN -> Token.NetworkType.OKEX_CHAIN + TokenDTO.NetworkTypeDTO.ONOMY -> Token.NetworkType.ONOMY + TokenDTO.NetworkTypeDTO.OSMOSIS -> Token.NetworkType.OSMOSIS + TokenDTO.NetworkTypeDTO.PERSISTENCE -> Token.NetworkType.PERSISTENCE + TokenDTO.NetworkTypeDTO.QUICKSILVER -> Token.NetworkType.QUICKSILVER + TokenDTO.NetworkTypeDTO.REGEN -> Token.NetworkType.REGEN + TokenDTO.NetworkTypeDTO.SECRET -> Token.NetworkType.SECRET + TokenDTO.NetworkTypeDTO.SENTINEL -> Token.NetworkType.SENTINEL + TokenDTO.NetworkTypeDTO.SOMMELIER -> Token.NetworkType.SOMMELIER + TokenDTO.NetworkTypeDTO.STAFI -> Token.NetworkType.STAFI + TokenDTO.NetworkTypeDTO.STARGAZE -> Token.NetworkType.STARGAZE + TokenDTO.NetworkTypeDTO.STRIDE -> Token.NetworkType.STRIDE + TokenDTO.NetworkTypeDTO.TERITORI -> Token.NetworkType.TERITORI + TokenDTO.NetworkTypeDTO.TGRADE -> Token.NetworkType.TGRADE + TokenDTO.NetworkTypeDTO.UMEE -> Token.NetworkType.UMEE + TokenDTO.NetworkTypeDTO.POLKADOT -> Token.NetworkType.POLKADOT + TokenDTO.NetworkTypeDTO.KUSAMA -> Token.NetworkType.KUSAMA + TokenDTO.NetworkTypeDTO.WESTEND -> Token.NetworkType.WESTEND + TokenDTO.NetworkTypeDTO.BINANCEBEACON -> Token.NetworkType.BINANCEBEACON + TokenDTO.NetworkTypeDTO.NEAR -> Token.NetworkType.NEAR + TokenDTO.NetworkTypeDTO.SOLANA -> Token.NetworkType.SOLANA + TokenDTO.NetworkTypeDTO.TEZOS -> Token.NetworkType.TEZOS + TokenDTO.NetworkTypeDTO.TRON -> Token.NetworkType.TRON + else -> Token.NetworkType.UNKNOWN + } + } + + private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args { + return Yield.Args( + enter = convertEnter(argsDTO.enter), + exit = argsDTO.exit?.let { convertEnter(it) }, + ) + } + + private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter { + return Yield.Args.Enter( + addresses = convertAddresses(enterDTO.addresses), + args = enterDTO.args.mapValues { convertAddressArgument(it.value) }, + ) + } + + private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses { + return Yield.Args.Enter.Addresses( + address = convertAddressArgument(addressesDTO.address), + additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) }, + ) + } + + private fun convertAddressArgument(addressArgumentDTO: AddressArgumentDTO): AddressArgument { + return AddressArgument( + required = addressArgumentDTO.required, + network = addressArgumentDTO.network, + minimum = addressArgumentDTO.minimum, + maximum = addressArgumentDTO.maximum, + ) + } + + private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status { + return Yield.Status( + enter = statusDTO.enter, + exit = statusDTO.exit, + ) + } + + private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata { + return Yield.Metadata( + name = metadataDTO.name, + logoUri = metadataDTO.logoUri, + description = metadataDTO.description, + documentation = metadataDTO.documentation, + gasFeeToken = convertToken(metadataDTO.gasFeeTokenDTO), + token = convertToken(metadataDTO.tokenDTO), + tokens = metadataDTO.tokensDTO.map { convertToken(it) }, + type = metadataDTO.type, + rewardSchedule = metadataDTO.rewardSchedule, + cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod), + warmupPeriod = convertPeriod(metadataDTO.warmupPeriod), + rewardClaiming = metadataDTO.rewardClaiming, + defaultValidator = metadataDTO.defaultValidator, + minimumStake = metadataDTO.minimumStake, + supportsMultipleValidators = metadataDTO.supportsMultipleValidators, + revshare = convertEnabled(metadataDTO.revshare), + fee = convertEnabled(metadataDTO.fee), + ) + } + + private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period { + return Yield.Metadata.Period( + days = periodDTO.days, + ) + } + + private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled { + return Yield.Metadata.Enabled( + enabled = enabledDTO.enabled, + ) + } + + private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator { + return Yield.Validator( + address = validatorDTO.address, + status = validatorDTO.status, + name = validatorDTO.name, + image = validatorDTO.image, + website = validatorDTO.website, + apr = validatorDTO.apr, + commission = validatorDTO.commission, + stakedBalance = validatorDTO.stakedBalance, + votingPower = validatorDTO.votingPower, + preferred = validatorDTO.preferred, + ) + } + + private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType { + return when (rewardTypeDTO) { + YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY + YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR + else -> Yield.RewardType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 7663df6823..092e8cfc62 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -2,7 +2,7 @@ package com.tangem.data.staking.di import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.local.token.StakingTokensStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -21,13 +21,13 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingFeatureToggles: StakingFeatureToggles, - stakingTokenStore: StakingTokensStore, + stakingTokenStore: StakingYieldsStore, dispatchers: CoroutineDispatcherProvider, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingFeatureToggles = stakingFeatureToggles, - stakingTokenStore = stakingTokenStore, + stakingYieldsStore = stakingTokenStore, dispatchers = dispatchers, ) } diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index f6a93898bb..80ff7080b9 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -1,9 +1,16 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.staking" +} + + dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + implementation(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index 55c8d1b3fa..b8609b2d1d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -1,6 +1,9 @@ package com.tangem.domain.staking import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.staking.error.StakingTokensError import com.tangem.domain.staking.repositories.StakingRepository /** @@ -10,6 +13,11 @@ class FetchStakingTokensUseCase( private val stakingRepository: StakingRepository, ) { suspend operator fun invoke(): Either { - return Either.catch { stakingRepository.fetchEnabledTokens() } + return either { + catch( + block = { stakingRepository.fetchEnabledYields() }, + catch = { StakingTokensError.DataError(it) }, + ) + } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 18717a31f7..2cc658f615 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -2,15 +2,16 @@ package com.tangem.domain.staking import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency /** - * Use case for getting info about staking availability for certain blockchain. + * Use case for getting info about staking capability in tangem app. */ class GetStakingAvailabilityUseCase( private val stakingRepository: StakingRepository, ) { - operator fun invoke(blockchainNetworkId: String): StakingAvailability { - return stakingRepository.getStakingAvailability(blockchainNetworkId) + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingAvailability { + return stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol) } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt new file mode 100644 index 0000000000..d97a592cbd --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.error + +sealed class StakingTokensError { + + data class DataError(val cause: Throwable) : StakingTokensError() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt index bf40ee3b58..d7eaa306e0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt @@ -5,4 +5,6 @@ sealed class StakingAvailability { data class Available(val integrationId: String) : StakingAvailability() data object Unavailable : StakingAvailability() + + data object TemporaryDisabled : StakingAvailability() } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt new file mode 100644 index 0000000000..3ef85e77dd --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt @@ -0,0 +1,172 @@ +package com.tangem.domain.staking.model + +import java.math.BigDecimal + +data class Yield( + val id: String, + val token: Token, + val tokens: List, + val args: Args, + val status: Status, + val apy: BigDecimal, + val rewardRate: Double, + val rewardType: RewardType, + val metadata: Metadata, + val validators: List, + val isAvailable: Boolean, +) { + data class Status( + val enter: Boolean, + val exit: Boolean?, + ) + + data class Args( + val enter: Enter, + val exit: Enter?, + ) { + data class Enter( + val addresses: Addresses, + val args: Map, + ) { + data class Addresses( + val address: AddressArgument, + val additionalAddresses: Map? = null, + ) + } + } + + data class Validator( + val address: String, + val status: String, + val name: String, + val image: String?, + val website: String?, + val apr: Double?, + val commission: Double?, + val stakedBalance: String?, + val votingPower: Double?, + val preferred: Boolean, + ) + + data class Metadata( + val name: String, + val logoUri: String, + val description: String, + val documentation: String?, + val gasFeeToken: Token, + val token: Token, + val tokens: List, + val type: String, + val rewardSchedule: String, + val cooldownPeriod: Period, + val warmupPeriod: Period, + val rewardClaiming: String, + val defaultValidator: String?, + val minimumStake: Int?, + val supportsMultipleValidators: Boolean, + val revshare: Enabled, + val fee: Enabled, + ) { + data class Period( + val days: Int, + ) + + data class Enabled( + val enabled: Boolean, + ) + } + + enum class RewardType { + APY, // compound rate + APR, // simple rate + UNKNOWN, + } +} + +data class Token( + val name: String, + val network: NetworkType, + val symbol: String, + val decimals: Int, + val address: String?, + val coinGeckoId: String?, + val logoURI: String?, + val isPoints: Boolean?, +) { + enum class NetworkType { + AVALANCHE_C, + AVALANCHE_ATOMIC, + AVALANCHE_P, + ARBITRUM, + BINANCE, + CELO, + ETHEREUM, + ETHEREUM_GOERLI, + ETHEREUM_HOLESKY, + FANTOM, + HARMONY, + OPTIMISM, + POLYGON, + GNOSIS, + MOONRIVER, + OKC, + ZKSYNC, + VICTION, + AGORIC, + AKASH, + AXELAR, + BAND_PROTOCOL, + BITSONG, + CANTO, + CHIHUAHUA, + COMDEX, + COREUM, + COSMOS, + CRESCENT, + CRONOS, + CUDOS, + DESMOS, + DYDX, + EVMOS, + FETCH_AI, + GRAVITY_BRIDGE, + INJECTIVE, + IRISNET, + JUNO, + KAVA, + KI_NETWORK, + MARS_PROTOCOL, + NYM, + OKEX_CHAIN, + ONOMY, + OSMOSIS, + PERSISTENCE, + QUICKSILVER, + REGEN, + SECRET, + SENTINEL, + SOMMELIER, + STAFI, + STARGAZE, + STRIDE, + TERITORI, + TGRADE, + UMEE, + POLKADOT, + KUSAMA, + WESTEND, + BINANCEBEACON, + NEAR, + SOLANA, + TEZOS, + TRON, + UNKNOWN, + } +} + +data class AddressArgument( + val required: Boolean, + val network: String? = null, + val minimum: Double? = null, + val maximum: Double? = null, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5e84852870..eafbb514fb 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,12 +2,18 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.tokens.model.CryptoCurrency interface StakingRepository { - fun getStakingAvailability(blockchainId: String): StakingAvailability + suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability suspend fun getEntryInfo(integrationId: String): StakingEntryInfo - suspend fun fetchEnabledTokens() + suspend fun fetchEnabledYields() + + fun isStakingSupported(currencyId: String): Boolean } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index de3ef835c5..56b3e441ab 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.domain.staking) implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e521d035af..ffef6ff04d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -2,6 +2,8 @@ package com.tangem.domain.tokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,6 +30,7 @@ class GetCryptoCurrencyActionsUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -121,6 +124,17 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Receive(scenario)) } + // staking + if (isStakingAvailable(cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) + } else { + disabledList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + ), + ) + } + // send val sendUnavailabilityReason = getSendUnavailabilityReason( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -234,6 +248,7 @@ class GetCryptoCurrencyActionsUseCase( } actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return actionsList } @@ -264,4 +279,11 @@ class GetCryptoCurrencyActionsUseCase( private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } + + private suspend fun isStakingAvailable(cryptoCurrency: CryptoCurrency): Boolean { + return stakingRepository.getStakingAvailabilityForActions( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) is StakingAvailability.Available + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index 54db16bf92..ead2d06c56 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -3,6 +3,9 @@ package com.tangem.domain.tokens.model sealed class ScenarioUnavailabilityReason { data object None : ScenarioUnavailabilityReason() + // staking-specific + data class StakingUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + // send&sell-specific data class PendingTransaction( val withdrawalScenario: WithdrawalScenario, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index 372f1e51d5..a3dbea0959 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -20,6 +20,8 @@ data class TokenActionsState( data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() 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 470cc0ac26..4b3900f2c3 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 @@ -260,7 +260,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - isStakingAvailable = false, + isStakingBlockShown = false, event = consumedEvent(), ) @@ -296,7 +296,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - isStakingAvailable = true, + isStakingBlockShown = true, event = consumedEvent(), ) 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 26f36f81ad..d1ec291d51 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 @@ -27,6 +27,6 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val isStakingAvailable: Boolean, + val isStakingBlockShown: Boolean, val event: StateEvent, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index c5b6078527..d1ef7792c8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -91,4 +91,19 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { dimContent = dimContent, ), ) + + /** + * Staking + * + * @property dimContent determines whether the button content will be dimmed + * @property onClick lambda be invoked when Swap button is clicked + */ + data class Stake(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_stake), + iconResId = R.drawable.ic_arrow_down_24, // TODO staking + onClick = onClick, + dimContent = dimContent, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt index dbf8a99f3c..afe2413ce6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -38,6 +38,12 @@ internal class TokenDetailsActionButtonsConverter( onLongClick = clickIntents::onCopyAddress, ) } + is TokenActionsState.ActionState.Stake -> { + TokenDetailsActionButton.Stake( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onStakeClick(action.unavailabilityReason) }, + ) + } is TokenActionsState.ActionState.Sell -> { TokenDetailsActionButton.Sell( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 2c29e74ce3..9cc42f3c2d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.tokendetails.presentation.tokendetails.state.* @@ -17,7 +16,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin -import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -27,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, - private val stakingAvailabilityProvider: Provider, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -67,7 +64,7 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available, + isStakingBlockShown = false, event = consumedEvent(), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index af67c4e57d..cec107a630 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -44,7 +44,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val stakingAvailabilityProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, symbol: String, @@ -55,7 +54,6 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, featureToggles = featureToggles, - stakingAvailabilityProvider = stakingAvailabilityProvider, ) } @@ -211,6 +209,12 @@ internal class TokenDetailsStateFactory( ) } + fun getStateWithUpdatedStakingAvailability(stakingAvailability: StakingAvailability): TokenDetailsState { + return currentStateProvider().copy( + isStakingBlockShown = stakingAvailability != StakingAvailability.Unavailable, + ) + } + fun getStateWithStaking(stakingEither: Either): TokenDetailsState { return currentStateProvider().copy( stakingBlockState = stakingStateConverter.convert(stakingEither), @@ -369,6 +373,12 @@ internal class TokenDetailsStateFactory( private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } is ScenarioUnavailabilityReason.PendingTransaction -> { when (unavailabilityReason.withdrawalScenario) { ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( 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 d9eb9ec18d..ddcb006eae 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 @@ -135,7 +135,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } - if (state.isStakingAvailable) { + if (state.isStakingBlockShown) { item( key = StakingBlockState::class.java, contentType = StakingBlockState::class.java, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 751b8dcd92..5de4fa2c87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -12,6 +12,8 @@ interface TokenDetailsClickIntents { fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) 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 610f053855..9bcb6d6a55 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 @@ -142,9 +142,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - stakingAvailabilityProvider = Provider { - getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value) - }, clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, @@ -369,7 +366,11 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateStakingInfo() { viewModelScope.launch(dispatchers.io) { - val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value) + val stakingAvailability = getStakingAvailabilityUseCase( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) + uiState = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) uiState = stateFactory.getStateWithStaking(stakingInfo) @@ -524,6 +525,10 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) { + Timber.e("Not implemented yet") + } + override fun onGenerateExtendedKey() { viewModelScope.launch(dispatchers.main) { val extendedKey = getExtendedPublicKeyForCurrencyUseCase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index afbd667405..19da0ca6a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -89,6 +89,19 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { ), ) + data class Stake( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_stake), + iconResId = R.drawable.ic_arrow_down_24, // TODO staking + onClick = onClick, + dimContent = dimContent, + ), + ) + /** * Sell * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 0f6d520acf..58ceb2ac53 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -64,6 +64,7 @@ internal class SetRefreshStateTransformer( is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button + is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index fa7478a428..417ff93ede 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -59,6 +59,11 @@ internal class MultiWalletCurrencyActionsConverter( icon = R.drawable.ic_arrow_down_24 action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } } + is TokenActionsState.ActionState.Stake -> { + title = resourceReference(R.string.common_stake) + icon = R.drawable.ic_arrow_down_24 // TODO staking replace icon + action = { clickIntents.onStakeClick(cryptoCurrencyStatus) } + } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) icon = R.drawable.ic_currency_24 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 318fddb6c3..606a27ad3e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -58,6 +59,8 @@ interface WalletCurrencyActionsClickIntents { fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) @@ -363,6 +366,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } + override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO staking + Timber.e("Not implemented yet") + } + private fun openExplorer() { val userWalletId = stateHolder.getSelectedWalletId() @@ -465,6 +473,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } is ScenarioUnavailabilityReason.PendingTransaction -> { when (unavailabilityReason.withdrawalScenario) { ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( From 199b7d37434da05b5d779e4e3ea7309876e8da76 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 6 Jun 2024 15:26:51 +0400 Subject: [PATCH 008/223] Updated on 2026-08-14 --- .../core/ui/ComposableContentProvider.kt | 13 ++ .../core/ui/extensions/TextReference.kt | 12 ++ .../kotlin/com/tangem/domain/core/lce/Lce.kt | 11 ++ .../com/tangem/domain/core/lce/LceRaise.kt | 44 +++++- features/details/impl/build.gradle.kts | 13 ++ .../features/details/DetailsFragment.kt | 46 +++--- .../details/component/DetailsComponent.kt | 9 +- .../component/UserWalletListComponent.kt | 9 +- .../component/WalletConnectComponent.kt | 23 --- .../component/impl/DefaultDetailsComponent.kt | 53 +++++++ .../impl/DefaultUserWalletListComponent.kt | 37 +++++ .../preview/PreviewDetailsComponent.kt | 7 +- .../preview/PreviewUserWalletListComponent.kt | 2 +- .../preview/PreviewWalletConnectComponent.kt | 17 --- .../features/details/di/ComponentModule.kt | 26 ++++ .../tangem/features/details/di/ModelModule.kt | 6 + .../details/entity/DetailsFooterUM.kt | 2 + .../features/details/entity/DetailsItemUM.kt | 17 +-- .../features/details/model/DetailsModel.kt | 68 ++++++++- .../details/model/UserWalletListModel.kt | 74 ++++++++++ .../features/details/ui/DetailsScreen.kt | 38 ++++- .../details/ui/UserWalletListBlock.kt | 24 ++- .../features/details/utils/ItemsBuilder.kt | 35 ++--- .../features/details/utils/SocialsBuilder.kt | 5 +- .../details/utils/UserWalletMappers.kt | 84 +++++++++++ .../features/details/utils/UserWalletSaver.kt | 139 ++++++++++++++++++ .../details/utils/UserWalletsFetcher.kt | 95 ++++++++++++ 27 files changed, 774 insertions(+), 135 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt b/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt new file mode 100644 index 0000000000..28868a7c18 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier + +@Stable +fun interface ComposableContentProvider { + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun Content(modifier: Modifier) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 2f22994f64..13373f947b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import org.intellij.markdown.MarkdownElementTypes +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract /** * Utility class for creating text as [String] or [StringRes]. @@ -197,6 +199,16 @@ operator fun TextReference.plus(ref: TextReference): TextReference { } } +@Suppress("NOTHING_TO_INLINE") +@OptIn(ExperimentalContracts::class) +inline fun TextReference?.isNullOrEmpty(): Boolean { + contract { + returns(false) implies (this@isNullOrEmpty != null) + } + + return this == null || this == TextReference.EMPTY +} + @Composable private fun formatAnnotated(rawString: String): AnnotatedString { val markdownDescriptor = rememberMarkdownParser() diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt index 65ef8788fa..0b65333482 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -93,4 +93,15 @@ sealed class Lce { ifContent = ::identity, ifError = { null }, ) + + /** + * Returns the error of this [Lce] if it's a [Lce.Error], `null` otherwise. + * + * @return The error of this [Lce] or `null`. + */ + fun errorOrNull(): E? = fold( + ifLoading = { null }, + ifContent = { null }, + ifError = ::identity, + ) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt index 90dbc6d172..2040b0774a 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt @@ -2,8 +2,10 @@ package com.tangem.domain.core.lce import arrow.atomic.Atomic import arrow.core.raise.Raise +import arrow.core.raise.RaiseDSL import arrow.core.raise.recover import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import kotlin.experimental.ExperimentalTypeInference @@ -18,18 +20,55 @@ class LceRaise @PublishedApi internal constructor( private val raise: Raise>, ) : Raise> by raise { + /** + * An [Atomic] boolean flag indicating whether a loading operation is in progress. + */ val isLoading: Atomic = Atomic(false) + /** + * Helper function to raise an [Lce.Error] state with the given error object. + * */ + @RaiseDSL + @JvmName(name = "raiseError") + fun raise(r: E): Nothing = raise(r = r.lceError()) + + /** + * Helper function to raise an [Lce.Loading] state. + */ + @RaiseDSL + fun raiseLoading(): Nothing = raise(r = lceLoading()) + + /** + * Execute the [Raise] context function resulting in [C] or any _logical error_ of type [OtherError], + * and transform any raised [OtherError] into [E], which is raised to the outer [Raise]. + * + * @see arrow.core.raise.withError + * */ + @RaiseDSL + @OptIn(ExperimentalTypeInference::class) + inline fun withError( + transform: (OtherError) -> E, + @BuilderInference block: LceRaise.() -> C, + ): C = recover( + block = { block(LceRaise(raise = this@recover)) }, + recover = { error -> + error.fold( + ifLoading = { raiseLoading() }, + ifError = { raise(transform(it)) }, + ifContent = { it }, + ) + }, + ) + /** * Binds the content of this [Lce] instance and handles its state. * If this is a [Lce.Loading] state, sets the [isLoading] flag to true and calls the [ifLoading] function. * If this is a [Lce.Content] state, returns the content. * If this is a [Lce.Error] state, raises the error. * - * @param ifLoading The function to call if this is a [Lce.Loading] state. - * By default, it raises a new [Lce.Loading] state. * @return The content of this [Lce] instance. */ + @RaiseDSL fun Lce.bind(): C = when (this) { is Lce.Loading -> { isLoading.set(true) @@ -48,6 +87,7 @@ class LceRaise @PublishedApi internal constructor( * * @return The content of this [Lce] instance. */ + @RaiseDSL fun Lce.bindOrNull(): C? = when (this) { is Lce.Loading -> { isLoading.set(true) diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 5060249621..8a26e80f40 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -25,9 +25,21 @@ dependencies { implementation(projects.core.analytics.models) /* Project - Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.card) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.walletConnect) implementation(projects.domain.legacy) + /* SDK */ + // TODO: For TangemError model, should be removed after card domain scanning refactoring + implementation(deps.tangem.card.core) + /* AndroidX */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) @@ -47,4 +59,5 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt index 28db59fc3d..4bd1e416ee 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt @@ -3,14 +3,16 @@ package com.tangem.features.details import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.Fragment +import com.arkivanov.decompose.defaultComponentContext import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.message.EventMessageEffect import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.component.DetailsComponent -import com.tangem.features.details.component.preview.PreviewDetailsComponent import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -21,8 +23,8 @@ internal class DetailsFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies - // @Inject - // internal lateinit var componentFactory: DetailsComponent.Factory + @Inject + internal lateinit var componentFactory: DetailsComponent.Factory @Inject internal lateinit var detailsRouter: DetailsRouter @@ -37,7 +39,7 @@ internal class DetailsFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - component.View(modifier = modifier) + component.Content(modifier = modifier) EventMessageEffect( messageHandler = messageHandler, @@ -46,27 +48,23 @@ internal class DetailsFragment : ComposeFragment() { } private fun initComponent(): DetailsComponent { - // TODO: Uncomment in [REDACTED_JIRA] - // val selectedUserWalletId = arguments?.getString(DetailsEntryPoint.USER_WALLET_ID_KEY) - // ?.let(::UserWalletId) - // - // - // requireNotNull(selectedUserWalletId) { "UserWalletId must be provided" } - // - // val context = rootContext.childByContext( - // componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - // messageHandler = messageHandler, - // router = detailsRouter, - // ) - // - // return componentFactory.create( - // context = context, - // params = DetailsComponent.Params( - // selectedUserWalletId = selectedUserWalletId, - // ), - // ) + val selectedUserWalletId = arguments?.getString(DetailsEntryPoint.USER_WALLET_ID_KEY) + ?.let(::UserWalletId) - return PreviewDetailsComponent() + requireNotNull(selectedUserWalletId) { "UserWalletId must be provided" } + + val context = rootContext.childByContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = messageHandler, + router = detailsRouter, + ) + + return componentFactory.create( + context = context, + params = DetailsComponent.Params( + selectedUserWalletId = selectedUserWalletId, + ), + ) } companion object : DetailsEntryPoint { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index 9731597b9b..1f4026d32a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -1,19 +1,14 @@ package com.tangem.features.details.component import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.ComposableContentProvider import com.tangem.domain.wallets.models.UserWalletId -interface DetailsComponent { +interface DetailsComponent : ComposableContentProvider { val snackbarHostState: SnackbarHostState - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - fun View(modifier: Modifier) - interface Factory { fun create(context: AppComponentContext, params: Params): DetailsComponent diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt index 0a76b5fcd1..8e7ffc063a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt @@ -1,14 +1,9 @@ package com.tangem.features.details.component -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.ComposableContentProvider -interface UserWalletListComponent { - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - fun View(modifier: Modifier) +interface UserWalletListComponent : ComposableContentProvider { interface Factory { fun create(context: AppComponentContext): UserWalletListComponent diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt deleted file mode 100644 index c8a317814b..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.features.details.component - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.wallets.models.UserWalletId - -interface WalletConnectComponent { - - suspend fun checkIsAvailable(): Boolean - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - fun View(modifier: Modifier) - - interface Factory { - fun create(context: AppComponentContext, params: Params): WalletConnectComponent - } - - data class Params( - val userWalletId: UserWalletId, - ) -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt new file mode 100644 index 0000000000..bc9a891d84 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt @@ -0,0 +1,53 @@ +package com.tangem.features.details.component.impl + +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.model.DetailsModel +import com.tangem.features.details.ui.DetailsScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultDetailsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: DetailsComponent.Params, + userWalletListComponentFactory: UserWalletListComponent.Factory, +) : DetailsComponent, AppComponentContext by context { + + private val model: DetailsModel = getOrCreateModel() + + private val userWalletListComponent = userWalletListComponentFactory.create( + context = child(key = "user_wallet_list"), + ) + + override val snackbarHostState: SnackbarHostState = SnackbarHostState() + + init { + model.provideUserWalletId(params.selectedUserWalletId) + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + DetailsScreen( + modifier = modifier, + state = state, + snackbarHostState = snackbarHostState, + userWalletListBlockContent = userWalletListComponent, + ) + } + + @AssistedFactory + interface Factory : DetailsComponent.Factory { + override fun create(context: AppComponentContext, params: DetailsComponent.Params): DefaultDetailsComponent + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt new file mode 100644 index 0000000000..53279cde55 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.details.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.model.UserWalletListModel +import com.tangem.features.details.ui.UserWalletListBlock +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultUserWalletListComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, +) : UserWalletListComponent, AppComponentContext by context { + + private val model: UserWalletListModel = getOrCreateModel() + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + UserWalletListBlock( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : UserWalletListComponent.Factory { + + override fun create(context: AppComponentContext): DefaultUserWalletListComponent + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 9c6aff4521..3981734a85 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -17,10 +17,8 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( - walletConnectComponent = PreviewWalletConnectComponent(), - userWalletListComponent = PreviewUserWalletListComponent(), router = PreviewRouter(), - ).buldAll() + ).buldAll(isWalletConnectAvailable = true) } private val previewFooter = DetailsFooterUM( @@ -36,11 +34,12 @@ internal class PreviewDetailsComponent : DetailsComponent { @Composable @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - override fun View(modifier: Modifier) { + override fun Content(modifier: Modifier) { DetailsScreen( modifier = modifier, state = previewState, snackbarHostState = snackbarHostState, + userWalletListBlockContent = PreviewUserWalletListComponent(), ) } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index c7628c2619..09950611e8 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -46,7 +46,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { @Composable @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - override fun View(modifier: Modifier) { + override fun Content(modifier: Modifier) { UserWalletListBlock(state = previewState, modifier = modifier) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt deleted file mode 100644 index 4f94585d78..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.details.component.preview - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.features.details.component.WalletConnectComponent -import com.tangem.features.details.ui.WalletConnectBlock - -internal class PreviewWalletConnectComponent : WalletConnectComponent { - - override suspend fun checkIsAvailable(): Boolean = true - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - override fun View(modifier: Modifier) { - WalletConnectBlock(onClick = { /* no-op */ }, modifier = modifier) - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt new file mode 100644 index 0000000000..27fd842ab3 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.details.di + +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.component.impl.DefaultDetailsComponent +import com.tangem.features.details.component.impl.DefaultUserWalletListComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindDetailsComponentFactory(factory: DefaultDetailsComponent.Factory): DetailsComponent.Factory + + @Binds + @Singleton + fun bindUserWalletListComponentFactory( + factory: DefaultUserWalletListComponent.Factory, + ): UserWalletListComponent.Factory +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt index 8b429e0438..63481527c2 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.details.di import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model import com.tangem.features.details.model.DetailsModel +import com.tangem.features.details.model.UserWalletListModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -17,4 +18,9 @@ internal interface ModelModule { @IntoMap @ClassKey(DetailsModel::class) fun provideDetailsModel(model: DetailsModel): Model + + @Binds + @IntoMap + @ClassKey(UserWalletListModel::class) + fun provideUserWalletListModel(model: UserWalletListModel): Model } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt index 267acbe9d0..c614a5af8d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt @@ -1,8 +1,10 @@ package com.tangem.features.details.entity import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.ImmutableList +@Immutable internal data class DetailsFooterUM( val appVersion: String, val socials: ImmutableList, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt index 1aa5a52ab5..ea5da389e8 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt @@ -1,9 +1,7 @@ package com.tangem.features.details.entity import androidx.annotation.DrawableRes -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.ui.Modifier import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -26,16 +24,11 @@ internal sealed class DetailsItemUM { ) } - data class Component( - override val id: String, - val content: Content, - ) : DetailsItemUM() { + data class WalletConnect(val onClick: () -> Unit) : DetailsItemUM() { + override val id: String = "wallet_connect" + } - fun interface Content { - - @Composable - @Suppress("TopLevelComposableFunctions", "ComposableFunctionName") - operator fun invoke(modifier: Modifier) - } + data object UserWalletList : DetailsItemUM() { + override val id: String = "user_wallet_list" } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 918c27eb97..189172b899 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -1,10 +1,74 @@ package com.tangem.features.details.model +import arrow.core.getOrElse +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.details.entity.DetailsFooterUM +import com.tangem.features.details.entity.DetailsItemUM +import com.tangem.features.details.entity.DetailsUM +import com.tangem.features.details.utils.ItemsBuilder +import com.tangem.features.details.utils.SocialsBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.version.AppVersionProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject -// TODO: Will be implemented later +@ComponentScoped internal class DetailsModel @Inject constructor( + private val socialsBuilder: SocialsBuilder, + private val itemsBuilder: ItemsBuilder, + private val appVersionProvider: AppVersionProvider, + private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, + private val router: Router, override val dispatchers: CoroutineDispatcherProvider, -) : Model() \ No newline at end of file +) : Model() { + + private val items: MutableStateFlow> = MutableStateFlow(value = persistentListOf()) + + val state: MutableStateFlow = MutableStateFlow( + value = DetailsUM( + items = items.value, + footer = DetailsFooterUM( + socials = socialsBuilder.buildAll(), + appVersion = getAppVersion(), + ), + popBack = router::pop, + ), + ) + + init { + items + .onEach(::updateState) + .launchIn(modelScope) + } + + fun provideUserWalletId(userWalletId: UserWalletId) = modelScope.launch { + val isWalletConnectAvailable = checkIsWalletConnectAvailableUseCase(userWalletId).getOrElse { + Timber.w("Unable to check WalletConnect availability: $it") + + false + } + + items.value = itemsBuilder.buldAll(isWalletConnectAvailable) + } + + private suspend fun updateState(items: ImmutableList) { + state.update { prevState -> + prevState.copy( + items = items, + ) + } + } + + private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})" +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt new file mode 100644 index 0000000000..202758486c --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -0,0 +1,74 @@ +package com.tangem.features.details.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM +import com.tangem.features.details.impl.R +import com.tangem.features.details.utils.UserWalletSaver +import com.tangem.features.details.utils.UserWalletsFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ComponentScoped +internal class UserWalletListModel @Inject constructor( + private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val userWalletSaver: UserWalletSaver, + private val userWalletsFetcher: UserWalletsFetcher, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) + + private val userWallets: SharedFlow> = userWalletsFetcher + .userWallets + .share() + + private val shouldSaveUserWallets: SharedFlow = shouldSaveUserWalletsUseCase() + .distinctUntilChanged() + .share() + + val state: MutableStateFlow = MutableStateFlow( + value = UserWalletListUM( + userWallets = persistentListOf(), + isWalletSavingInProgress = false, + addNewWalletText = TextReference.EMPTY, + onAddNewWalletClick = ::addUserWallet, + ), + ) + + init { + combine( + userWallets, + shouldSaveUserWallets, + isWalletSavingInProgress, + transform = ::updateState, + ).launchIn(modelScope) + } + + private suspend fun updateState( + userWallets: ImmutableList, + shouldSaveUserWallets: Boolean, + isWalletSavingInProgress: Boolean, + ) = state.update { value -> + value.copy( + userWallets = userWallets, + isWalletSavingInProgress = isWalletSavingInProgress, + addNewWalletText = if (shouldSaveUserWallets) { + resourceReference(R.string.user_wallet_list_add_button) + } else { + resourceReference(R.string.scan_card_settings_button) + }, + ) + } + + private fun addUserWallet() = withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet() + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 0b03305816..c0f66280bc 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.ComposableContentProvider import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.res.TangemTheme @@ -32,7 +33,12 @@ private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) { +internal fun DetailsScreen( + state: DetailsUM, + snackbarHostState: SnackbarHostState, + userWalletListBlockContent: ComposableContentProvider, + modifier: Modifier = Modifier, +) { val backgroundColor = TangemTheme.colors.background.secondary val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -56,6 +62,7 @@ internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostStat Content( modifier = Modifier.padding(paddingValues), state = state, + userWalletListBlockContent = userWalletListBlockContent, ) } } @@ -109,7 +116,11 @@ private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, mo } @Composable -private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { +private fun Content( + state: DetailsUM, + userWalletListBlockContent: ComposableContentProvider, + modifier: Modifier = Modifier, +) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), @@ -125,6 +136,7 @@ private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { Block( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), model = block, + userWalletListBlockContent = userWalletListBlockContent, ) } @@ -138,7 +150,11 @@ private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { } @Composable -private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) { +private fun Block( + model: DetailsItemUM, + userWalletListBlockContent: ComposableContentProvider, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth() @@ -149,22 +165,28 @@ private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) { horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, ) { + val itemModifier = Modifier.fillMaxWidth() + when (model) { is DetailsItemUM.Basic -> { model.items.forEach { item -> key(item.id) { BlockItem( - modifier = Modifier.fillMaxWidth(), + modifier = itemModifier, model = item, ) } } } - is DetailsItemUM.Component -> { - model.content( - modifier = Modifier.fillMaxWidth(), + is DetailsItemUM.WalletConnect -> { + WalletConnectBlock( + modifier = itemModifier, + onClick = model.onClick, ) } + is DetailsItemUM.UserWalletList -> { + userWalletListBlockContent.Content(modifier = itemModifier) + } } } } @@ -222,7 +244,7 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) { @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun Preview_DetailsScreen() { TangemThemePreview { - PreviewDetailsComponent().View(modifier = Modifier.fillMaxSize()) + PreviewDetailsComponent().Content(modifier = Modifier.fillMaxSize()) } } // endregion Preview \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 254c434047..578e14df1f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,7 +1,9 @@ package com.tangem.features.details.ui +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -104,12 +106,24 @@ private fun AddWalletButton( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - Icon( + AnimatedContent( modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_plus_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) + targetState = isInProgress, + ) { isInProgress -> + if (isInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.accent, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_plus_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } Text( text = text.resolveReference(), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 9bd974e94c..fb7e8768e4 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -1,11 +1,10 @@ package com.tangem.features.details.utils +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.details.component.UserWalletListComponent -import com.tangem.features.details.component.WalletConnectComponent import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R @@ -13,47 +12,39 @@ import com.tangem.features.details.routing.DetailsRoute import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject -internal class ItemsBuilder( - private val walletConnectComponent: WalletConnectComponent, - private val userWalletListComponent: UserWalletListComponent, +@ComponentScoped +internal class ItemsBuilder @Inject constructor( private val router: Router, ) { - suspend fun buldAll(): ImmutableList = buildList { - buildWalletConnectBlock()?.let(::add) + suspend fun buldAll(isWalletConnectAvailable: Boolean): ImmutableList = buildList { + buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) buildUserWalletListBlock().let(::add) buildShopBlock().let(::add) buildSettingsBlock().let(::add) buildSupportBlock().let(::add) }.toImmutableList() - private suspend fun buildWalletConnectBlock(): DetailsItemUM? { - return if (walletConnectComponent.checkIsAvailable()) { - DetailsItemUM.Component( - id = "wallet_connect", - content = { - walletConnectComponent.View(modifier = it) - }, + private suspend fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { + return if (isWalletConnectAvailable) { + DetailsItemUM.WalletConnect( + onClick = { router.push(DetailsRoute.Screen(AppScreen.WalletConnectSessions)) }, ) } else { null } } - private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.Component( - id = "user_wallet_list", - content = { - userWalletListComponent.View(modifier = it) - }, - ) + private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.UserWalletList private fun buildShopBlock(): DetailsItemUM = DetailsItemUM.Basic( id = "shop", items = persistentListOf( DetailsItemUM.Basic.Item( id = "buy_tangem_wallet", - title = stringReference("Buy Tangem Wallet"), // TODO: Move to resources in [REDACTED_TASK_KEY] + title = resourceReference(R.string.details_buy_wallet), iconRes = R.drawable.ic_tangem_24, onClick = { router.push(DetailsRoute.Url(BUY_TANGEM_URL)) }, ), @@ -86,7 +77,7 @@ internal class ItemsBuilder( items = persistentListOf( DetailsItemUM.Basic.Item( id = "send_feedback", - title = stringReference("Send feedback"), // TODO: Move to resources in [REDACTED_TASK_KEY] + title = resourceReference(R.string.details_send_feedback), iconRes = R.drawable.ic_comment_24, onClick = { router.push(DetailsRoute.Feedback) }, ), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt index 6896f1e7d0..9d54a022c2 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt @@ -1,14 +1,17 @@ package com.tangem.features.details.utils import androidx.compose.ui.text.intl.Locale +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.impl.R import com.tangem.features.details.routing.DetailsRoute import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject -internal class SocialsBuilder( +@ComponentScoped +internal class SocialsBuilder @Inject constructor( private val router: Router, ) { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt new file mode 100644 index 0000000000..992fee20eb --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -0,0 +1,84 @@ +package com.tangem.features.details.utils + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM +import com.tangem.features.details.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal fun List.toUiModels( + onClick: (UserWalletId) -> Unit, + appCurrency: AppCurrency? = null, + balances: Map = emptyMap(), +): ImmutableList = this.map { model -> + val balance = balances[model.walletId] + model.mapToUiModel( + balance = balance, + appCurrency = appCurrency, + onClick = { onClick(model.walletId) }, + ) +}.toImmutableList() + +private fun UserWallet.mapToUiModel( + balance: TotalFiatBalance?, + appCurrency: AppCurrency?, + onClick: () -> Unit, +): UserWalletUM = UserWalletUM( + id = walletId, + name = name, + information = getInfo(appCurrency, balance), + imageResId = resolveImage(), + onClick = onClick, +) + +private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBalance?): TextReference { + val cardCount = getCardCount() + val cardCountRef = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + val amount = when (balance) { + is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized } + is TotalFiatBalance.Failed, + is TotalFiatBalance.Loading, + null, + -> null + } + + return if (amount != null && appCurrency != null) { + val divider = stringReference(value = " • ") + val formattedAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + val amountRef = stringReference(formattedAmount) + TextReference.Combined(wrappedList(cardCountRef, divider, amountRef)) + } else { + cardCountRef + } +} + +private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount.inc() + is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc() + is CardDTO.BackupStatus.NoBackup, + null, + -> 1 +} + +@DrawableRes +private fun UserWallet.resolveImage(): Int { + // TODO: Implement image resolving [REDACTED_JIRA] + return R.drawable.ill_card_wallet_2_211_343 +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt new file mode 100644 index 0000000000..243c6849d7 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -0,0 +1,139 @@ +package com.tangem.features.details.utils + +import arrow.core.raise.* +import arrow.core.recover +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.AppScreen +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.features.details.impl.R +import com.tangem.features.details.routing.DetailsRoute +import javax.inject.Inject + +@ComponentScoped +@Suppress("LongParameterList") +internal class UserWalletSaver @Inject constructor( + private val scanCardProcessor: ScanCardProcessor, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val selectWalletUseCase: SelectWalletUseCase, + private val reduxStateHolder: ReduxStateHolder, + private val messageSender: UiMessageSender, + private val router: Router, +) { + + suspend fun scanAndSaveUserWallet() = recover( + block = { + val response = scanCard() + val userWallet = createUserWallet(response) + + saveWallet(userWallet) + + router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + }, + recover = { error -> + val message = error.message + + if (!message.isNullOrEmpty()) { + messageSender.send(SnackbarMessage(message)) + } + }, + ) + + private suspend fun Raise.saveWallet(userWallet: UserWallet) { + saveWalletUseCase(userWallet).recover { error -> + when (error) { + is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet) + is SaveWalletError.DataError -> { + val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { + Error.Unkonwn + } + + raise(Error.Message(messageRef)) + } + } + }.bind() + + reduxStateHolder.onUserWalletSelected(userWallet) + } + + private suspend fun Raise.selectUserWallet(userWallet: UserWallet) { + withError({ Error.Unkonwn }) { + selectWalletUseCase(userWallet.walletId).bind() + } + + router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + } + + private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { + val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build() + + return ensureNotNull(userWallet) { Error.Unkonwn } + } + + private suspend fun Raise.scanCard(): ScanResponse { + var response: ScanResponse? = null + + scanCardProcessor.scan( + analyticsSource = AnalyticsParam.ScreensSources.Settings, + onWalletNotCreated = { + raise(Error.WalletNotCreated) + }, + disclaimerWillShow = { + router.pop() + raise(Error.DisclaimerWillShow) + }, + onSuccess = { + response = it + }, + onFailure = { tangemError -> + val error = if (!tangemError.silent) { + val message = tangemError.messageResId + ?.let(::resourceReference) + ?: stringReference(tangemError.customMessage) + + Error.Message(message) + } else { + Error.Silent + } + + raise(error) + }, + ) + + return response!! + } + + sealed class Error { + + open val message: TextReference? = null + + data object WalletNotCreated : Error() + + data object DisclaimerWillShow : Error() + + data object Silent : Error() + + data class Message(override val message: TextReference) : Error() + + data object Unkonwn : Error() { + + override val message: TextReference = resourceReference(R.string.common_unknown_error) + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt new file mode 100644 index 0000000000..73b54461fb --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -0,0 +1,95 @@ +package com.tangem.features.details.utils + +import arrow.core.Either +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.lce +import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.core.utils.toLce +import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM +import com.tangem.features.details.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ComponentScoped +internal class UserWalletsFetcher @Inject constructor( + private val getWalletsUseCase: GetWalletsUseCase, + private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val messageSender: UiMessageSender, +) { + + val userWallets: Flow> = getWalletsUseCase() + .distinctUntilChanged() + .transform { wallets -> + if (wallets.isEmpty()) { + error("Wallets must not be empty") + } else { + emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) + } + + combine( + getSelectedAppCurrencyUseCase(), + getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)), + ) { maybeAppCurrency, maybeBalances -> + val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse( + ifLoading = { return@combine }, + ifError = { + val message = resourceReference(R.string.common_unknown_error) + messageSender.send(SnackbarMessage(message)) + + return@combine + }, + ) + + emit(models) + }.collect() + } + + private fun createUiModels( + wallets: List, + maybeAppCurrency: Either, + maybeBalances: Lce>, + ): Lce> = lce { + val balances = withError( + transform = { Error.UnableToGetBalances }, + block = { maybeBalances.bind() }, + ) + val appCurrency = withError( + transform = { Error.UnableToGetAppCurrency }, + block = { maybeAppCurrency.toLce().bind() }, + ) + + wallets.toUiModels( + appCurrency = appCurrency, + balances = balances, + onClick = ::navigateToWalletSettings, + ) + } + + private fun navigateToWalletSettings(userWalletId: UserWalletId) { + val message = stringReference("Wallet settings have not yet been implemented: $userWalletId") + messageSender.send(SnackbarMessage(message)) + } + + sealed class Error { + + data object UnableToGetAppCurrency : Error() + + data object UnableToGetBalances : Error() + } +} \ No newline at end of file From 20adf074844a0327d375acbcfad13aa35e162517 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 13:23:01 +0500 Subject: [PATCH 009/223] Updated on 2026-08-14 --- .../send/redux/reducers/AmountReducer.kt | 2 +- common/ui/.gitignore | 1 + common/ui/build.gradle.kts | 33 +++++++++++++++++++ common/ui/consumer-rules.pro | 0 common/ui/proguard-rules.pro | 21 ++++++++++++ core/ui/build.gradle.kts | 3 -- .../ui/components/transactions/Transaction.kt | 2 +- .../main/java/com/tangem/utils}/Strings.kt | 2 +- .../tangem/utils/transformer/Transformer.kt | 5 +++ features/send/impl/build.gradle.kts | 2 +- .../ui/amount/AmountFieldContainer.kt | 2 +- .../ui/recipient/SendRecipientContent.kt | 2 +- features/swap/presentation/build.gradle.kts | 1 - .../feature/swap/ui/SwapScreenContent.kt | 2 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 2 +- features/tokendetails/impl/build.gradle.kts | 1 - .../ui/components/TokenDetailsBalanceBlock.kt | 2 +- features/wallet/impl/build.gradle.kts | 1 - .../component/token/TokenCryptoAmount.kt | 2 +- .../common/component/token/TokenFiatAmount.kt | 2 +- .../wallet/state/model/WalletCardState.kt | 2 +- .../wallet/ui/components/common/WalletCard.kt | 2 +- settings.gradle.kts | 4 +++ 23 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 common/ui/.gitignore create mode 100644 common/ui/build.gradle.kts create mode 100644 common/ui/consumer-rules.pro create mode 100644 common/ui/proguard-rules.pro rename {common/src/main/java/com/tangem/common => core/utils/src/main/java/com/tangem/utils}/Strings.kt (70%) create mode 100644 core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt index 1f3b82b267..76daaa5802 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.send.redux.reducers -import com.tangem.common.Strings.STARS import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.features.send.redux.AmountAction @@ -11,6 +10,7 @@ import com.tangem.tap.features.send.redux.states.AmountState import com.tangem.tap.features.send.redux.states.InputViewValue import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.utils.Strings.STARS import java.math.BigDecimal /** diff --git a/common/ui/.gitignore b/common/ui/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/common/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts new file mode 100644 index 0000000000..fc0e428770 --- /dev/null +++ b/common/ui/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.ui" +} + +dependencies { + + /** Compose */ + implementation(deps.compose.material3) + implementation(deps.compose.material) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + + /** Deps */ + implementation(deps.kotlin.immutable.collections) + + /** Project - Common */ + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Project - Domain */ + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency.models) +} \ No newline at end of file diff --git a/common/ui/consumer-rules.pro b/common/ui/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/ui/proguard-rules.pro b/common/ui/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/common/ui/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index e7c5bfc4e7..eb2b8502e1 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -9,9 +9,6 @@ android { } dependencies { - /** Project - Common */ - implementation(projects.common) - /** Project - Domain */ implementation(projects.domain.tokens.models) implementation(projects.domain.appTheme.models) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index e43769432c..3b99a455ce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import com.tangem.common.Strings import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -35,6 +34,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.utils.Strings import java.util.UUID /** diff --git a/common/src/main/java/com/tangem/common/Strings.kt b/core/utils/src/main/java/com/tangem/utils/Strings.kt similarity index 70% rename from common/src/main/java/com/tangem/common/Strings.kt rename to core/utils/src/main/java/com/tangem/utils/Strings.kt index 943b0de614..59adbdab5b 100644 --- a/common/src/main/java/com/tangem/common/Strings.kt +++ b/core/utils/src/main/java/com/tangem/utils/Strings.kt @@ -1,4 +1,4 @@ -package com.tangem.common +package com.tangem.utils object Strings { diff --git a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt new file mode 100644 index 0000000000..e4e35c4299 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt @@ -0,0 +1,5 @@ +package com.tangem.utils.transformer + +interface Transformer { + fun transform(prevState: S, value: V): S +} \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 66e103efad..7d61a5c6a0 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation(projects.core.datasource) /** Common */ - implementation(projects.common) + implementation(projects.common.ui) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt index eb78c8d0a1..fe4e6cc285 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt @@ -12,11 +12,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign -import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.currency.tokenicon.TokenIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.utils.Strings.STARS private const val AMOUNT_FIELD_KEY = "amountFieldKey" diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index b3402a2bbe..b762119b42 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -21,7 +21,6 @@ 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.PreviewParameterProvider -import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -35,6 +34,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.RecipientSta import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub import com.tangem.features.send.impl.presentation.ui.common.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Strings.STARS import kotlinx.collections.immutable.ImmutableList private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 643cf8f201..6335a9d729 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.utils) implementation(projects.core.ui) - implementation(projects.common) /** Domain modules **/ implementation(projects.domain.appCurrency) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 7be20ace87..a0c5919e82 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout -import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig @@ -38,6 +37,7 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R +import com.tangem.utils.Strings.STARS @Suppress("LongMethod") @Composable diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 081b492357..ff4b879c0e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.Strings import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.currency.tokenicon.TokenIcon @@ -30,6 +29,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.R +import com.tangem.utils.Strings import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index bff55fb9ad..dc7674a9a7 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { kapt(deps.hilt.kapt) /** Core modules */ - implementation(projects.common) implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 3f448ae33a..06d871b579 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -11,7 +11,6 @@ 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.common.Strings.STARS import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.res.TangemTheme @@ -21,6 +20,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPre import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.Strings.STARS import kotlinx.collections.immutable.toImmutableList @Composable diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 6f499d7be1..1c2c062dbd 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(projects.common) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt index 1d8ddf1d9d..ff7c37e28b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt @@ -8,10 +8,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R +import com.tangem.utils.Strings import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt index 8e0535314e..6bc1be0dbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt @@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.text.style.TextOverflow -import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme +import com.tangem.utils.Strings import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt index d4feb6a295..d20706f5f8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt @@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import com.tangem.common.Strings import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.Strings /** Wallet card state */ @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 3962d07d47..7cc578f380 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import com.tangem.common.Strings import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText @@ -54,6 +53,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.utils.Strings private const val HALF_OF_ITEM_WIDTH = 0.5 diff --git a/settings.gradle.kts b/settings.gradle.kts index 9f9643c29f..d15c98e5bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -115,6 +115,10 @@ include(":core:deep-links:global") include(":core:decompose") // endregion Core modules +// region Common modules +include(":common:ui") +// endregion + // region Libs modules include(":libs:auth") include(":libs:blockchain-sdk") From d3ca29be3b7f86f2c631e316aeca4fc9bf653aa3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 13:53:53 +0500 Subject: [PATCH 010/223] Updated on 2026-08-14 --- .../amountScreen/AmountScreenClickIntents.kt | 24 ++++ .../ui/amountScreen/AmountScreenContent.kt | 70 +++++++++ .../converters/AmountCurrencyTransformer.kt | 42 ++++++ .../AmountPastedTriggerDismissTransformer.kt | 17 +++ .../converters/AmountReduceByTransformer.kt | 66 +++++++++ .../converters/AmountReduceToTransformer.kt | 55 +++++++ .../converters/AmountStateConverter.kt | 82 +++++++++++ .../field/AmountFieldChangeTransformer.kt | 82 +++++++++++ .../converters/field/AmountFieldConverter.kt | 82 +++++++++++ .../field/AmountFieldMaxAmountTransformer.kt | 53 +++++++ .../ui/amountScreen/models/AmountField.kt | 39 +++++ .../models/AmountSegmentedButtonsConfig.kt | 21 +++ .../ui/amountScreen/models/AmountState.kt | 32 +++++ .../preview/AmountScreenClickIntentsStub.kt | 16 +++ .../preview/AmountStatePreviewData.kt | 87 +++++++++++ .../ui/amountScreen/ui/AmountButtons.kt | 124 ++++++++++++++++ .../common/ui/amountScreen/ui/AmountField.kt | 136 ++++++++++++++++++ .../amountScreen/ui/AmountFieldContainer.kt | 69 +++++++++ .../ui/amountScreen/utils/AmountUtils.kt | 59 ++++++++ 19 files changed, 1156 insertions(+) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt new file mode 100644 index 0000000000..a913361583 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt @@ -0,0 +1,24 @@ +package com.tangem.common.ui.amountScreen + +/** Amount screen clicks */ +interface AmountScreenClickIntents { + + /** On amount [value] changed */ + fun onAmountValueChange(value: String) + + /** Click triggered on value paste */ + fun onAmountPasteTriggerDismiss() + + /** On max amount click */ + fun onMaxValueClick() + + /** + * On currency change from crypto currency to app currency clicked + * + * @param isFiat indicates currency to change + */ + fun onCurrencyChangeClick(isFiat: Boolean) + + /** On next screen click */ + fun onAmountNext() +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt new file mode 100644 index 0000000000..3d228be20b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -0,0 +1,70 @@ +package com.tangem.common.ui.amountScreen + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +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.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.ui.amountField +import com.tangem.common.ui.amountScreen.ui.buttons +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Amount screen with field + * * @param amountState amount state + * @param isBalanceHiding flag hidden balances + * @param clickIntents amount screen clicks + */ +@Composable +fun AmountScreenContent(amountState: AmountState, isBalanceHiding: Boolean, clickIntents: AmountScreenClickIntents) { + // Do not put fillMaxSize() in here + LazyColumn( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + amountField(amountState = amountState, isBalanceHiding = isBalanceHiding) + buttons( + segmentedButtonConfig = amountState.segmentedButtonConfig, + clickIntents = clickIntents, + isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, + selectedButton = amountState.selectedButton, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SendAmountContentPreview( + @PreviewParameter(SendAmountContentPreviewProvider::class) amountState: AmountState, +) { + TangemThemePreview { + AmountScreenContent( + amountState = amountState, + isBalanceHiding = false, + clickIntents = AmountScreenClickIntentsStub, + ) + } +} + +private class SendAmountContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.amountState, + ) +} +// endregion \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt new file mode 100644 index 0000000000..d4264afd5c --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.common.ui.amountScreen.converters + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer + +/** + * Selected currency change from crypto currency to app currency and vice versa + * + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountCurrencyTransformer( + private val cryptoCurrencyStatusProvider: Provider, +) : Transformer { + override fun transform(prevState: AmountState, value: Boolean): AmountState { + val amountTextField = prevState.amountTextField + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + val isDoneActionEnabled = prevState.isPrimaryButtonEnabled + return if (amountTextField.isFiatValue == value && !isValidFiatRate) { + prevState + } else { + return prevState.copy( + amountTextField = amountTextField.copy( + isFiatValue = value, + isValuePasted = true, + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ), + selectedButton = prevState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, + ) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt new file mode 100644 index 0000000000..4946b2d297 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.common.ui.amountScreen.converters + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.utils.transformer.Transformer + +/** + * Dismisses indication on pasted value + */ +class AmountPastedTriggerDismissTransformer : Transformer { + override fun transform(prevState: AmountState, value: Boolean): AmountState { + return prevState.copy( + amountTextField = prevState.amountTextField.copy( + isValuePasted = false, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt new file mode 100644 index 0000000000..a10cc83951 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -0,0 +1,66 @@ +package com.tangem.common.ui.amountScreen.converters + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.checkExceedBalance +import com.tangem.common.ui.amountScreen.utils.getFiatValue +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +/** + * Reduces amount by specific value + * + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountReduceByTransformer( + private val cryptoCurrencyStatusProvider: Provider, +) : Transformer { + override fun transform(prevState: AmountState, value: ReduceByData): AmountState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountTextField = prevState.amountTextField + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val amountValue = prevState.amountTextField.cryptoAmount.value ?: return prevState + + val decimalCryptoValue = amountValue.minus(value.reduceAmountByDiff) + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = false, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) { + decimalFiatValue.isNullOrZero() + } else { + decimalCryptoValue.isNullOrZero() + } + return prevState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + keyboardType = KeyboardType.Number, + ), + ), + ) + } + + data class ReduceByData( + val reduceAmountBy: BigDecimal, + val reduceAmountByDiff: BigDecimal, + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt new file mode 100644 index 0000000000..e49b22924e --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.amountScreen.converters + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.checkExceedBalance +import com.tangem.common.ui.amountScreen.utils.getFiatValue +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +/** + * Reduces amount to specific value + * + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountReduceToTransformer( + private val cryptoCurrencyStatusProvider: Provider, +) : Transformer { + override fun transform(prevState: AmountState, value: BigDecimal): AmountState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountTextField = prevState.amountTextField + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val cryptoValue = value.parseBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = false, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isNullOrZero() + return prevState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = value), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, value), + keyboardType = KeyboardType.Number, + ), + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt new file mode 100644 index 0000000000..31be99af9a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -0,0 +1,82 @@ +package com.tangem.common.ui.amountScreen.converters + +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.persistentListOf + +/** + * Converts initial [String] to [AmountState] + * + * @property clickIntents amount screen clicks + * @property appCurrencyProvider selected app currency provider + * @property userWalletProvider selected user wallet provider + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property iconStateConverter currency icon converter + */ +class AmountStateConverter( + private val clickIntents: AmountScreenClickIntents, + private val appCurrencyProvider: Provider, + private val userWalletProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val iconStateConverter: CryptoCurrencyToIconStateConverter, +) : Converter { + + private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { + AmountFieldConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + ) + } + + override fun convert(value: String): AmountState { + val userWallet = userWalletProvider() + val appCurrency = appCurrencyProvider() + val status = cryptoCurrencyStatusProvider() + val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol) + val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) + val noFeeRate = status.value.fiatRate.isNullOrZero() + + return AmountState( + walletName = userWallet.name, + walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), + tokenIconState = iconStateConverter.convert(status), + amountTextField = amountFieldConverter.convert(value), + isPrimaryButtonEnabled = false, + appCurrencyCode = appCurrency.code, + segmentedButtonConfig = persistentListOf( + AmountSegmentedButtonsConfig( + title = stringReference(status.currency.symbol), + iconState = iconStateConverter.convertCustom( + value = status, + forceGrayscale = noFeeRate, + showCustomTokenBadge = false, + ), + isFiat = false, + ), + AmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconUrl = appCurrency.iconSmallUrl, + isFiat = true, + ), + ), + isSegmentedButtonsEnabled = !noFeeRate, + selectedButton = 0, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt new file mode 100644 index 0000000000..d8c937246f --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -0,0 +1,82 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.checkExceedBalance +import com.tangem.common.ui.amountScreen.utils.getCryptoValue +import com.tangem.common.ui.amountScreen.utils.getFiatValue +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +/** + * Amount value change + * + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountFieldChangeTransformer( + private val cryptoCurrencyStatusProvider: Provider, +) : Transformer { + + override fun transform(prevState: AmountState, value: String): AmountState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountTextField = prevState.amountTextField + + if (value.isEmpty()) return prevState.emptyState() + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val trimmedValue = value.trim() + val cryptoValue = trimmedValue.getCryptoValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = amountTextField.isFiatValue, + decimals = cryptoDecimals, + ) + val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = amountTextField.isFiatValue, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) { + decimalFiatValue.isNullOrZero() + } else { + decimalCryptoValue.isNullOrZero() + } + return prevState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + keyboardType = KeyboardType.Number, + ), + ), + ) + } + + private fun AmountState.emptyState(): AmountState { + return copy( + isPrimaryButtonEnabled = false, + amountTextField = amountTextField.copy( + value = "", + fiatValue = "", + cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), + fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), + isError = false, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt new file mode 100644 index 0000000000..7c8ed55206 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -0,0 +1,82 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.convertToAmount +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal + +/** + * Converts initial [String] to [AmountField] + * + * @property clickIntents amount screen clicks + * @property appCurrencyProvider selected app currency provider + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountFieldConverter( + private val clickIntents: AmountScreenClickIntents, + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, +) : Converter { + + override fun convert(value: String): AmountField { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO + val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val (fiatValue, fiatDecimal) = when { + fiatRate.isNullOrZero() -> "" to null + value.isEmpty() -> "" to BigDecimal.ZERO + else -> { + val fiatDecimal = fiatRate?.multiply(cryptoDecimal) + val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() + fiatValue to fiatDecimal + } + } + val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() + return AmountField( + value = value, + fiatValue = fiatValue, + onValueChange = clickIntents::onAmountValueChange, + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onAmountNext() }, + ), + isFiatValue = false, + cryptoAmount = cryptoAmount, + fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), + isError = false, + error = TextReference.Res(R.string.send_validation_amount_exceeds_balance), + isFiatUnavailable = fiatRate == null, + isValuePasted = false, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + ) + } + + private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( + currencySymbol = appCurrency.symbol, + value = fiatValue, + decimals = FIAT_DECIMALS, + type = AmountType.FiatType(appCurrency.code), + ) + + private companion object { + private const val FIAT_DECIMALS = 2 + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt new file mode 100644 index 0000000000..d18b31c41a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.Provider +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.RoundingMode + +/** + * Selects maximum amount value + * + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountFieldMaxAmountTransformer( + private val cryptoCurrencyStatusProvider: Provider, +) : Transformer { + + override fun transform(prevState: AmountState, value: Unit): AmountState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountTextField = prevState.amountTextField + + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val decimalCryptoValue = cryptoCurrencyStatus.value.amount + val decimalFiatValue = cryptoCurrencyStatus.value.fiatAmount + + if (decimalCryptoValue.isNullOrZero()) return prevState + + val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() + val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() + val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() + return prevState.copy( + isPrimaryButtonEnabled = true, + amountTextField = amountTextField.copy( + isValuePasted = true, + value = cryptoValue, + fiatValue = fiatValue, + isError = false, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt new file mode 100644 index 0000000000..252b68473b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt @@ -0,0 +1,39 @@ +package com.tangem.common.ui.amountScreen.models + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Amount + +/** + * Model for amount field + * + * @param value entered value + * @param onValueChange on value change + * @param keyboardOptions keyboard options + * @param keyboardActions keyboard actions + * @param cryptoAmount value as amount + * @param fiatAmount value in fiat as amount + * @param isFiatValue indicates if app currency or crypto currency is selected + * @param fiatValue value in fiat + * @param isFiatUnavailable indicates if fiat rates are unavailable + * @param isValuePasted indicated if value was pasted + * @param onValuePastedTriggerDismiss on value pasted action + * @param isError indicates is value invalid + * @param error error text + */ +data class AmountField( + val value: String, + val onValueChange: (String) -> Unit, + val keyboardOptions: KeyboardOptions, + val keyboardActions: KeyboardActions, + val cryptoAmount: Amount, + val fiatAmount: Amount, + val isFiatValue: Boolean, + val fiatValue: String, + val isFiatUnavailable: Boolean, + val isValuePasted: Boolean, + val onValuePastedTriggerDismiss: () -> Unit, + val isError: Boolean, + val error: TextReference, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt new file mode 100644 index 0000000000..733ae20d2e --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt @@ -0,0 +1,21 @@ +package com.tangem.common.ui.amountScreen.models + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference + +/** + * Segmented buttons config + * + * @param title button title + * @param iconState currency icon state + * @param iconUrl currency icon url + * @param isFiat is fiat currency + */ +@Immutable +data class AmountSegmentedButtonsConfig( + val title: TextReference, + val iconState: TokenIconState? = null, + val iconUrl: String? = null, + val isFiat: Boolean, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt new file mode 100644 index 0000000000..702a0bc968 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -0,0 +1,32 @@ +package com.tangem.common.ui.amountScreen.models + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.PersistentList + +/** + * Model for amount state + * + * @param isPrimaryButtonEnabled indicates if next state button enabled + * @param walletName user wallet name + * @param walletBalance user crypto currency balance in wallet + * @param tokenIconState crypto currency icon state + * @param segmentedButtonConfig currency switcher config + * @param selectedButton selected currency index + * @param isSegmentedButtonsEnabled indicates if currency switches is enabled + * @param amountTextField amount field state + * @param appCurrencyCode app currency code + */ +@Stable +data class AmountState( + val isPrimaryButtonEnabled: Boolean, + val walletName: String, + val walletBalance: TextReference, + val tokenIconState: TokenIconState, + val segmentedButtonConfig: PersistentList, + val selectedButton: Int, + val isSegmentedButtonsEnabled: Boolean, + val amountTextField: AmountField, + val appCurrencyCode: String, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt new file mode 100644 index 0000000000..9509a15aed --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt @@ -0,0 +1,16 @@ +package com.tangem.common.ui.amountScreen.preview + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents + +object AmountScreenClickIntentsStub : AmountScreenClickIntents { + + override fun onAmountValueChange(value: String) {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onMaxValueClick() {} + + override fun onAmountPasteTriggerDismiss() {} + + override fun onAmountNext() {} +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt new file mode 100644 index 0000000000..db17019b58 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -0,0 +1,87 @@ +package com.tangem.common.ui.amountScreen.preview + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.tokens.model.AmountType +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +object AmountStatePreviewData { + + val amountState = AmountState( + isPrimaryButtonEnabled = false, + walletName = "Family Wallet", + walletBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), + tokenIconState = TokenIconState.Loading, + segmentedButtonConfig = persistentListOf( + AmountSegmentedButtonsConfig( + title = stringReference("USDT"), + iconState = TokenIconState.Locked, + isFiat = false, + ), + AmountSegmentedButtonsConfig( + title = stringReference("USD"), + isFiat = true, + ), + ), + appCurrencyCode = "usd", + amountTextField = AmountField( + value = "", + onValueChange = {}, + keyboardOptions = KeyboardOptions.Default, + keyboardActions = KeyboardActions.Default, + cryptoAmount = Amount( + currencySymbol = "USDT", + value = BigDecimal.ZERO, + decimals = 18, + type = AmountType.CoinType, + ), + fiatAmount = Amount( + currencySymbol = "$", + value = BigDecimal.ZERO, + decimals = 2, + type = AmountType.CoinType, + ), + isFiatValue = false, + fiatValue = "123.123", + isFiatUnavailable = false, + isError = false, + error = TextReference.EMPTY, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + ), + isSegmentedButtonsEnabled = true, + selectedButton = 0, + ) + + val amountWithValueState = amountState.copy( + amountTextField = amountState.amountTextField.copy( + value = "100.00", + cryptoAmount = amountState.amountTextField.cryptoAmount.copy( + value = BigDecimal("100.00"), + ), + fiatValue = "99.98", + fiatAmount = amountState.amountTextField.fiatAmount.copy( + value = BigDecimal("99.98"), + ), + ), + ) + + val amountWithValueFiatState = amountWithValueState.copy( + amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false), + ) + + val amountErrorState = amountWithValueState.copy( + amountTextField = amountWithValueState.amountTextField.copy( + isError = true, + error = stringReference("Insufficient funds for transfer"), + ), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt new file mode 100644 index 0000000000..03bf6b4b69 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -0,0 +1,124 @@ +package com.tangem.common.ui.amountScreen.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.currency.fiaticon.FiatIcon +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.PersistentList + +private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" + +internal fun LazyListScope.buttons( + segmentedButtonConfig: PersistentList, + clickIntents: AmountScreenClickIntents, + isSegmentedButtonsEnabled: Boolean, + selectedButton: Int, +) { + item( + key = AMOUNT_BUTTONS_KEY, + ) { + val hapticFeedback = LocalHapticFeedback.current + Row( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + ) { + if (segmentedButtonConfig.isNotEmpty()) { + SegmentedButtons( + modifier = Modifier + .weight(1f) + .height(TangemTheme.dimens.size40), + config = segmentedButtonConfig, + showIndication = false, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clickIntents.onCurrencyChangeClick(it.isFiat) + }, + initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), + isEnabled = isSegmentedButtonsEnabled, + ) { + SendAmountCurrencyButton( + button = it, + isSegmentedButtonsEnabled = isSegmentedButtonsEnabled, + ) + } + } else { + SpacerWMax() + } + Text( + text = stringResource(R.string.send_max_amount), + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .height(TangemTheme.dimens.size40) + .clip(shape = RoundedCornerShape(TangemTheme.dimens.radius26)) + .background(TangemTheme.colors.button.secondary) + .clickable { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clickIntents.onMaxValueClick() + } + .padding( + vertical = TangemTheme.dimens.spacing10, + horizontal = TangemTheme.dimens.spacing34, + ), + ) + } + } +} + +@Composable +private fun SendAmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { + Row( + modifier = Modifier + .fillMaxSize() + .padding( + horizontal = TangemTheme.dimens.spacing10, + ), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + val iconModifier = Modifier + .size(TangemTheme.dimens.size18) + .padding(horizontal = TangemTheme.dimens.spacing1) + if (button.isFiat) { + FiatIcon( + url = button.iconUrl, + size = TangemTheme.dimens.size18, + isGrayscale = !isSegmentedButtonsEnabled, + modifier = iconModifier, + ) + } else if (button.iconState != null) { + TokenIcon( + state = button.iconState, + shouldDisplayNetwork = false, + modifier = iconModifier, + ) + } + Text( + text = button.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.button, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing8, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt new file mode 100644 index 0000000000..0d67797f50 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -0,0 +1,136 @@ +package com.tangem.common.ui.amountScreen.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeightIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.BottomCenter +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.core.ui.components.fields.AmountTextField +import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.rememberDecimalFormat +import kotlinx.coroutines.delay + +@Composable +internal fun AmountField(sendField: AmountField, appCurrencyCode: String) { + val decimalFormat = rememberDecimalFormat() + val isFiatValue = sendField.isFiatValue + val currencyCode = if (isFiatValue) appCurrencyCode else null + val (primaryAmount, primaryValue) = if (isFiatValue) { + sendField.fiatAmount to sendField.fiatValue + } else { + sendField.cryptoAmount to sendField.value + } + val requester = remember { FocusRequester() } + + AmountTextField( + value = primaryValue, + decimals = primaryAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, + currencyCode = currencyCode, + decimalFormat = decimalFormat, + ), + onValueChange = sendField.onValueChange, + keyboardOptions = sendField.keyboardOptions, + keyboardActions = sendField.keyboardActions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + isAutoResize = true, + isValuePasted = sendField.isValuePasted, + onValuePastedTriggerDismiss = sendField.onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .requiredHeightIn(min = TangemTheme.dimens.size32), + ) + + LaunchedEffect(key1 = Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } + + AmountSecondary(sendField, appCurrencyCode) +} + +@Composable +private fun AmountSecondary(sendField: AmountField, appCurrencyCode: String) { + val secondaryAmount = if (sendField.isFiatValue) sendField.cryptoAmount else sendField.fiatAmount + Box( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing8, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + ) { + val text = if (sendField.isFiatValue) { + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = secondaryAmount.value, + cryptoCurrency = secondaryAmount.currencySymbol, + decimals = secondaryAmount.decimals, + ) + } else { + BigDecimalFormatter.formatFiatAmount( + fiatAmount = secondaryAmount.value, + fiatCurrencySymbol = secondaryAmount.currencySymbol, + fiatCurrencyCode = appCurrencyCode, + ) + } + Text( + text = text, + style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .align(BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing32), + ) + AmountFieldError( + isError = sendField.isError, + error = sendField.error, + modifier = Modifier + .align(BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing12), + ) + } +} + +@Composable +private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = isError, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier, + ) { + Text( + text = error.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.Center, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt new file mode 100644 index 0000000000..add0475c98 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -0,0 +1,69 @@ +package com.tangem.common.ui.amountScreen.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.utils.Strings.STARS + +private const val AMOUNT_FIELD_KEY = "amountFieldKey" + +internal fun LazyListScope.amountField( + amountState: AmountState, + isBalanceHiding: Boolean, + modifier: Modifier = Modifier, +) { + item(key = AMOUNT_FIELD_KEY) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.background.action), + ) { + Text( + text = amountState.walletName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing14), + ) + + val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference() + AnimatedContent( + targetState = balance, + label = "Hide Balance Animation", + ) { + Text( + text = it, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2), + ) + } + TokenIcon( + state = amountState.tokenIconState, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32), + ) + AmountField( + sendField = amountState.amountTextField, + appCurrencyCode = amountState.appCurrencyCode, + ) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt new file mode 100644 index 0000000000..2ef758b1a7 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -0,0 +1,59 @@ +package com.tangem.common.ui.amountScreen.utils + +import androidx.compose.ui.text.input.ImeAction +import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.isNullOrZero +import java.math.BigDecimal +import java.math.RoundingMode + +internal fun String.getCryptoValue(fiatRate: BigDecimal?, isFiatValue: Boolean, decimals: Int): String { + return if (isFiatValue && fiatRate != null) { + parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN) + .parseBigDecimal(decimals) + } else { + this + } +} + +internal fun String.getFiatValue( + fiatRate: BigDecimal?, + isFiatValue: Boolean, + decimals: Int, +): Pair { + return if (fiatRate != null) { + val fiatValue = if (!isFiatValue) { + parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals) + } else { + this + } + val decimalFiatValue = fiatValue.parseToBigDecimal(decimals) + fiatValue to decimalFiatValue + } else { + "" to null + } +} + +internal fun String.checkExceedBalance( + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountTextField: AmountField, +): Boolean { + val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) + val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) + return if (amountTextField.isFiatValue) { + fiatDecimal > currencyFiatAmount + } else { + cryptoDecimal > currencyCryptoAmount + } +} + +internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = + if (!isExceedBalance && !decimalCryptoValue.isNullOrZero()) { + ImeAction.Done + } else { + ImeAction.None + } \ No newline at end of file From 77654b781458ae783877e343c26fc9fe8e2a9746 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 13:55:44 +0500 Subject: [PATCH 011/223] Updated on 2026-08-14 --- common/ui/consumer-rules.pro | 0 common/ui/proguard-rules.pro | 21 --------------------- 2 files changed, 21 deletions(-) delete mode 100644 common/ui/consumer-rules.pro delete mode 100644 common/ui/proguard-rules.pro diff --git a/common/ui/consumer-rules.pro b/common/ui/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/ui/proguard-rules.pro b/common/ui/proguard-rules.pro deleted file mode 100644 index 481bb43481..0000000000 --- a/common/ui/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file From cd5904764e23aa8f65450f5510e25f60ebbde749 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 6 Jun 2024 21:02:59 +0500 Subject: [PATCH 012/223] Updated on 2026-08-14 --- .../ui/amountScreen/AmountScreenContent.kt | 2 +- .../converters/field/AmountFieldConverter.kt | 8 ++--- .../{AmountField.kt => AmountFieldModel.kt} | 2 +- .../ui/amountScreen/models/AmountState.kt | 2 +- .../preview/AmountStatePreviewData.kt | 4 +-- .../ui/amountScreen/ui/AmountButtons.kt | 4 +-- .../common/ui/amountScreen/ui/AmountField.kt | 32 +++++++++---------- .../amountScreen/ui/AmountFieldContainer.kt | 2 +- .../ui/amountScreen/utils/AmountUtils.kt | 4 +-- 9 files changed, 30 insertions(+), 30 deletions(-) rename common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/{AmountField.kt => AmountFieldModel.kt} (97%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index 3d228be20b..0285b03b87 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.res.TangemThemePreview /** * Amount screen with field - * * @param amountState amount state + * @param amountState amount state * @param isBalanceHiding flag hidden balances * @param clickIntents amount screen clicks */ diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index 7c8ed55206..bfe5259127 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents -import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -30,9 +30,9 @@ class AmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, -) : Converter { +) : Converter { - override fun convert(value: String): AmountField { + override fun convert(value: String): AmountFieldModel { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) @@ -47,7 +47,7 @@ class AmountFieldConverter( } } val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() - return AmountField( + return AmountFieldModel( value = value, fiatValue = fiatValue, onValueChange = clickIntents::onAmountValueChange, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt index 252b68473b..572239504d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt @@ -22,7 +22,7 @@ import com.tangem.domain.tokens.model.Amount * @param isError indicates is value invalid * @param error error text */ -data class AmountField( +data class AmountFieldModel( val value: String, val onValueChange: (String) -> Unit, val keyboardOptions: KeyboardOptions, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index 702a0bc968..a420689714 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -27,6 +27,6 @@ data class AmountState( val segmentedButtonConfig: PersistentList, val selectedButton: Int, val isSegmentedButtonsEnabled: Boolean, - val amountTextField: AmountField, + val amountTextField: AmountFieldModel, val appCurrencyCode: String, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index db17019b58..fa1ce561e4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -2,7 +2,7 @@ package com.tangem.common.ui.amountScreen.preview import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.tokenicon.TokenIconState @@ -32,7 +32,7 @@ object AmountStatePreviewData { ), ), appCurrencyCode = "usd", - amountTextField = AmountField( + amountTextField = AmountFieldModel( value = "", onValueChange = {}, keyboardOptions = KeyboardOptions.Default, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index 03bf6b4b69..5b23386ee8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -53,7 +53,7 @@ internal fun LazyListScope.buttons( initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), isEnabled = isSegmentedButtonsEnabled, ) { - SendAmountCurrencyButton( + AmountCurrencyButton( button = it, isSegmentedButtonsEnabled = isSegmentedButtonsEnabled, ) @@ -84,7 +84,7 @@ internal fun LazyListScope.buttons( } @Composable -private fun SendAmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { +private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { Row( modifier = Modifier .fillMaxSize() diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 0d67797f50..1144bf4bcd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection -import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference @@ -27,14 +27,14 @@ import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay @Composable -internal fun AmountField(sendField: AmountField, appCurrencyCode: String) { +internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) { val decimalFormat = rememberDecimalFormat() - val isFiatValue = sendField.isFiatValue + val isFiatValue = amountField.isFiatValue val currencyCode = if (isFiatValue) appCurrencyCode else null val (primaryAmount, primaryValue) = if (isFiatValue) { - sendField.fiatAmount to sendField.fiatValue + amountField.fiatAmount to amountField.fiatValue } else { - sendField.cryptoAmount to sendField.value + amountField.cryptoAmount to amountField.value } val requester = remember { FocusRequester() } @@ -47,16 +47,16 @@ internal fun AmountField(sendField: AmountField, appCurrencyCode: String) { currencyCode = currencyCode, decimalFormat = decimalFormat, ), - onValueChange = sendField.onValueChange, - keyboardOptions = sendField.keyboardOptions, - keyboardActions = sendField.keyboardActions, + onValueChange = amountField.onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, textStyle = TangemTheme.typography.h2.copy( color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), isAutoResize = true, - isValuePasted = sendField.isValuePasted, - onValuePastedTriggerDismiss = sendField.onValuePastedTriggerDismiss, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, modifier = Modifier .focusRequester(requester) .padding( @@ -72,12 +72,12 @@ internal fun AmountField(sendField: AmountField, appCurrencyCode: String) { requester.requestFocus() } - AmountSecondary(sendField, appCurrencyCode) + AmountSecondary(amountField, appCurrencyCode) } @Composable -private fun AmountSecondary(sendField: AmountField, appCurrencyCode: String) { - val secondaryAmount = if (sendField.isFiatValue) sendField.cryptoAmount else sendField.fiatAmount +private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) { + val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount Box( modifier = Modifier .padding( @@ -86,7 +86,7 @@ private fun AmountSecondary(sendField: AmountField, appCurrencyCode: String) { end = TangemTheme.dimens.spacing12, ), ) { - val text = if (sendField.isFiatValue) { + val text = if (amountField.isFiatValue) { BigDecimalFormatter.formatCryptoAmount( cryptoAmount = secondaryAmount.value, cryptoCurrency = secondaryAmount.currencySymbol, @@ -109,8 +109,8 @@ private fun AmountSecondary(sendField: AmountField, appCurrencyCode: String) { .padding(bottom = TangemTheme.dimens.spacing32), ) AmountFieldError( - isError = sendField.isError, - error = sendField.error, + isError = amountField.isError, + error = amountField.error, modifier = Modifier .align(BottomCenter) .padding(bottom = TangemTheme.dimens.spacing12), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index add0475c98..28d9782293 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -61,7 +61,7 @@ internal fun LazyListScope.amountField( .padding(top = TangemTheme.dimens.spacing32), ) AmountField( - sendField = amountState.amountTextField, + amountField = amountState.amountTextField, appCurrencyCode = amountState.appCurrencyCode, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt index 2ef758b1a7..26492fa913 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -1,7 +1,7 @@ package com.tangem.common.ui.amountScreen.utils import androidx.compose.ui.text.input.ImeAction -import com.tangem.common.ui.amountScreen.models.AmountField +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -38,7 +38,7 @@ internal fun String.getFiatValue( internal fun String.checkExceedBalance( cryptoCurrencyStatus: CryptoCurrencyStatus, - amountTextField: AmountField, + amountTextField: AmountFieldModel, ): Boolean { val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO From 9cc6f26cf4259e334ea40ff7cbcce4af43737644 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 10 Jun 2024 18:03:44 +0300 Subject: [PATCH 013/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 5 + .../tap/common/extensions/Navigation.kt | 1 + .../tap/proxy/redux/DaggerGraphAction.kt | 2 + .../tap/proxy/redux/DaggerGraphReducer.kt | 1 + .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../tangem/core/navigation/NavigationState.kt | 1 + .../model/ScenarioUnavailabilityReason.kt | 2 +- .../staking/api/navigation/StakingRouter.kt | 5 + features/staking/impl/build.gradle.kts | 9 ++ .../impl/navigation/DefaultStakingRouter.kt | 3 +- .../impl/navigation/InnerStakingRouter.kt | 5 +- .../impl/presentation/StakingFragment.kt | 73 +++++++++++++ .../presentation/state/StakingAlertState.kt | 24 +++++ .../impl/presentation/state/StakingEvent.kt | 12 +++ .../state/StakingStateController.kt | 47 ++++++++ .../presentation/state/StakingStateRouter.kt | 72 +++++++++++++ .../impl/presentation/state/StakingUiState.kt | 101 ++++++++++++++++++ .../HideBalanceStateTransformer.kt | 12 +++ .../StakingScreenStateTransformer.kt | 8 ++ .../viewmodel/StakingClickIntents.kt | 5 + .../viewmodel/StakingViewModel.kt | 84 +++++++++++++++ 21 files changed, 471 insertions(+), 3 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 982967b3ea..a267125f20 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -49,6 +49,7 @@ import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -158,6 +159,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var emailSender: EmailSender + @Inject + lateinit var stakingRouter: StakingRouter + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -242,6 +246,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac sendRouter = sendRouter, qrScanningRouter = qrScanningRouter, emailSender = emailSender, + stakingRouter = stakingRouter, ), ) } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 4b0a740760..70631857fc 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -167,5 +167,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment() AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment() + AppScreen.Staking -> store.inject(getDependency = DaggerGraphState::stakingRouter).getEntryFragment() } } \ No newline at end of file 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 f854ba0dae..3b479b590c 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 @@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -25,5 +26,6 @@ sealed interface DaggerGraphAction : Action { val sendRouter: SendRouter, val qrScanningRouter: QrScanningRouter, val emailSender: EmailSender, + val stakingRouter: StakingRouter, ) : 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 b25fdce12b..9f1f5fddca 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 @@ -23,6 +23,7 @@ object DaggerGraphReducer { sendRouter = action.sendRouter, qrScanningRouter = action.qrScanningRouter, emailSender = action.emailSender, + stakingRouter = action.stakingRouter, ) } } 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 a61bdcde57..3dc61535c0 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 @@ -30,6 +30,7 @@ import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggle import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -79,4 +80,5 @@ data class DaggerGraphState( val assetLoader: AssetLoader? = null, val detailsFeatureToggles: DetailsFeatureToggles? = null, val detailsEntryPoint: DetailsEntryPoint? = null, + val stakingRouter: StakingRouter? = null, ) : StateType \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt index a0a6d64f25..40d6612fed 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt @@ -35,4 +35,5 @@ enum class AppScreen(val isDialogFragment: Boolean = false) { SaveWallet(isDialogFragment = true), AppCurrencySelector, ModalNotification(isDialogFragment = true), + Staking, } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index ead2d06c56..340ca7d60d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -27,6 +27,6 @@ sealed class ScenarioUnavailabilityReason { data object UnassociatedAsset : ScenarioUnavailabilityReason() enum class WithdrawalScenario { - SELL, SEND + SELL, SEND // TODO staking create&process STAKING } } \ No newline at end of file diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt index d2b216513e..1749258dfd 100644 --- a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt @@ -5,4 +5,9 @@ import androidx.fragment.app.Fragment interface StakingRouter { fun getEntryFragment(): Fragment + + companion object { + const val CRYPTO_CURRENCY_KEY = "CRYPTO_CURRENCY_KEY" + const val USER_WALLET_ID_KEY = "USER_WALLET_ID_KEY" + } } \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index ac2ce74301..5640ea3503 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -47,6 +47,15 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) + + /** Domain */ + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + /** Common */ implementation(projects.common) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 83cd830e4d..351a4a7a28 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.features.staking.impl.presentation.StakingFragment internal class DefaultStakingRouter : InnerStakingRouter { - override fun getEntryFragment(): Fragment = TODO() + override fun getEntryFragment(): Fragment = StakingFragment.create() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index 7d7aa226d4..dd7187d902 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -2,4 +2,7 @@ package com.tangem.features.staking.impl.navigation import com.tangem.features.staking.api.navigation.StakingRouter -interface InnerStakingRouter : StakingRouter \ No newline at end of file +interface InnerStakingRouter : StakingRouter { + + // TODO staking +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt new file mode 100644 index 0000000000..fab926ce46 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -0,0 +1,73 @@ +package com.tangem.features.staking.impl.presentation + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel +import dagger.hilt.android.AndroidEntryPoint +import java.lang.ref.WeakReference +import javax.inject.Inject + +/** + * Staking fragment + */ +@AndroidEntryPoint +internal class StakingFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + @Inject + lateinit var router: StakingRouter + + @Inject + lateinit var analyticsEventsHandler: AnalyticsEventHandler + + private val viewModel by viewModels() + private val innerStakingRouter: InnerStakingRouter + get() = requireNotNull(router as? InnerStakingRouter) { + "innerStakingRouter should be instance of InnerStakingRouter" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + + viewModel.setRouter( + innerStakingRouter, + StakingStateRouter( + fragmentManager = WeakReference(parentFragmentManager), + ), + ) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.tertiary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + val currentState = viewModel.stakingStateRouter.currentState.collectAsStateWithLifecycle() + // StakingScreen(viewModel.uiState, currentState.value) + } + + override fun onDestroy() { + lifecycle.removeObserver(viewModel) + super.onDestroy() + } + + companion object { + /** Create staking fragment instance */ + fun create(): StakingFragment = StakingFragment() + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt new file mode 100644 index 0000000000..c133fe00ca --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.staking.impl.R + +@Immutable +internal sealed class StakingAlertState { + + abstract val title: TextReference? + abstract val message: TextReference + open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + open val onConfirmClick: (() -> Unit)? = null + + data class GenericError( + override val title: TextReference? = TODO(), + override val onConfirmClick: () -> Unit, + ) : StakingAlertState() { + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_support) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt new file mode 100644 index 0000000000..dbc800ea01 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class StakingEvent { + + data class ShowSnackBar(val text: TextReference) : StakingEvent() + + data class ShowAlert(val alert: StakingAlertState) : StakingEvent() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt new file mode 100644 index 0000000000..1d0095389b --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -0,0 +1,47 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.features.staking.impl.presentation.state.transformers.StakingScreenStateTransformer +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class StakingStateController @Inject constructor() { + + val value: StakingUiState get() = uiState.value + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow get() = mutableUiState.asStateFlow() + + fun update(function: (StakingUiState) -> StakingUiState) { + mutableUiState.update(function = function) + } + + fun update(transformer: StakingScreenStateTransformer) { + mutableUiState.update(function = transformer::transform) + } + + fun clear() { + mutableUiState.update { getInitialState() } + } + + private fun getInitialState(): StakingUiState { + return StakingUiState( + clickIntents = object : StakingClickIntents {}, + cryptoCurrencyName = "", + currentScreen = StakingUiStateType.InitialInfo, + initialInfoState = StakingStates.InitialInfoState.Empty(), + amountState = StakingStates.AmountState.Empty(), + validatorState = StakingStates.ValidatorState.Empty(), + confirmStakingState = StakingStates.ConfirmStakingState.Empty(), + isBalanceHidden = false, + event = consumedEvent(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt new file mode 100644 index 0000000000..8e7182f088 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -0,0 +1,72 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.lang.ref.WeakReference + +internal class StakingStateRouter( + private val fragmentManager: WeakReference, +) { + private var mutableCurrentState: MutableStateFlow = MutableStateFlow(getInitState()) + + val currentState: StateFlow + get() = mutableCurrentState.asStateFlow() + + fun clear() { + mutableCurrentState.update { getInitState() } + } + + fun popBackStack() { + fragmentManager.get()?.popBackStack() + } + + fun onBackClick(isSuccess: Boolean = false) { + val type = currentState.value + when { + isSuccess -> popBackStack() + else -> when (type) { + StakingUiStateType.Amount -> showInitial() + StakingUiStateType.ValidatorAndFee -> showAmount() + else -> popBackStack() + } + } + } + + fun onNextClick() { + when (currentState.value) { + StakingUiStateType.InitialInfo -> showAmount() + StakingUiStateType.Amount, + StakingUiStateType.ValidatorAndFee, + -> showConfirm() + StakingUiStateType.Confirm -> onBackClick() + } + } + + fun onPrevClick() { + when (currentState.value) { + StakingUiStateType.Amount -> showInitial() + else -> popBackStack() + } + } + + private fun showInitial() { + mutableCurrentState.update { StakingUiStateType.InitialInfo } + } + + fun showAmount() { + mutableCurrentState.update { StakingUiStateType.Amount } + } + + fun showValidator() { + mutableCurrentState.update { StakingUiStateType.ValidatorAndFee } + } + + fun showConfirm() { + mutableCurrentState.update { StakingUiStateType.Confirm } + } + + private fun getInitState() = StakingUiStateType.InitialInfo +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt new file mode 100644 index 0000000000..8355cab544 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -0,0 +1,101 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents + +/** + * Ui states of the staking screen + */ +@Immutable +internal data class StakingUiState( + val clickIntents: StakingClickIntents, + val cryptoCurrencyName: String, + val currentScreen: StakingUiStateType, + val initialInfoState: StakingStates.InitialInfoState, + val amountState: StakingStates.AmountState, + val validatorState: StakingStates.ValidatorState, + val confirmStakingState: StakingStates.ConfirmStakingState, + val isBalanceHidden: Boolean, + val event: StateEvent, +) { + + fun copyWrapped( + initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, + amountState: StakingStates.AmountState = this.amountState, + validatorState: StakingStates.ValidatorState = this.validatorState, + confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState, + ): StakingUiState = copy( + initialInfoState = initialInfoState, + amountState = amountState, + validatorState = validatorState, + confirmStakingState = confirmStakingState, + ) +} + +internal sealed class StakingStates { + + abstract val isPrimaryButtonEnabled: Boolean + + /** Initial info state */ + sealed class InitialInfoState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + ) : InitialInfoState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : InitialInfoState() + } + + /** Amount state */ + sealed class AmountState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + ) : AmountState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : AmountState() + } + + /** Validator state */ + sealed class ValidatorState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + ) : ValidatorState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : ValidatorState() + } + + /** Fee state */ + sealed class FeeState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + ) : FeeState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : FeeState() + } + + /** Confirm state */ + sealed class ConfirmStakingState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + ) : ConfirmStakingState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : ConfirmStakingState() + } +} + +enum class StakingUiStateType { + InitialInfo, + Amount, + ValidatorAndFee, + Confirm, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt new file mode 100644 index 0000000000..90afd05bb9 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingUiState + +internal class HideBalanceStateTransformer( + private val isBalanceHidden: Boolean, +) : StakingScreenStateTransformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy(isBalanceHidden = isBalanceHidden) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt new file mode 100644 index 0000000000..05d13135f0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingUiState + +internal interface StakingScreenStateTransformer { + + fun transform(prevState: StakingUiState): StakingUiState +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt new file mode 100644 index 0000000000..1aec6214cd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -0,0 +1,5 @@ +package com.tangem.features.staking.impl.presentation.viewmodel + +internal interface StakingClickIntents { + // TODO staking +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt new file mode 100644 index 0000000000..91cb3a2423 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -0,0 +1,84 @@ +package com.tangem.features.staking.impl.presentation.viewmodel + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.properties.Delegates + +@HiltViewModel +internal class StakingViewModel @Inject constructor( + private val stateController: StakingStateController, + private val dispatchers: CoroutineDispatcherProvider, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { + + val uiState: StateFlow = stateController.uiState + val value: StakingUiState get() = uiState.value + + var stakingStateRouter: StakingStateRouter by Delegates.notNull() + private set + + private val cryptoCurrency: CryptoCurrency = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_KEY] + ?: error("This screen can't open without `CryptoCurrency`") + + private val userWalletId: UserWalletId = savedStateHandle.get(StakingRouter.USER_WALLET_ID_KEY) + ?.let { stringValue -> UserWalletId(stringValue) } + ?: error("This screen can't open without `UserWalletId`") + + private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + + private var innerRouter: InnerStakingRouter by Delegates.notNull() + + init { + subscribeOnBalanceHiding() + subscribeOnCurrencyStatusUpdates() + } + + fun setRouter(router: InnerStakingRouter, stakingStateRouter: StakingStateRouter) { + innerRouter = router + this.stakingStateRouter = stakingStateRouter + } + + private fun subscribeOnCurrencyStatusUpdates() { + viewModelScope.launch { + getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrency.id).fold( + ifRight = { + cryptoCurrencyStatus = it + }, + ifLeft = { + // TODO staking error + }, + ) + } + } + + private fun subscribeOnBalanceHiding() { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + stateController.update(transformer = HideBalanceStateTransformer(it.isBalanceHidden)) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } +} \ No newline at end of file From 4d3791a56b16bc86c72fca1a5c90ac4f7b1ff52b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Mar 2024 19:55:54 +0500 Subject: [PATCH 014/223] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 7 ++ .../pushes/TangemPushNotificationService.kt | 103 ++++++++++++++++++ .../com/tangem/domain/common/LogConfig.kt | 2 +- gradle/dependencies.toml | 1 + 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 417f9ea050..72424b337b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -171,6 +171,7 @@ dependencies { implementation(platform(deps.firebase.bom)) implementation(deps.firebase.analytics) implementation(deps.firebase.crashlytics) + implementation(deps.firebase.messaging) /** Tangem libraries */ implementation(deps.tangem.blockchain) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cd7e794eb..be57eab1df 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -144,5 +144,12 @@ android:resource="@xml/provider_paths" /> + + + + + diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt new file mode 100644 index 0000000000..4a207104ca --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -0,0 +1,103 @@ +package com.tangem.tap.common.pushes + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import coil.executeBlocking +import coil.request.ImageRequest +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import com.tangem.domain.common.LogConfig +import com.tangem.tap.MainActivity +import com.tangem.tap.common.images.createCoilImageLoader +import com.tangem.wallet.R +import timber.log.Timber + +@SuppressLint("MissingFirebaseInstanceTokenRefresh") +internal class TangemPushNotificationService : FirebaseMessagingService() { + + override fun onNewToken(token: String) { + super.onNewToken(token) + Timber.d("New FCM token received: $token") + } + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + + val notification = message.notification ?: return + val channelId = notification.channelId ?: TANGEM_CHANNEL_ID + + // TODO refactoring: [REDACTED_JIRA] + val intent = Intent(applicationContext, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + val pendingIntent = PendingIntent.getActivity( + /* context = */ this, + /* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE, + /* intent = */ intent, + /* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + + val notificationBuilder = + NotificationCompat.Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.ic_tangem_24) + .setContentTitle(notification.title) + .setContentText(notification.body) + .setPriority(message.priority) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setVibrate(notification.vibrateTimings) + .apply { + notification.imageUrl?.let { uri -> + val bitmap = getBitmapImageFromUrl(uri) + setStyle( + NotificationCompat + .BigPictureStyle() + .bigPicture(bitmap), + ).setLargeIcon(bitmap) + } + } + + val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannel = NotificationChannel( + channelId, + ContextCompat.getString(applicationContext, R.string.tangem_app_name), + NotificationManager.IMPORTANCE_HIGH, + ) + notificationManager.createNotificationChannel(notificationChannel) + } + + // Generating unique notification id + val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt() + + notificationManager.notify( + /* id = */ uniqueId, + /* notification = */ notificationBuilder.build(), + ) + } + + private fun getBitmapImageFromUrl(url: Uri): Bitmap? { + return createCoilImageLoader( + applicationContext, + logEnabled = LogConfig.imageLoader, + ).executeBlocking( + ImageRequest.Builder(applicationContext) + .data(url) + .build(), + ).drawable?.toBitmap() + } + + private companion object { + const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications + const val PUSH_NOTIFICATION_REQUEST_CODE = 123 + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 9373ae2a5b..9dbf3c435b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -19,6 +19,6 @@ object NetworkLogConfig { } object AnalyticsHandlersLogConfig { - const val firebase: Boolean = false + val firebase: Boolean = BuildConfig.LOG_ENABLED val amplitude: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 3992a40b54..c4f0245a43 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -176,6 +176,7 @@ compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx" } # endregion Firebase # region Tangem From e67a77e97314a0c4cea563c5bb593d06297cc042 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 14:50:25 +0500 Subject: [PATCH 015/223] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 50 ++++++++++++++ .../local/preferences/PreferencesKeys.kt | 14 ++++ .../settings/DefaultPermissionRepository.kt | 68 +++++++++++++++++++ .../data/settings/di/SettingsDataModule.kt | 12 +++- .../settings/DelayPermissionRequestUseCase.kt | 13 ++++ .../IsFirstTimeAskingPermissionUseCase.kt | 11 +++ .../settings/NeverRequestPermissionUseCase.kt | 13 ++++ .../SetFirstTimeAskingPermissionUseCase.kt | 11 +++ .../settings/ShouldAskPermissionUseCase.kt | 10 +++ .../ShouldInitiallyAskPermissionUseCase.kt | 11 +++ .../repositories/PermissionRepository.kt | 39 +++++++++++ 11 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt 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 index bac2d55655..90cc3380f0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -7,6 +7,7 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository +import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.tap.domain.sdk.TangemSdkManager @@ -17,6 +18,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object SettingsDomainModule { @@ -164,4 +166,52 @@ internal object SettingsDomainModule { ): IncrementAppLaunchCounterUseCase { return IncrementAppLaunchCounterUseCase(settingsRepository = settingsRepository) } + + // region PushPermissionRepository + @Provides + @Singleton + fun provideShouldInitiallyAskPermissionUseCase( + permissionRepository: PermissionRepository, + ): ShouldInitiallyAskPermissionUseCase { + return ShouldInitiallyAskPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideIsFirstTimeAskingPermissionUseCase( + permissionRepository: PermissionRepository, + ): IsFirstTimeAskingPermissionUseCase { + return IsFirstTimeAskingPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideSetFirstTimeAskingPushPermissionUseCase( + permissionRepository: PermissionRepository, + ): SetFirstTimeAskingPermissionUseCase { + return SetFirstTimeAskingPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideDelayPermissionRequestUseCase( + permissionRepository: PermissionRepository, + ): DelayPermissionRequestUseCase { + return DelayPermissionRequestUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideShouldAskPermissionUseCase(permissionRepository: PermissionRepository): ShouldAskPermissionUseCase { + return ShouldAskPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideNeverRequestPermissionUseCase( + permissionRepository: PermissionRepository, + ): NeverRequestPermissionUseCase { + return NeverRequestPermissionUseCase(repository = permissionRepository) + } + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index e069422752..4d357add91 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -94,6 +94,20 @@ object PreferencesKeys { val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") + + // region Permission + fun getShouldShowPermission(permission: Int) = booleanPreferencesKey("shouldShowPushPermission_$permission") + + fun getShouldShowInitialPermissionScreen(permission: Int) = + booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") + + fun getIsFirstTimeAskingPermission(permission: Int) = + booleanPreferencesKey("shouldAskInitialPushPermission_$permission") + + fun getPermissionLaunchCount(permission: Int) = intPreferencesKey("pushPermissionLaunchCount_$permission") + + fun getPermissionDaysCount(permission: Int) = longPreferencesKey("pushPermissionDaysCount_$permission") + // endregion } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt new file mode 100644 index 0000000000..c2f07bae1c --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt @@ -0,0 +1,68 @@ +package com.tangem.data.settings + +import android.os.SystemClock +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.PreferencesKeys.getIsFirstTimeAskingPermission +import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionDaysCount +import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionLaunchCount +import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowInitialPermissionScreen +import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowPermission +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.settings.repositories.PermissionRepository + +internal class DefaultPermissionRepository( + private val appPreferencesStore: AppPreferencesStore, +) : PermissionRepository { + + override suspend fun shouldInitiallyShowPermissionScreen(permission: Int): Boolean { + val key = getShouldShowInitialPermissionScreen(permission) + val initialPermissionScreen = appPreferencesStore.getSyncOrDefault(key = key, default = true) + if (initialPermissionScreen) appPreferencesStore.store(key = key, value = false) + return initialPermissionScreen + } + + override suspend fun isFirstTimeAskingPermission(permission: Int): Boolean = appPreferencesStore.getSyncOrDefault( + key = getIsFirstTimeAskingPermission(permission), + default = true, + ) + + override suspend fun setFirstTimeAskingPermission(permission: Int, value: Boolean) { + appPreferencesStore.store( + key = getIsFirstTimeAskingPermission(permission), + value = value, + ) + } + + override suspend fun shouldAskPermission(permission: Int): Boolean { + val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) + val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0) + val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0) + val currentLaunchCounter = appPreferencesStore.getSyncOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) + + val nowMillis = SystemClock.elapsedRealtime() + val isDaysDelayed = delayedDays < nowMillis + val isLaunchesDelayed = delayedLaunches < currentLaunchCounter + return shouldAskPermission && isDaysDelayed && isLaunchesDelayed + } + + override suspend fun neverAskPermission(permission: Int) { + appPreferencesStore.store(key = getShouldShowPermission(permission), value = false) + } + + override suspend fun delayPermissionAsking(permission: Int) { + appPreferencesStore.editData { + val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) + val nowMillis = SystemClock.elapsedRealtime() + + it[getPermissionLaunchCount(permission)] = appLaunchCounter + DELAY_LAUNCH_COUNT + it[getPermissionDaysCount(permission)] = nowMillis + DELAY_DAYS_COUNT + } + } + + private companion object { + const val DELAY_LAUNCH_COUNT = 5 + const val DELAY_DAYS_COUNT = 3L * 24 * 3600 * 1000 // 3 days in millis + } +} \ 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 index 4f6bf8e17b..da47f1f604 100644 --- 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 @@ -1,12 +1,14 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository -import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.settings.DefaultPromoSettingsRepository +import com.tangem.data.settings.DefaultPermissionRepository +import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository +import com.tangem.domain.settings.repositories.PermissionRepository +import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,4 +36,10 @@ internal object SettingsDataModule { fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository { return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore) } + + @Provides + @Singleton + fun providePushPermissionRepository(appPreferencesStore: AppPreferencesStore): PermissionRepository { + return DefaultPermissionRepository(appPreferencesStore = appPreferencesStore) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt new file mode 100644 index 0000000000..7c4c74baba --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class DelayPermissionRequestUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: Int): Either = Either.catch { + repository.delayPermissionAsking(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt new file mode 100644 index 0000000000..f6b9f562d8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { + + suspend operator fun invoke(permission: Int): Either = Either.catch { + repository.isFirstTimeAskingPermission(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt new file mode 100644 index 0000000000..7b9f0219b5 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class NeverRequestPermissionUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: Int): Either = Either.catch { + repository.neverAskPermission(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt new file mode 100644 index 0000000000..6a82495cf4 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { + + suspend operator fun invoke(permission: Int): Either = Either.catch { + repository.setFirstTimeAskingPermission(permission, false) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt new file mode 100644 index 0000000000..2cf3378e84 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.PermissionRepository + +class ShouldAskPermissionUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: Int): Boolean = repository.shouldAskPermission(permission) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt new file mode 100644 index 0000000000..b020fc8c9e --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class ShouldInitiallyAskPermissionUseCase(private val repository: PermissionRepository) { + + suspend operator fun invoke(permission: Int): Either = Either.catch { + repository.shouldInitiallyShowPermissionScreen(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt new file mode 100644 index 0000000000..390868c8d4 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.settings.repositories + +interface PermissionRepository { + + /** + * Return true if should display screen ONCE asking to allow [permission]. + * False otherwise or screen already was displayed + */ + suspend fun shouldInitiallyShowPermissionScreen(permission: Int): Boolean + + /** + * Indicates which time [permission] was asked via platform dialog. + * NOTE: Use this method to indicate either reroute to settings or display platform dialog. + */ + suspend fun isFirstTimeAskingPermission(permission: Int): Boolean + + /** + * Sets value indicating that [permission] was asked via platform dialog. + * NOTE: Use this method to indicate either reroute to settings or display platform dialog. + */ + suspend fun setFirstTimeAskingPermission(permission: Int, value: Boolean) + + /** + * Is clear to ask [permission]. + * User could already granted or permanently denied permission + * Or there is an active delay before next request + */ + suspend fun shouldAskPermission(permission: Int): Boolean + + /** + * Permanently deny [permission] and never request again + */ + suspend fun neverAskPermission(permission: Int) + + /** + * Delay next [permission] request for some time or active sessions + */ + suspend fun delayPermissionAsking(permission: Int) +} \ No newline at end of file From 81362b8ae9e63613a858b2261968af70812cc932 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 16:48:02 +0500 Subject: [PATCH 016/223] Updated on 2026-08-14 --- .../core/ui/components/BoxWithGradient.kt | 41 +++++ .../appbar/AppBarWithAdditionalButtons.kt | 96 +++++++--- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + features/disclaimer/api/.gitignore | 1 + features/disclaimer/api/build.gradle.kts | 15 ++ .../disclaimer/api/DisclaimerRouter.kt | 12 ++ features/disclaimer/impl/.gitignore | 1 + features/disclaimer/impl/build.gradle.kts | 45 +++++ .../disclaimer/impl/DisclaimerFragment.kt | 58 ++++++ .../impl/di/DisclaimerRouterModule.kt | 24 +++ .../navigation/DefaultDisclaimerRouter.kt | 23 +++ .../impl/navigation/InnerDisclaimerRouter.kt | 10 + .../presentation/state/DisclaimerState.kt | 16 ++ .../impl/presentation/ui/DisclaimerScreen.kt | 174 ++++++++++++++++++ .../ui/DisclaimerWebViewClient.kt | 62 +++++++ .../viewmodel/DisclaimerClickIntents.kt | 6 + .../viewmodel/DisclaimerViewModel.kt | 42 +++++ gradle/dependencies.toml | 1 + settings.gradle.kts | 3 + 19 files changed, 608 insertions(+), 23 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt create mode 100644 features/disclaimer/api/.gitignore create mode 100644 features/disclaimer/api/build.gradle.kts create mode 100644 features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt create mode 100644 features/disclaimer/impl/.gitignore create mode 100644 features/disclaimer/impl/build.gradle.kts create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt new file mode 100644 index 0000000000..b51d1c0f08 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun BoxWithGradient( + modifier: Modifier = Modifier, + gradient: Brush = BottomGradient, + content: @Composable BoxScope.() -> Unit, +) { + val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) + + Box(modifier = modifier.fillMaxSize()) { + content() + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(TangemTheme.dimens.size164 + bottomInsetsPx.dp) + .background(gradient), + ) + } +} + +private val BottomGradient: Brush = Brush.verticalGradient( + colors = listOf( + TangemColorPalette.Black.copy(alpha = 0f), + TangemColorPalette.Black.copy(alpha = 0.75f), + TangemColorPalette.Black.copy(alpha = 0.95f), + TangemColorPalette.Black, + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 685358db13..dd23347e63 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -1,23 +1,52 @@ package com.tangem.core.ui.components.appbar import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.AdditionalButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * App bar with title and two additional buttons + * + * @param startButton button information attached to the left edge + * @param endButton button information attached to the right edge + * + * @see + * Figma Component + */ +@Composable +fun AppBarWithAdditionalButtons( + text: TextReference, + modifier: Modifier = Modifier, + startButton: AdditionalButton? = null, + endButton: AdditionalButton? = null, +) { + AppBarWithAdditionalButtons( + text = text.resolveReference(), + startButton = startButton, + endButton = endButton, + modifier = modifier, + ) +} /** * App bar with title and two additional buttons @@ -31,24 +60,33 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun AppBarWithAdditionalButtons( text: String, + modifier: Modifier = Modifier, startButton: AdditionalButton? = null, endButton: AdditionalButton? = null, ) { Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .heightIn(TangemTheme.dimens.size56) .padding(all = TangemTheme.dimens.spacing16), ) { if (startButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) { - Icon( - painter = painterResource(id = startButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = startButton.iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterStart) + .clickable( + onClick = startButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } Text( @@ -60,14 +98,23 @@ fun AppBarWithAdditionalButtons( ) if (endButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) { - Icon( - painter = painterResource(id = endButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = endButton.iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterEnd) + .clickable( + onClick = endButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } } } @@ -87,6 +134,7 @@ private fun Preview_AppBarWithAdditionalButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -102,6 +150,7 @@ private fun Preview_AppBarWithOnlyStartButtons() { iconRes = R.drawable.ic_scan_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -117,6 +166,7 @@ private fun Preview_AppBarWithOnlyEndButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } \ 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 55321fa0b2..863f3e59cc 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 @@ -83,6 +83,7 @@ data class TangemDimens internal constructor( val size96: Dp = 96.dp, val size102: Dp = 102.dp, val size108: Dp = 108.dp, + val size110: Dp = 110.dp, val size116: Dp = 116.dp, val size120: Dp = 120.dp, val size142: Dp = 142.dp, diff --git a/features/disclaimer/api/.gitignore b/features/disclaimer/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts new file mode 100644 index 0000000000..fcd117da1b --- /dev/null +++ b/features/disclaimer/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt new file mode 100644 index 0000000000..5575d38b51 --- /dev/null +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt @@ -0,0 +1,12 @@ +package com.tangem.features.disclaimer.api + +import androidx.fragment.app.Fragment + +interface DisclaimerRouter { + + fun entryFragment(): Fragment + + companion object { + const val IS_TOS_ACCEPTED_KEY = "is_tos_accepted" + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/.gitignore b/features/disclaimer/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts new file mode 100644 index 0000000000..bc658e93f3 --- /dev/null +++ b/features/disclaimer/impl/build.gradle.kts @@ -0,0 +1,45 @@ +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.features.disclaimer.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.appCompat) + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) + implementation(deps.compose.material3) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.card) + + /** Feature modules */ + implementation(projects.features.disclaimer.api) + + /** DI */ + implementation(deps.hilt.android) + implementation(project(":domain:settings")) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt new file mode 100644 index 0000000000..e0a30ce395 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt @@ -0,0 +1,58 @@ +package com.tangem.features.disclaimer.impl + +import android.os.Bundle +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen +import com.tangem.features.disclaimer.impl.presentation.viewmodel.DisclaimerViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class DisclaimerFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + @Inject + lateinit var reduxNavController: ReduxNavController + + private val viewModel by viewModels() + + private val isTosAccepted: Boolean + get() = arguments?.getBoolean(DisclaimerRouter.IS_TOS_ACCEPTED_KEY) ?: false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.primary + BackHandler { + if (isTosAccepted) { + reduxNavController.popBackStack() + } else { + requireActivity().finish() + } + } + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + DisclaimerScreen(viewModel.state, reduxNavController::popBackStack) + } + + companion object { + /** Create disclaimer fragment instance */ + fun create(): DisclaimerFragment = DisclaimerFragment() + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt new file mode 100644 index 0000000000..8b3d34e029 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.core.navigation.ReduxNavController +import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [DisclaimerRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +object DisclaimerRouterModule { + + @Provides + @ActivityScoped + fun provideDisclaimerRouter(reduxNavController: ReduxNavController): DisclaimerRouter { + return DefaultDisclaimerRouter(reduxNavController) + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt new file mode 100644 index 0000000000..4860d3265d --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt @@ -0,0 +1,23 @@ +package com.tangem.features.disclaimer.impl.navigation + +import androidx.fragment.app.Fragment +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.ReduxNavController +import com.tangem.features.disclaimer.impl.DisclaimerFragment +import javax.inject.Inject + +internal class DefaultDisclaimerRouter @Inject constructor( + private val reduxNavController: ReduxNavController, +) : InnerDisclaimerRouter { + + override fun entryFragment(): Fragment = DisclaimerFragment.create() + + override fun openPushNotificationPermission() { + // todo [REDACTED_JIRA] + } + + override fun openHome() { + reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt new file mode 100644 index 0000000000..5077db0901 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt @@ -0,0 +1,10 @@ +package com.tangem.features.disclaimer.impl.navigation + +import com.tangem.features.disclaimer.api.DisclaimerRouter + +internal interface InnerDisclaimerRouter : DisclaimerRouter { + + fun openPushNotificationPermission() + + fun openHome() +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt new file mode 100644 index 0000000000..01d0a398a6 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt @@ -0,0 +1,16 @@ +package com.tangem.features.disclaimer.impl.presentation.state + +internal data class DisclaimerState( + val url: String, + val isTosAccepted: Boolean, + val onAccept: (Boolean) -> Unit, +) + +internal object DummyDisclaimer { + + val state = DisclaimerState( + url = "https://tangem.com/tangem_tos.html", + isTosAccepted = false, + onAccept = {}, + ) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt new file mode 100644 index 0000000000..f99bd7bc4e --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt @@ -0,0 +1,174 @@ +package com.tangem.features.disclaimer.impl.presentation.ui + +import android.Manifest +import android.content.res.Configuration +import android.os.Build +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.disclaimer.impl.R +import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState +import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer + +@Composable +internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { + val bottomPadding = if (state.isTosAccepted) TangemTheme.dimens.spacing16 else TangemTheme.dimens.size64 + Box( + modifier = Modifier + .statusBarsPadding() + .background(TangemColorPalette.Dark6), + ) { + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = bottomPadding), + ) { + AppBarWithAdditionalButtons( + text = resourceReference(R.string.disclaimer_title), + startButton = AdditionalButton( + iconRes = R.drawable.ic_back_24, + onIconClicked = onBackClick, + ).takeIf { state.isTosAccepted }, + ) + DisclaimerContent(state.url) + } + if (!state.isTosAccepted) { + DisclaimerButton(state.onAccept) + } + } +} + +@Composable +private fun ColumnScope.DisclaimerContent(url: String) { + val transparent = Color.Transparent + + val progressState = remember { mutableStateOf(ProgressState.Loading) } + val webClient = remember { DisclaimerWebViewClient(progressState) } + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + AndroidView( + factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(transparent.toArgb()) + settings.allowFileAccess = false + settings.javaScriptEnabled = false + overScrollMode = View.OVER_SCROLL_NEVER + webViewClient = webClient + loadUrl(url) + } + }, + ) + + when (progressState.value) { + ProgressState.Loading -> CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) + ProgressState.Error -> Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing16) + .align(Alignment.Center), + ) { + Text( + text = stringResource(id = R.string.disclaimer_error_loading), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.constantWhite, + ) + PrimaryButton( + text = stringResource(R.string.common_retry), + onClick = { webClient.reset() }, + ) + } + else -> Unit + } + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { + val shouldAskPushPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS).status.isGranted + } else { + true + } + val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .height(TangemTheme.dimens.size110 + bottomInsetsPx.dp) + .background( + Brush.verticalGradient( + colors = listOf( + TangemTheme.colors.background.primary.copy(alpha = 0f), + TangemTheme.colors.background.primary.copy(alpha = 0.75f), + TangemTheme.colors.background.primary.copy(alpha = 0.95f), + TangemTheme.colors.background.primary, + ), + ), + ), + ) + PrimaryButton( + text = stringResource(id = R.string.common_accept), + onClick = { onAccept(shouldAskPushPermission) }, + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DisclaimerScreen_Preview() { + TangemThemePreview { + DisclaimerScreen(state = DummyDisclaimer.state, onBackClick = {}) + } +} +// endregion \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt new file mode 100644 index 0000000000..348cad4107 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt @@ -0,0 +1,62 @@ +package com.tangem.features.disclaimer.impl.presentation.ui + +import android.graphics.Bitmap +import android.webkit.* +import androidx.compose.runtime.MutableState + +internal enum class ProgressState { + Loading, + Done, + Error, +} + +internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { + + private var loadingUrl: String? = null + private var loadedUrl: String? = null + + fun reset() { + loadingUrl = null + loadedUrl = null + progressState.value = ProgressState.Loading + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + + if (loadingUrl != url) progressState.value = ProgressState.Loading + loadingUrl = url + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + + if (loadedUrl != url) progressState.value = ProgressState.Done + loadedUrl = url + } + + override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) { + super.onReceivedError(view, resourceRequest, error) + error?.let { progressState.value = ProgressState.Error } + } + + override fun onReceivedHttpError( + view: WebView?, + resourceRequest: WebResourceRequest?, + errorResponse: WebResourceResponse?, + ) { + super.onReceivedHttpError(view, resourceRequest, errorResponse) + + if (resourceRequest != null && errorResponse != null) { + val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl + val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE + val isNotDone = progressState.value != ProgressState.Done + if (isDifferentUrl || isSuccessCode || isNotDone) return + progressState.value = ProgressState.Error + } + } + + companion object { + private const val RESPONSE_USER_ERROR_STATUS_CODE = 400 + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt new file mode 100644 index 0000000000..d80a1236ed --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt @@ -0,0 +1,6 @@ +package com.tangem.features.disclaimer.impl.presentation.viewmodel + +internal interface DisclaimerClickIntents { + + fun onAccept(shouldAskPushPermission: Boolean) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt new file mode 100644 index 0000000000..bfc97b16d8 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt @@ -0,0 +1,42 @@ +package com.tangem.features.disclaimer.impl.presentation.viewmodel + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.domain.card.repository.CardRepository +import com.tangem.features.disclaimer.api.DisclaimerRouter.Companion.IS_TOS_ACCEPTED_KEY +import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter +import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class DisclaimerViewModel @Inject constructor( + private val cardRepository: CardRepository, + private val disclaimerRouter: DefaultDisclaimerRouter, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, DisclaimerClickIntents { + + private val isTosAccepted: Boolean = savedStateHandle[IS_TOS_ACCEPTED_KEY] ?: false + + val state: DisclaimerState + get() = DisclaimerState( + onAccept = ::onAccept, + url = DISCLAIMER_URL, + isTosAccepted = isTosAccepted, + ) + + override fun onAccept(shouldAskPushPermission: Boolean) { + viewModelScope.launch { + cardRepository.acceptTangemTOS() + // todo [REDACTED_JIRA] + disclaimerRouter.openHome() + } + } + + private companion object { + const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index c4f0245a43..7fb3f2a8f0 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -168,6 +168,7 @@ compose-navigation-hilt = { module = "androidx.hilt:hilt-navigation-compose", ve compose-accompanist-appCompatTheme = { module = "com.google.accompanist:accompanist-appcompat-theme", version.ref = "compose-accompanist" } compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "compose-accompanist" } compose-accompanist-webView = { module = "com.google.accompanist:accompanist-webview", version.ref = "compose-accompanist" } +compose-accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "compose-accompanist" } compose-paging = { module = "androidx.paging:paging-compose", version.ref = "compose-paging" } compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "compose-reorderable" } # endregion Compose diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e8f4f4945..82e7f8bef8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -176,6 +176,9 @@ include(":features:staking:impl") include(":features:details:api") include(":features:details:impl") + +include(":features:disclaimer:api") +include(":features:disclaimer:impl") // endregion Feature modules // region Domain modules From 820379bb788331d89f5858fb7eb6ad5347d39943 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 17:08:24 +0300 Subject: [PATCH 017/223] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 9 + .../tangem/tap/features/main/MainViewModel.kt | 7 +- .../models/response/model/YieldDTO.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 13 ++ .../ui/components/rows/RoundableCornersRow.kt | 147 +++++++++++++ data/staking/build.gradle.kts | 1 - .../data/staking/DefaultStakingRepository.kt | 72 ++++--- .../data/staking/di/StakingDataModule.kt | 3 - domain/staking/build.gradle.kts | 3 + .../tangem/domain/staking/GetYieldUseCase.kt | 16 ++ .../com/tangem/domain/staking/model/Yield.kt | 25 +-- .../staking/repositories/StakingRepository.kt | 15 +- .../referral/presentation/build.gradle.kts | 12 +- .../tangem/feature/referral/ui/AwardItems.kt | 72 ------- .../feature/referral/ui/CornersToRound.kt | 27 --- .../referral/ui/ParticipateBottomBlock.kt | 10 +- .../staking/api/navigation/StakingRouter.kt | 3 +- features/staking/impl/build.gradle.kts | 1 + .../impl/presentation/StakingFragment.kt | 5 +- .../state/StakingStateController.kt | 4 +- .../impl/presentation/state/StakingUiState.kt | 9 + .../state/stub/StakingClickIntentsStub.kt | 8 + .../SetInitialDataStateTransformer.kt | 77 +++++++ .../ui/StakingInitialInfoContent.kt | 200 ++++++++++++++++++ .../impl/presentation/ui/StakingScreen.kt | 125 +++++++++++ .../viewmodel/StakingClickIntents.kt | 3 +- .../viewmodel/StakingViewModel.kt | 29 ++- features/tokendetails/impl/build.gradle.kts | 2 + .../tokendetails/TokenDetailsPreviewData.kt | 1 + .../tokendetails/state/StakingBlockState.kt | 1 + .../state/factory/TokenDetailsStateFactory.kt | 1 + .../factory/TokenStakingStateConverter.kt | 3 + .../ui/components/TokenStakingBlock.kt | 5 +- .../viewmodels/TokenDetailsClickIntents.kt | 2 + .../viewmodels/TokenDetailsViewModel.kt | 31 ++- 36 files changed, 765 insertions(+), 181 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt delete mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt delete mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 532e167dbd..790bb01a7a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.domain.settings.* import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase +import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -31,6 +32,14 @@ internal object StakingDomainModule { ) } + @Provides + @Singleton + fun provideGetYieldUseCase(stakingRepository: StakingRepository): GetYieldUseCase { + return GetYieldUseCase( + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase { diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 07650632f0..c900ea3b50 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.main.model.MainScreenState import com.tangem.tap.store @@ -45,6 +46,7 @@ internal class MainViewModel @Inject constructor( private val sendFeatureToggles: SendFeatureToggles, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, + private val stakingFeatureToggles: StakingFeatureToggles, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -71,7 +73,10 @@ internal class MainViewModel @Inject constructor( observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() - fetchStakingTokens() + + if (stakingFeatureToggles.isStakingEnabled) { + fetchStakingTokens() + } viewModelScope.launch(dispatchers.main) { deleteDeprecatedLogsUseCase() diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index 3e0c439e09..bf8956f11f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -75,7 +75,7 @@ data class YieldDTO( @Json(name = "website") val website: String?, @Json(name = "apr") - val apr: Double?, + val apr: BigDecimal?, @Json(name = "commission") val commission: Double?, @Json(name = "stakedBalance") diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 67531e72a0..afda2fa213 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,5 +1,6 @@ + Выберите сеть Добавить токен Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. @@ -300,6 +301,7 @@ %1$d из %2$d кошельков например Bitcoin + Ваш портфель был обновлен Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать Выберите кошелек diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 130168cb3a..8ef0f52ec8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ + Choose network Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. @@ -64,6 +65,7 @@ Not enough ADA Accept Access denied + Allow Apply Approval Attention @@ -111,6 +113,7 @@ Primary Card Passphrase Paste + %1$s-%2$s Read more Receive Reject @@ -300,10 +303,13 @@ %1$d of %2$d wallets e.g. BTC I trust, hodl I must + Your portfolio has been updated The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote Choose wallet The wallet doesn\'t support more than one network + Market + Sort By You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -536,9 +542,13 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name + APR + APY Available Average Reward Rate %s est. profit + Market rating + Metrics Minimum Requirement No rewards to claim On stake @@ -627,6 +637,9 @@ This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation Use %s or scan a card to have an access to your wallet + Stay up to date with the latest features and news + Be able to get important market notifications + Would you like to use
Push-notifications? Add new wallet Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt new file mode 100644 index 0000000000..e83444c298 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -0,0 +1,147 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.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.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.R + +@Suppress("LongParameterList") +@Composable +fun RoundableCornersRow( + startText: String, + startTextColor: Color, + startTextStyle: TextStyle, + endText: String, + endTextColor: Color, + endTextStyle: TextStyle, + cornersToRound: CornersToRound, + iconResId: Int? = null, +) { + Surface( + shape = cornersToRound.getShape(), + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = startText, + color = startTextColor, + maxLines = 1, + style = startTextStyle, + ) + if (iconResId != null) { + Icon( + modifier = Modifier + .padding(TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = endText, + color = endTextColor, + maxLines = 1, + style = endTextStyle, + ) + } + } +} + +enum class CornersToRound { + + ALL_4, + TOP_2, + BOTTOM_2, + ZERO, + ; + + @Suppress("TopLevelComposableFunctions") + @Composable + fun getShape(): RoundedCornerShape { + val radius = TangemTheme.dimens.radius12 + return when (this) { + ALL_4 -> RoundedCornerShape(radius) + TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) + BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + ZERO -> RoundedCornerShape(0.dp) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_RoundableCornersRow( + @PreviewParameter(RoundableCornersRowDataProvider::class) previewData: RoundableCornersRowPreviewData, +) { + TangemThemePreview { + Box(modifier = Modifier.background(color = TangemTheme.colors.icon.attention)) { + RoundableCornersRow( + startText = previewData.startText, + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = previewData.endText, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = previewData.cornersToRound, + iconResId = previewData.iconResId, + ) + } + } +} + +private data class RoundableCornersRowPreviewData( + val startText: String, + val endText: String, + val cornersToRound: CornersToRound, + val iconResId: Int? = null, +) + +private class RoundableCornersRowDataProvider : + PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + getPreviewData(cornersToRound = CornersToRound.ZERO), + getPreviewData(cornersToRound = CornersToRound.TOP_2), + getPreviewData(cornersToRound = CornersToRound.BOTTOM_2), + getPreviewData(cornersToRound = CornersToRound.ZERO, iconResId = R.drawable.ic_alert_24), + getPreviewData(cornersToRound = CornersToRound.TOP_2, iconResId = R.drawable.ic_alert_24), + getPreviewData(cornersToRound = CornersToRound.BOTTOM_2, iconResId = R.drawable.ic_alert_24), + ) + + private fun getPreviewData(cornersToRound: CornersToRound, iconResId: Int? = null) = RoundableCornersRowPreviewData( + startText = "startText", + endText = "endText", + cornersToRound = cornersToRound, + iconResId = iconResId, + ) +} \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 3c16b7c878..186d53da6e 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation(projects.core.utils) implementation(projects.domain.tokens.models) implementation(projects.domain.staking) - implementation(projects.features.staking.api) // region DI diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index be421d5a75..cc0d2ba7b3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -11,28 +11,62 @@ import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, - private val stakingFeatureToggles: StakingFeatureToggles, private val stakingYieldsStore: StakingYieldsStore, private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { private val yieldConverter = YieldConverter() + override fun isStakingSupported(currencyId: String): Boolean { + return integrationIds.contains(currencyId) + } + + override suspend fun fetchEnabledYields() { + withContext(dispatchers.io) { + val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + + stakingYieldsStore.store(stakingTokensWithYields.data) + } + } + + override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield { + return withContext(dispatchers.io) { + val yields = getEnabledYields() ?: error("No yields found") + val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available") + + val prefetchedYield = findPrefetchedYield( + yields = yields, + currencyId = rawCurrencyId, + symbol = symbol, + ) + + prefetchedYield ?: error("Staking is unavailable") + } + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { + return withContext(dispatchers.io) { + val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() + + StakingEntryInfo( + interestRate = yield.apy, + periodInDays = yield.metadata.cooldownPeriod.days, + tokenSymbol = yield.token.symbol, + ) + } + } + override suspend fun getStakingAvailabilityForActions( cryptoCurrencyId: CryptoCurrency.ID, symbol: String, ): StakingAvailability { val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable - if (!stakingFeatureToggles.isStakingEnabled) { - return StakingAvailability.Unavailable - } return withContext(dispatchers.io) { val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable @@ -56,34 +90,6 @@ internal class DefaultStakingRepository( .find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } - override fun isStakingSupported(currencyId: String): Boolean { - return integrationIds.contains(currencyId) - } - - override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { - return withContext(dispatchers.io) { - val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() - - StakingEntryInfo( - interestRate = yield.apy, - periodInDays = yield.metadata.cooldownPeriod.days, - tokenSymbol = yield.token.symbol, - ) - } - } - - override suspend fun fetchEnabledYields() { - if (!stakingFeatureToggles.isStakingEnabled) { - return - } - - withContext(dispatchers.io) { - val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() - - stakingYieldsStore.store(stakingTokensWithYields.data) - } - } - private suspend fun getEnabledYields(): List? { val yields = stakingYieldsStore.getSyncOrNull() ?: return null return yields.map { yieldConverter.convert(it) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 092e8cfc62..0b47b925d1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -4,7 +4,6 @@ import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -20,13 +19,11 @@ internal object StakingDataModule { @Singleton fun provideStakingRepository( stakeKitApi: StakeKitApi, - stakingFeatureToggles: StakingFeatureToggles, stakingTokenStore: StakingYieldsStore, dispatchers: CoroutineDispatcherProvider, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, - stakingFeatureToggles = stakingFeatureToggles, stakingYieldsStore = stakingTokenStore, dispatchers = dispatchers, ) diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 80ff7080b9..252bfb49d9 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -12,5 +12,8 @@ android { dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + implementation(deps.kotlin.serialization) + + implementation(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt new file mode 100644 index 0000000000..a63dc81cdc --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Use case for getting staking yield for staking scenario start. + */ +class GetYieldUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either { + return Either.catch { stakingRepository.getYield(cryptoCurrencyId, symbol) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt index 3ef85e77dd..853c3d4247 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt @@ -1,5 +1,6 @@ package com.tangem.domain.staking.model +import java.io.Serializable import java.math.BigDecimal data class Yield( @@ -14,24 +15,24 @@ data class Yield( val metadata: Metadata, val validators: List, val isAvailable: Boolean, -) { +) : Serializable { data class Status( val enter: Boolean, val exit: Boolean?, - ) + ) : Serializable data class Args( val enter: Enter, val exit: Enter?, - ) { + ) : Serializable { data class Enter( val addresses: Addresses, val args: Map, - ) { + ) : Serializable { data class Addresses( val address: AddressArgument, val additionalAddresses: Map? = null, - ) + ) : Serializable } } @@ -41,12 +42,12 @@ data class Yield( val name: String, val image: String?, val website: String?, - val apr: Double?, + val apr: BigDecimal?, val commission: Double?, val stakedBalance: String?, val votingPower: Double?, val preferred: Boolean, - ) + ) : Serializable data class Metadata( val name: String, @@ -66,14 +67,14 @@ data class Yield( val supportsMultipleValidators: Boolean, val revshare: Enabled, val fee: Enabled, - ) { + ) : Serializable { data class Period( val days: Int, - ) + ) : Serializable data class Enabled( val enabled: Boolean, - ) + ) : Serializable } enum class RewardType { @@ -92,7 +93,7 @@ data class Token( val coinGeckoId: String?, val logoURI: String?, val isPoints: Boolean?, -) { +) : Serializable { enum class NetworkType { AVALANCHE_C, AVALANCHE_ATOMIC, @@ -169,4 +170,4 @@ data class AddressArgument( val network: String? = null, val minimum: Double? = null, val maximum: Double? = null, -) \ No newline at end of file +) : Serializable \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index eafbb514fb..2e1fe82f5d 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,18 +2,21 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency interface StakingRepository { + fun isStakingSupported(currencyId: String): Boolean + + suspend fun fetchEnabledYields() + + suspend fun getEntryInfo(integrationId: String): StakingEntryInfo + + suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield + suspend fun getStakingAvailabilityForActions( cryptoCurrencyId: CryptoCurrency.ID, symbol: String, ): StakingAvailability - - suspend fun getEntryInfo(integrationId: String): StakingEntryInfo - - suspend fun fetchEnabledYields() - - fun isStakingSupported(currencyId: String): Boolean } \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 0a90fdc329..900ab7de1b 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -12,12 +12,12 @@ android { dependencies { /** Core modules */ - implementation(project(":core:analytics")) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:res")) - implementation(project(":core:utils")) - implementation(project(":core:ui")) - implementation(project(":libs:crypto")) + implementation(projects.core.res) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.libs.crypto) /** AndroidX */ implementation(deps.androidx.appCompat) @@ -30,7 +30,7 @@ dependencies { implementation(deps.compose.ui.tooling) /** Domain */ - implementation(project(":features:referral:domain")) + implementation(projects.features.referral.domain) /** Other libraries */ implementation(deps.compose.shimmer) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt deleted file mode 100644 index 5f11c074e1..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Surface -import androidx.compose.material3.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.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Suppress("LongParameterList") -@Composable -internal fun AwardText( - startText: String, - startTextColor: Color, - startTextStyle: TextStyle, - endText: String, - endTextColor: Color, - endTextStyle: TextStyle, - cornersToRound: CornersToRound, -) { - Surface( - shape = cornersToRound.getShape(), - color = TangemTheme.colors.background.primary, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(TangemTheme.dimens.size48) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = startText, - color = startTextColor, - maxLines = 1, - style = startTextStyle, - ) - - Text( - text = endText, - color = endTextColor, - maxLines = 1, - style = endTextStyle, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_AwardItem() { - TangemThemePreview { - AwardText( - startText = "startText", - startTextColor = TangemTheme.colors.text.tertiary, - startTextStyle = TangemTheme.typography.subtitle2, - endText = "endText", - endTextColor = TangemTheme.colors.text.primary1, - endTextStyle = TangemTheme.typography.subtitle2, - cornersToRound = CornersToRound.TOP_2, - ) - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt deleted file mode 100644 index b5070cdb28..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -internal enum class CornersToRound { - - ALL_4, - TOP_2, - BOTTOM_2, - ZERO, - ; - - @Suppress("TopLevelComposableFunctions") - @Composable - fun getShape(): RoundedCornerShape { - val radius = TangemTheme.dimens.radius12 - return when (this) { - ALL_4 -> RoundedCornerShape(radius) - TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) - BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - ZERO -> RoundedCornerShape(0.dp) - } - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index cd403043df..01e2c4b3d1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity import com.tangem.core.ui.components.PrimaryButtonIconStart +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward @@ -82,7 +84,7 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { val isExpectedAwardsPresent = expectedAwards != null - AwardText( + RoundableCornersRow( startText = stringResource(id = R.string.referral_friends_bought_title), startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, @@ -111,7 +113,7 @@ private fun Awards(expectedAwards: ExpectedAwards) { thickness = TangemTheme.dimens.size0_5, color = TangemTheme.colors.stroke.primary, ) - AwardText( + RoundableCornersRow( startText = if (expectedAwards.expectedAwards.isNotEmpty()) { stringResource(id = R.string.referral_expected_awards) } else { @@ -137,7 +139,7 @@ private fun Awards(expectedAwards: ExpectedAwards) { val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode) initialItems.forEachIndexed { index, expectedAward -> - AwardText( + RoundableCornersRow( startText = expectedAward.paymentDate, startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.subtitle2, @@ -218,7 +220,7 @@ private fun LessMoreButton(isExpanded: MutableState) { private fun ExtraItems(extraItems: List) { Column { extraItems.forEach { expectedAward -> - AwardText( + RoundableCornersRow( startText = expectedAward.paymentDate, startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.subtitle2, diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt index 1749258dfd..05277770a8 100644 --- a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt @@ -7,7 +7,8 @@ interface StakingRouter { fun getEntryFragment(): Fragment companion object { - const val CRYPTO_CURRENCY_KEY = "CRYPTO_CURRENCY_KEY" + const val CRYPTO_CURRENCY_ID_KEY = "CRYPTO_CURRENCY_ID_KEY" const val USER_WALLET_ID_KEY = "USER_WALLET_ID_KEY" + const val YIELD_KEY = "YIELD_KEY" } } \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 5640ea3503..e608669082 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.staking) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index fab926ce46..e96be559de 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.ui.StakingScreen import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel import dagger.hilt.android.AndroidEntryPoint import java.lang.ref.WeakReference @@ -57,8 +58,8 @@ internal class StakingFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } - val currentState = viewModel.stakingStateRouter.currentState.collectAsStateWithLifecycle() - // StakingScreen(viewModel.uiState, currentState.value) + val currentState = viewModel.uiState.collectAsStateWithLifecycle() + StakingScreen(currentState.value) } override fun onDestroy() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 1d0095389b..9fdd84f082 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -1,8 +1,8 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.event.consumedEvent +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.StakingScreenStateTransformer -import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -33,7 +33,7 @@ internal class StakingStateController @Inject constructor() { private fun getInitialState(): StakingUiState { return StakingUiState( - clickIntents = object : StakingClickIntents {}, + clickIntents = StakingClickIntentsStub, cryptoCurrencyName = "", currentScreen = StakingUiStateType.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 8355cab544..3852cd6ed2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents /** @@ -41,6 +42,14 @@ internal sealed class StakingStates { sealed class InitialInfoState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val available: String, + val onStake: String, + val aprRange: TextReference, + val unbondingPeriod: String, + val minimumRequirement: String, + val rewardClaiming: String, + val warmupPeriod: String, + val rewardSchedule: String, ) : InitialInfoState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt new file mode 100644 index 0000000000..16a8763435 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -0,0 +1,8 @@ +package com.tangem.features.staking.impl.presentation.state.stub + +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents + +object StakingClickIntentsStub : StakingClickIntents { + + override fun onBackClick() {} +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt new file mode 100644 index 0000000000..3ee1b970cd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -0,0 +1,77 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.Provider +import java.math.BigDecimal + +internal class SetInitialDataStateTransformer( + private val clickIntents: StakingClickIntents, + private val yield: Yield, + private val cryptoCurrencyStatusProvider: Provider, +) : StakingScreenStateTransformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + clickIntents = clickIntents, + currentScreen = StakingUiStateType.InitialInfo, + initialInfoState = createInitialInfoState(), + ) + } + + private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + return StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = true, + available = BigDecimalFormatter.formatCryptoAmount( + cryptoCurrencyStatus.value.amount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + onStake = "0 SOL", // TODO staking add after adding /balances request + aprRange = getAprRange(), + unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(), + minimumRequirement = yield.metadata.minimumStake.toString(), + rewardClaiming = yield.metadata.rewardClaiming, + warmupPeriod = yield.metadata.warmupPeriod.days.toString(), + rewardSchedule = yield.metadata.rewardSchedule, + + ) + } + + private fun getAprRange(): TextReference { + val aprValues = yield.validators.mapNotNull { it.apr } + + val minApr = aprValues.min() + val maxApr = aprValues.max() + + val formattedMinApr = BigDecimalFormatter.formatPercent( + percent = minApr, + useAbsoluteValue = true, + ) + val formattedMaxApr = BigDecimalFormatter.formatPercent( + percent = maxApr, + useAbsoluteValue = true, + ) + + if (maxApr - minApr < EQUALITY_THRESHOLD) { + return stringReference("$formattedMinApr%") + } + return resourceReference(R.string.common_percent_range, wrappedList(formattedMinApr, formattedMaxApr)) + } + + companion object { + private val EQUALITY_THRESHOLD = BigDecimal(1E-10) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt new file mode 100644 index 0000000000..7ff32c89e2 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -0,0 +1,200 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates + +@Composable +internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { + if (state !is StakingStates.InitialInfoState.Data) return + + Column( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16), + ) { + MetricsBlock(state) + Spacer(modifier = Modifier.height(8.dp)) + StakingDetailsRows(state) + Spacer(modifier = Modifier.height(8.dp)) + PrimaryButton( + text = stringResource(id = R.string.common_stake), + modifier = Modifier.fillMaxWidth(), + onClick = { + // TODO staking + }, + ) + } +} + +@Composable +private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { + Column( + modifier = Modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius12), + ) + .padding(16.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.staking_details_metrics_block_header), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1F)) { + Text( + text = stringResource(id = R.string.staking_details_apr), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = state.aprRange.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + Column(modifier = Modifier.weight(1F)) { + Row { + Text( + modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), + text = stringResource(id = R.string.staking_details_market_rating), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .align(Alignment.CenterVertically), + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + ) + } + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = "1", // TODO staking + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + } + } +} + +@Composable +internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_available), + endText = state.available, + cornersToRound = CornersToRound.TOP_2, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_on_stake), + endText = state.onStake, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_apy), + endText = state.aprRange.resolveReference(), + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_unbonding_period), + endText = state.unbondingPeriod, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_minimum_requirement), + endText = state.minimumRequirement, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_claiming), + endText = state.rewardClaiming, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_warmup_period), + endText = state.warmupPeriod, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_schedule), + endText = state.rewardSchedule, + cornersToRound = CornersToRound.BOTTOM_2, + ) +} + +@Composable +private fun InitialInfoContentRow(startText: String, endText: String, cornersToRound: CornersToRound) { + RoundableCornersRow( + startText = startText, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.body2, + endText = endText, + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = cornersToRound, + iconResId = null, // TODO staking add bottom sheets when text will be available + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingInitialInfoContent_Preview( + @PreviewParameter(StakingInitialInfoContentPreviewProvider::class) feeState: StakingStates.InitialInfoState.Data, +) { + TangemThemePreview { + StakingInitialInfoContent( + state = feeState, + ) + } +} + +private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = false, + available = "15 SOL", + onStake = "0 SOL", + aprRange = stringReference("2.54-5.12%"), + unbondingPeriod = "3d", + minimumRequirement = "12 SOL", + rewardClaiming = "Auto", + warmupPeriod = "Days", + rewardSchedule = "Block", + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt new file mode 100644 index 0000000000..c1f1bf5a95 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -0,0 +1,125 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.withIndex + +@Composable +internal fun StakingScreen(uiState: StakingUiState) { + BackHandler(onBack = uiState.clickIntents::onBackClick) + Column( + modifier = Modifier + .fillMaxSize() + .imePadding() + .systemBarsPadding() + .background(color = TangemTheme.colors.background.tertiary), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SendAppBar( + uiState = uiState, + ) + StakingScreenContent( + uiState = uiState, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun SendAppBar(uiState: StakingUiState) { + val titleRes = when (uiState.currentScreen) { + StakingUiStateType.InitialInfo -> stringResource(id = R.string.common_stake) + StakingUiStateType.Amount -> stringResource(id = R.string.send_amount_label) + StakingUiStateType.ValidatorAndFee -> stringResource(id = R.string.common_stake) + StakingUiStateType.Confirm -> "" + } + val backIcon = when (uiState.currentScreen) { + StakingUiStateType.Amount, + StakingUiStateType.ValidatorAndFee, + StakingUiStateType.Confirm, + -> { + R.drawable.ic_close_24 + } + StakingUiStateType.InitialInfo -> { + R.drawable.ic_back_24 + } + } + AppBarWithBackButtonAndIcon( + text = titleRes, + backIconRes = backIcon, + onBackClick = uiState.clickIntents::onBackClick, + backgroundColor = TangemTheme.colors.background.tertiary, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { + val currentScreen = uiState.currentScreen + var currentStateProxy by remember { mutableStateOf(currentScreen) } + var isTransitionAnimationRunning by remember { mutableStateOf(false) } + + // Prevent quick screen changes to avoid some of the transition animation distortions + LaunchedEffect(currentScreen) { + snapshotFlow { isTransitionAnimationRunning } + .withIndex() + .map { (index, running) -> + if (running && index != 0) { + delay(timeMillis = 200) + } + running + } + .first { !it } + + currentStateProxy = currentScreen + } + // Restrict pressing the back button while screen transition is running to avoid most of the animation distortions + BackHandler(enabled = isTransitionAnimationRunning) {} + + // Box is needed to fix animation with resizing of AnimatedContent + Box(modifier = modifier.fillMaxSize()) { + AnimatedContent( + targetState = currentStateProxy, + contentAlignment = Alignment.TopCenter, + label = "Staking Screen Navigation", + transitionSpec = { + val direction = if (initialState.ordinal < targetState.ordinal) { + AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End + } + + slideIntoContainer(towards = direction, animationSpec = tween()) + .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) + }, + ) { state -> + isTransitionAnimationRunning = transition.targetState != transition.currentState + + when (state) { + StakingUiStateType.InitialInfo -> StakingInitialInfoContent( + state = uiState.initialInfoState, + ) + else -> Unit + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 1aec6214cd..30f890ae97 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.viewmodel internal interface StakingClickIntents { - // TODO staking + + fun onBackClick() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 91cb3a2423..2c94bdea46 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -15,6 +16,8 @@ import com.tangem.features.staking.impl.presentation.state.StakingStateControlle import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStateRouter import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* @@ -37,12 +40,15 @@ internal class StakingViewModel @Inject constructor( var stakingStateRouter: StakingStateRouter by Delegates.notNull() private set - private val cryptoCurrency: CryptoCurrency = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_KEY] - ?: error("This screen can't open without `CryptoCurrency`") + private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_ID_KEY] + ?: error("This screen can't be opened without `CryptoCurrency.ID`") private val userWalletId: UserWalletId = savedStateHandle.get(StakingRouter.USER_WALLET_ID_KEY) ?.let { stringValue -> UserWalletId(stringValue) } - ?: error("This screen can't open without `UserWalletId`") + ?: error("This screen can't be opened without `UserWalletId`") + + private val yield: Yield = savedStateHandle[StakingRouter.YIELD_KEY] + ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -53,16 +59,27 @@ internal class StakingViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() } - fun setRouter(router: InnerStakingRouter, stakingStateRouter: StakingStateRouter) { + override fun onBackClick() { + stakingStateRouter.onBackClick() + } + + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router - this.stakingStateRouter = stakingStateRouter + this.stakingStateRouter = stateRouter } private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch { - getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrency.id).fold( + getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( ifRight = { cryptoCurrencyStatus = it + stateController.update( + transformer = SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + ), + ) }, ifLeft = { // TODO staking error diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index dc7674a9a7..514d108158 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -84,4 +84,6 @@ dependencies { /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) + implementation(projects.features.staking.api) + } \ 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 4b3900f2c3..f99dbb1cd3 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 @@ -283,6 +283,7 @@ internal object TokenDetailsPreviewData { periodInDays = 4, tokenSymbol = "XLM", iconState = iconState, + onStakeClicked = {}, ), notifications = persistentListOf(), txHistoryState = TxHistoryState.NotSupported( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt index f1668235b6..76d9483c9b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -16,5 +16,6 @@ internal sealed interface StakingBlockState { val interestRate: String, val periodInDays: Int, val tokenSymbol: String, + val onStakeClicked: () -> Unit, ) : StakingBlockState } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index cec107a630..9f8de13d25 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -103,6 +103,7 @@ internal class TokenDetailsStateFactory( private val stakingStateConverter by lazy { TokenStakingStateConverter( currentStateProvider = currentStateProvider, + clickIntents = clickIntents, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 371e2e766d..0bdf52927f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -5,10 +5,12 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter internal class TokenStakingStateConverter( + private val clickIntents: TokenDetailsClickIntents, private val currentStateProvider: Provider, ) : Converter, StakingBlockState> { @@ -28,6 +30,7 @@ internal class TokenStakingStateConverter( periodInDays = it.periodInDays, tokenSymbol = it.tokenSymbol, iconState = currentStateProvider().tokenInfoBlockState.iconState, + onStakeClicked = clickIntents::onStakeBannerClick, ) }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt index 6ce576304f..d888903a28 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt @@ -121,8 +121,8 @@ private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconSta } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = "Stake", - onClick = { /* [REDACTED_TODO_COMMENT] */ }, + text = stringResource(id = R.string.common_stake), + onClick = stakingBlockState.onStakeClicked, ) } } @@ -191,6 +191,7 @@ private class StakingBlockStateProvider : CollectionPreviewParameterProvider Date: Thu, 13 Jun 2024 18:27:21 +0500 Subject: [PATCH 018/223] Updated on 2026-08-14 --- .../core/ui/components/showcase/Showcase.kt | 143 ++++++++++++++++++ .../ui/components/showcase/ShowcaseContent.kt | 71 +++++++++ .../ui/components/showcase/ShowcaseItem.kt | 30 ++++ .../showcase/model/ShowcaseButtonModel.kt | 8 + .../showcase/model/ShowcaseItemModel.kt | 9 ++ .../drawable/ic_notifications_unread_24.xml | 11 ++ .../main/res/drawable/ic_rocket_launch_24.xml | 11 ++ .../main/res/drawable/ic_storefront_24.xml | 11 ++ 8 files changed, 294 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt create mode 100644 core/ui/src/main/res/drawable/ic_notifications_unread_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_rocket_launch_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_storefront_24.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt new file mode 100644 index 0000000000..8112024b54 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.components.showcase + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param headerIconRes big header icon + * @param headerText header text + * @param showcaseItems list of bullet points + * @param primaryButton primary button + * @param secondaryButton secondary button + * @param modifier compose modifier + * + * @see Figma component + */ +@Composable +fun Showcase( + @DrawableRes headerIconRes: Int, + headerText: TextReference, + showcaseItems: ImmutableList, + primaryButton: ShowcaseButtonModel, + secondaryButton: ShowcaseButtonModel, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + ShowcaseContent( + headerIconRes = headerIconRes, + headerText = headerText, + showcaseItems = showcaseItems, + modifier = Modifier + .weight(1f) + .align(Alignment.CenterHorizontally), + ) + ShowcaseButtons( + primaryButtonText = primaryButton.buttonText, + onPrimaryClick = primaryButton.onClick, + secondaryButtonText = secondaryButton.buttonText, + onSecondaryClick = secondaryButton.onClick, + ) + } +} + +@Composable +private fun ShowcaseButtons( + primaryButtonText: TextReference, + secondaryButtonText: TextReference, + onPrimaryClick: () -> Unit, + onSecondaryClick: () -> Unit, + hint: TextReference? = null, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PrimaryButton( + text = primaryButtonText.resolveReference(), + onClick = onPrimaryClick, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) + SecondaryButton( + text = secondaryButtonText.resolveReference(), + onClick = onSecondaryClick, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing16, + ), + ) + hint?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Showcase_Preview() { + TangemThemePreview { + Showcase( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + R.drawable.ic_rocket_launch_24, + resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + R.drawable.ic_storefront_24, + resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + primaryButton = ShowcaseButtonModel(resourceReference(R.string.common_allow), {}), + secondaryButton = ShowcaseButtonModel(resourceReference(R.string.common_later), {}), + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt new file mode 100644 index 0000000000..336172a208 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt @@ -0,0 +1,71 @@ +package com.tangem.core.ui.components.showcase + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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 com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun ShowcaseContent( + @DrawableRes headerIconRes: Int, + headerText: TextReference, + showcaseItems: ImmutableList, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier, + ) { + Icon( + painter = painterResource(id = headerIconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(TangemTheme.dimens.size56), + ) + Text( + text = headerText.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + ) + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + repeat(showcaseItems.size) { index -> + ShowcaseItem( + iconRes = showcaseItems[index].iconRes, + text = showcaseItems[index].text, + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt new file mode 100644 index 0000000000..824144dbf5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.components.showcase + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) { + Row { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing20), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt new file mode 100644 index 0000000000..602465c864 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.components.showcase.model + +import com.tangem.core.ui.extensions.TextReference + +data class ShowcaseButtonModel( + val buttonText: TextReference, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt new file mode 100644 index 0000000000..f9865aab9e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.components.showcase.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +data class ShowcaseItemModel( + @DrawableRes val iconRes: Int, + val text: TextReference, +) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml b/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml new file mode 100644 index 0000000000..c50e451c69 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml b/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml new file mode 100644 index 0000000000..a7b572f5f0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_storefront_24.xml b/core/ui/src/main/res/drawable/ic_storefront_24.xml new file mode 100644 index 0000000000..7f17e6c496 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_storefront_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + From 119e34b98b8bd3f6b70251e6b03cff75a8147f78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 5 Jun 2024 12:15:51 +0400 Subject: [PATCH 019/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index a267125f20..70192d4bb6 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -298,19 +298,21 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - val pendingIntent = PendingIntent.getActivity( - this, - 0, - Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, - ) - val intentFilters = arrayOf( - IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), - ) - nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) + val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + if (nfcAdapter?.isEnabled == true) { + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + val intentFilters = arrayOf( + IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), + ) + nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) + } // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) @@ -319,8 +321,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onPause() { super.onPause() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - nfcAdapter.disableForegroundDispatch(this) + val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + + if (nfcAdapter?.isEnabled == true) { + nfcAdapter.disableForegroundDispatch(this) + } } override fun onStop() { From 40c7d20bdaa5479d8258cc1defbc874e3c30ecbc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 14:49:38 +0400 Subject: [PATCH 020/223] Updated on 2026-08-14 --- .../java/com/tangem/tap/TangemApplication.kt | 4 +-- ...ackManager.kt => LegacyFeedbackManager.kt} | 4 +-- .../common/feedback/ProxyFeedbackManager.kt | 28 +++++++++++++++++++ .../tap/common/redux/global/GlobalAction.kt | 4 +-- .../tap/common/redux/global/GlobalState.kt | 4 +-- .../java/com/tangem/tap/di/UtilsModule.kt | 18 ++++++++++++ .../com/tangem/tap/proxy/AppStateHolder.kt | 6 ---- .../feedback/DummyFeedbackManager.kt | 8 ++++++ .../navigation/feedback/FeedbackManager.kt | 6 ++++ .../core/navigation/feedback/FeedbackType.kt | 13 +++++++++ .../tangem/domain/redux/ReduxStateHolder.kt | 2 -- .../tangem/features/details/DetailsRouter.kt | 5 +++- 12 files changed, 85 insertions(+), 17 deletions(-) rename app/src/main/java/com/tangem/tap/common/feedback/{FeedbackManager.kt => LegacyFeedbackManager.kt} (98%) create mode 100644 app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt create mode 100644 app/src/main/java/com/tangem/tap/di/UtilsModule.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 040fb176a2..47aeee6e6f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -53,7 +53,7 @@ import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHand import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.common.log.TimberFormatStrategy @@ -363,7 +363,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector, ) - val feedbackManager = FeedbackManager( + val feedbackManager = LegacyFeedbackManager( infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, chatManager = ChatManager(foregroundActivityObserver), diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt rename to app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt index 98aa013e84..ed99833b04 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt @@ -25,7 +25,7 @@ import java.io.StringWriter /** [REDACTED_AUTHOR] */ -class FeedbackManager( +class LegacyFeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val chatManager: ChatManager, @@ -144,7 +144,7 @@ class FeedbackManager( } } - companion object { + private companion object { const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" const val FEEDBACK_FILE = "feedback.txt" diff --git a/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt new file mode 100644 index 0000000000..680468bef1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.common.feedback + +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.feedback.FeedbackType +import com.tangem.tap.store +import timber.log.Timber + +internal class ProxyFeedbackManager : FeedbackManager { + + override fun sendEmail(type: FeedbackType) { + val manager = store.state.globalState.feedbackManager + + if (manager == null) { + Timber.e("Feedback manager is not initialized") + return + } + + val data = when (type) { + is FeedbackType.Feedback -> FeedbackEmail() + is FeedbackType.RateCanBeBetter -> RateCanBeBetterEmail() + is FeedbackType.ScanFails -> ScanFailsEmail() + is FeedbackType.SendTransactionFailed -> SendTransactionFailedEmail(type.error) + is FeedbackType.Support -> FeedbackEmail() + } + + manager.sendEmail(data) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 8439279a0a..4b41139f70 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -9,7 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.feedback.FeedbackData -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction @@ -79,7 +79,7 @@ sealed class GlobalAction : Action { data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() - data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() + data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 7a75d16d87..234e7e1de1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -5,7 +5,7 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.onboarding.OnboardingManager @@ -20,7 +20,7 @@ data class GlobalState( val tapWalletManager: TapWalletManager = TapWalletManager(), val configManager: ConfigManager? = null, val warningManager: WarningMessagesManager? = null, - val feedbackManager: FeedbackManager? = null, + val feedbackManager: LegacyFeedbackManager? = null, val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt new file mode 100644 index 0000000000..42a9877cd6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di + +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.tap.common.feedback.ProxyFeedbackManager +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 UtilsModule { + + @Provides + @Singleton + fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager() +} \ No newline at end of file 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 cf7de3bfe1..5faa769457 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -10,9 +10,7 @@ import com.tangem.domain.wallets.models.UserWallet 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.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.ExchangeService import org.rekotlin.Action @@ -56,8 +54,4 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde override suspend fun onUserWalletSelected(userWallet: UserWallet) { mainStore?.onUserWalletSelected(userWallet) } - - override fun sendFeedbackEmail() { - mainStore?.dispatch(GlobalAction.SendEmail(FeedbackEmail())) - } } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt new file mode 100644 index 0000000000..2f92f40ad9 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.feedback + +class DummyFeedbackManager : FeedbackManager { + + override fun sendEmail(type: FeedbackType) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt new file mode 100644 index 0000000000..a76d2704e6 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.feedback + +interface FeedbackManager { + + fun sendEmail(type: FeedbackType) +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt new file mode 100644 index 0000000000..b1470f5e4d --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt @@ -0,0 +1,13 @@ +package com.tangem.core.navigation.feedback + +sealed class FeedbackType { + data object RateCanBeBetter : FeedbackType() + + data object ScanFails : FeedbackType() + + data class SendTransactionFailed(val error: String) : FeedbackType() + + data object Feedback : FeedbackType() + + data object Support : FeedbackType() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt index 80e03d9347..58c0bb0dde 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -10,6 +10,4 @@ interface ReduxStateHolder { suspend fun dispatchWithMain(action: Action) suspend fun onUserWalletSelected(userWallet: UserWallet) - - fun sendFeedbackEmail() } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt index fbd703a49a..2613e8b19e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -4,6 +4,8 @@ import com.tangem.core.decompose.navigation.Route import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.feedback.FeedbackType import com.tangem.domain.redux.ReduxStateHolder import com.tangem.features.details.routing.DetailsRoute import com.tangem.features.tester.api.TesterRouter @@ -13,6 +15,7 @@ import javax.inject.Inject internal class DetailsRouter @Inject constructor( private val reduxNavController: ReduxNavController, private val reduxStateHolder: ReduxStateHolder, + private val feedbackManager: FeedbackManager, private val testerRouter: TesterRouter, ) : Router { @@ -23,7 +26,7 @@ internal class DetailsRouter @Inject constructor( reduxNavController.navigate(NavigationAction.NavigateTo(route.screen, bundle = route.params)) } is DetailsRoute.Feedback -> { - reduxStateHolder.sendFeedbackEmail() + feedbackManager.sendEmail(FeedbackType.Feedback) } is DetailsRoute.TesterMenu -> { testerRouter.startTesterScreen() From 768b44f44f4729f02b75c45d09dbf51030e878e6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 14:57:43 +0400 Subject: [PATCH 021/223] Updated on 2026-08-14 --- .../com/tangem/tap/common/extensions/Store.kt | 3 ++- .../redux/navigation/NavigationMiddleware.kt | 6 ------ ...ssianCardholdersWarningBottomSheetDialog.kt | 4 ++-- .../CustomTabsUrlOpener.kt} | 18 +++++++++++++++--- .../main/java/com/tangem/tap/di/UtilsModule.kt | 6 ++++++ .../details/ui/details/DetailsViewModel.kt | 3 ++- .../tangem/core/navigation/NavigationAction.kt | 2 -- .../core/navigation/url/DummyUrlOpener.kt | 8 ++++++++ .../tangem/core/navigation/url/UrlOpener.kt | 6 ++++++ .../tangem/features/details/DetailsRouter.kt | 6 +++--- .../features/send/impl/di/SendRouterModule.kt | 5 +++-- .../send/impl/navigation/DefaultSendRouter.kt | 4 +++- .../di/TokenDetailsRouterModule.kt | 5 +++-- .../router/DefaultTokenDetailsRouter.kt | 4 +++- .../feature/wallet/di/WalletRouterModule.kt | 5 +++-- .../presentation/router/DefaultWalletRouter.kt | 4 +++- 16 files changed, 62 insertions(+), 27 deletions(-) rename app/src/main/java/com/tangem/tap/common/{CustomTabsManager.kt => url/CustomTabsUrlOpener.kt} (72%) create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index d16d3488e2..24795a52d0 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -6,6 +6,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.url.urlOpener import com.tangem.tap.domain.TapError import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope @@ -83,7 +84,7 @@ suspend fun dispatchOnMain(vararg actions: Action) { } fun Store<*>.dispatchOpenUrl(url: String) { - dispatch(NavigationAction.OpenUrl(url)) + urlOpener.openUrl(url) } fun Store<*>.dispatchShare(url: String) { diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index ad2b8f3178..b598324140 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -7,7 +7,6 @@ import android.provider.Settings import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.tap.activityResultCaller -import com.tangem.tap.common.CustomTabsManager import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.store @@ -47,11 +46,6 @@ val navigationMiddleware: Middleware = { _, state -> } } } - is NavigationAction.OpenUrl -> { - navState?.activity?.get()?.let { - CustomTabsManager().openUrl(action.url, it) - } - } is NavigationAction.OpenDocument -> { val intent = Intent(Intent.ACTION_VIEW) intent.data = action.url diff --git a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt index 64763a6216..05dd766362 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt @@ -5,11 +5,11 @@ import android.os.Bundle import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.url.urlOpener import com.tangem.tap.store import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding @@ -44,7 +44,7 @@ class RussianCardholdersWarningBottomSheetDialog( dismiss() } binding?.btnNo?.setOnClickListener { - store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL)) + urlOpener.openUrl(INSTRUCTION_URL) dismiss() } } diff --git a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt similarity index 72% rename from app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt rename to app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index c56c238e57..72ec8c830c 100644 --- a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common +package com.tangem.tap.common.url import android.content.Context import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -8,12 +8,24 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT +import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.getColorCompat +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R -class CustomTabsManager { - fun openUrl(url: String, context: Context) { +internal val urlOpener = CustomTabsUrlOpener + +internal object CustomTabsUrlOpener : UrlOpener { + + override fun openUrl(url: String) { + foregroundActivityObserver.withForegroundActivity { + openUrl(url, context = it) + } + } + + private fun openUrl(url: String, context: Context) { if (url.isEmpty()) return val customTabsIntent = CustomTabsIntent.Builder() .setDefaultColorSchemeParams( diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index 42a9877cd6..a19ec4e188 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.feedback.ProxyFeedbackManager +import com.tangem.tap.common.url.CustomTabsUrlOpener import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -12,6 +14,10 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object UtilsModule { + @Provides + @Singleton + fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener + @Provides @Singleton fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 95a6865b3c..df60e9704e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -22,6 +22,7 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.url.urlOpener import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -179,7 +180,7 @@ internal class DetailsViewModel( private fun handleSocialNetworkClick(link: SocialNetworkLink) { Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatchOnMain(NavigationAction.OpenUrl(link.url)) + urlOpener.openUrl(link.url) } private fun getSocialLinks(): ImmutableList { diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt index 676958ef92..69e8ea227b 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt @@ -17,8 +17,6 @@ sealed class NavigationAction : Action { data class PopBackTo(val screen: AppScreen? = null, val inclusive: Boolean = false) : NavigationAction() - data class OpenUrl(val url: String) : NavigationAction() - data class OpenDocument(val url: Uri) : NavigationAction() object OpenBiometricsSettings : NavigationAction() diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt b/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt new file mode 100644 index 0000000000..cff4946b40 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.url + +class DummyUrlOpener : UrlOpener { + + override fun openUrl(url: String) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt b/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt new file mode 100644 index 0000000000..d78ba1b1eb --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.url + +interface UrlOpener { + + fun openUrl(url: String) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt index 2613e8b19e..07a52559da 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -6,7 +6,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController import com.tangem.core.navigation.feedback.FeedbackManager import com.tangem.core.navigation.feedback.FeedbackType -import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.details.routing.DetailsRoute import com.tangem.features.tester.api.TesterRouter import javax.inject.Inject @@ -14,9 +14,9 @@ import javax.inject.Inject // TODO: Remove after [REDACTED_JIRA] internal class DetailsRouter @Inject constructor( private val reduxNavController: ReduxNavController, - private val reduxStateHolder: ReduxStateHolder, private val feedbackManager: FeedbackManager, private val testerRouter: TesterRouter, + private val urlOpener: UrlOpener, ) : Router { override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { @@ -32,7 +32,7 @@ internal class DetailsRouter @Inject constructor( testerRouter.startTesterScreen() } is DetailsRoute.Url -> { - reduxNavController.navigate(NavigationAction.OpenUrl(route.url)) + urlOpener.openUrl(route.url) } } onComplete(true) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 7f38cacd35..352d2aaf2b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter import dagger.Module @@ -18,7 +19,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter { - return DefaultSendRouter(reduxNavController) + fun provideSendRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): SendRouter { + return DefaultSendRouter(reduxNavController, urlOpener) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 0629979fd1..59eade45d0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -14,12 +15,13 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultSendRouter( private val reduxNavController: ReduxNavController, + private val urlOpener: UrlOpener, ) : InnerSendRouter { override fun getEntryFragment(): Fragment = SendFragment.create() override fun openUrl(url: String) { - reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + urlOpener.openUrl(url) } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { 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 b87eb5b538..425de226e1 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,6 +1,7 @@ package com.tangem.feature.tokendetails.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.Module @@ -15,7 +16,7 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(reduxNavController: ReduxNavController): TokenDetailsRouter { - return DefaultTokenDetailsRouter(reduxNavController) + fun provideTokenDetailsRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): TokenDetailsRouter { + return DefaultTokenDetailsRouter(reduxNavController, urlOpener) } } \ 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 cadf3b9549..cb1c500d1a 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 @@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment @@ -12,6 +13,7 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultTokenDetailsRouter( private val reduxNavController: ReduxNavController, + private val urlOpener: UrlOpener, ) : InnerTokenDetailsRouter { override fun getEntryFragment(): Fragment = TokenDetailsFragment() @@ -21,7 +23,7 @@ internal class DefaultTokenDetailsRouter( } override fun openUrl(url: String) { - reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + urlOpener.openUrl(url) } override fun share(text: String) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 1e64fc4122..69ec26457f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.url.UrlOpener import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module @@ -15,7 +16,7 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { - return DefaultWalletRouter(reduxNavController = reduxNavController) + fun provideWalletRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): WalletRouter { + return DefaultWalletRouter(reduxNavController, urlOpener) } } \ 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 6276cdb2e9..e8b5cc2fb9 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 @@ -20,6 +20,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController import com.tangem.core.navigation.StateDialog +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.onboarding.navigation.OnboardingRouter @@ -37,6 +38,7 @@ import kotlin.properties.Delegates /** Default implementation of wallet feature router */ internal class DefaultWalletRouter( private val reduxNavController: ReduxNavController, + private val urlOpener: UrlOpener, ) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() @@ -138,7 +140,7 @@ internal class DefaultWalletRouter( } override fun openUrl(url: String) { - reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) + urlOpener.openUrl(url) } override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { From e4039b220cdc528b87af95d2d53f6a07cc4fd810 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:00:57 +0400 Subject: [PATCH 022/223] Updated on 2026-08-14 --- .../com/tangem/tap/common/extensions/Store.kt | 4 ++-- .../com/tangem/tap/common/extensions/UI.kt | 15 ++++-------- .../redux/navigation/NavigationMiddleware.kt | 8 +++---- .../tap/common/share/IntentShareManager.kt | 24 +++++++++++++++++++ .../java/com/tangem/tap/di/UtilsModule.kt | 6 +++++ .../core/navigation/NavigationAction.kt | 2 -- .../navigation/share/DummyShareManager.kt | 8 +++++++ .../core/navigation/share/ShareManager.kt | 6 +++++ .../di/TokenDetailsRouterModule.kt | 9 +++++-- .../router/DefaultTokenDetailsRouter.kt | 4 +++- 10 files changed, 64 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 24795a52d0..a413800a36 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,11 +1,11 @@ package com.tangem.tap.common.extensions -import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.StateDialog import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.share.shareManager import com.tangem.tap.common.url.urlOpener import com.tangem.tap.domain.TapError import com.tangem.tap.proxy.redux.DaggerGraphState @@ -88,7 +88,7 @@ fun Store<*>.dispatchOpenUrl(url: String) { } fun Store<*>.dispatchShare(url: String) { - dispatch(NavigationAction.Share(url)) + shareManager.shareText(url) } inline fun Store.inject(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 524edaded8..b14e9e4090 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -3,7 +3,10 @@ package com.tangem.tap.common.extensions import android.app.Activity -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.ContextWrapper import android.graphics.drawable.Drawable import android.view.View import androidx.annotation.* @@ -78,16 +81,6 @@ fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? { return clipData.getItemAt(0).text } -fun Context.shareText(text: String) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) -} - fun View.getString(resId: Int, vararg formatArgs: Any?): String { return context.getString(resId, *formatArgs) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index b598324140..28689fc006 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -7,7 +7,10 @@ import android.provider.Settings import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.tap.activityResultCaller -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.openFragment +import com.tangem.tap.common.extensions.popBackTo import com.tangem.tap.common.redux.AppState import com.tangem.tap.store import org.rekotlin.Middleware @@ -71,9 +74,6 @@ val navigationMiddleware: Middleware = { _, state -> } activityResultCaller.activityResultLauncher?.launch(intent) } - is NavigationAction.Share -> { - navState?.activity?.get()?.shareText(action.data) - } is NavigationAction.ActivityCreated, is NavigationAction.ActivityDestroyed, -> Unit diff --git a/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt new file mode 100644 index 0000000000..e681c2c36f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.common.share + +import android.content.Intent +import com.tangem.core.navigation.share.ShareManager +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal val shareManager = IntentShareManager + +internal object IntentShareManager : ShareManager { + + override fun shareText(text: String) { + foregroundActivityObserver.withForegroundActivity { activity -> + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + + activity.startActivity(shareIntent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index a19ec4e188..c8a58125d1 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -1,8 +1,10 @@ package com.tangem.tap.di import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.feedback.ProxyFeedbackManager +import com.tangem.tap.common.share.IntentShareManager import com.tangem.tap.common.url.CustomTabsUrlOpener import dagger.Module import dagger.Provides @@ -14,6 +16,10 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object UtilsModule { + @Provides + @Singleton + fun provideShareManager(): ShareManager = IntentShareManager + @Provides @Singleton fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt index 69e8ea227b..2122aff4b6 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt @@ -23,8 +23,6 @@ sealed class NavigationAction : Action { data class OpenDialog(val stateDialog: StateDialog) : NavigationAction() - data class Share(val data: String) : NavigationAction() - data class ActivityCreated(val activity: WeakReference) : NavigationAction() data class ActivityDestroyed(val activity: WeakReference) : NavigationAction() diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt new file mode 100644 index 0000000000..20769b6995 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.share + +class DummyShareManager : ShareManager { + + override fun shareText(text: String) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt new file mode 100644 index 0000000000..81146f78f4 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.share + +interface ShareManager { + + fun shareText(text: String) +} \ No newline at end of file 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 425de226e1..186a4fcc7b 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,6 +1,7 @@ package com.tangem.feature.tokendetails.di import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -16,7 +17,11 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): TokenDetailsRouter { - return DefaultTokenDetailsRouter(reduxNavController, urlOpener) + fun provideTokenDetailsRouter( + reduxNavController: ReduxNavController, + urlOpener: UrlOpener, + shareManager: ShareManager, + ): TokenDetailsRouter { + return DefaultTokenDetailsRouter(reduxNavController, urlOpener, shareManager) } } \ 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 cb1c500d1a..16b2a547ca 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 @@ -5,6 +5,7 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -14,6 +15,7 @@ import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultTokenDetailsRouter( private val reduxNavController: ReduxNavController, private val urlOpener: UrlOpener, + private val shareManager: ShareManager, ) : InnerTokenDetailsRouter { override fun getEntryFragment(): Fragment = TokenDetailsFragment() @@ -27,7 +29,7 @@ internal class DefaultTokenDetailsRouter( } override fun share(text: String) { - reduxNavController.navigate(NavigationAction.Share(text)) + shareManager.shareText(text) } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { From eed9d7e9eced56adcf30c09d8be9d2a657316800 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:15:55 +0400 Subject: [PATCH 023/223] Updated on 2026-08-14 --- .../com/tangem/tap/ActivityResultCaller.kt | 24 +++++++++++++++++++ .../redux/navigation/NavigationMiddleware.kt | 23 ------------------ .../details/redux/DetailsMiddleware.kt | 6 ++--- .../saveWallet/redux/SaveWalletMiddleware.kt | 7 ++---- .../core/navigation/NavigationAction.kt | 2 -- 5 files changed, 28 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt index 16a05117ac..3ef77ac909 100644 --- a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt +++ b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt @@ -1,8 +1,32 @@ package com.tangem.tap import android.content.Intent +import android.hardware.biometrics.BiometricManager +import android.os.Build +import android.provider.Settings import androidx.activity.result.ActivityResultLauncher interface ActivityResultCaller { val activityResultLauncher: ActivityResultLauncher? +} + +internal fun ActivityResultCaller.openSystemBiometrySettings() { + val settingsAction = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + Settings.ACTION_BIOMETRIC_ENROLL + } + else -> { + Settings.ACTION_SECURITY_SETTINGS + } + } + val intent = Intent(settingsAction).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BiometricManager.Authenticators.BIOMETRIC_STRONG, + ) + } + } + + activityResultLauncher?.launch(intent) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index 28689fc006..78936f78fb 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -1,12 +1,8 @@ package com.tangem.tap.common.redux.navigation import android.content.Intent -import android.hardware.biometrics.BiometricManager -import android.os.Build -import android.provider.Settings import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.activityResultCaller import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.openFragment @@ -55,25 +51,6 @@ val navigationMiddleware: Middleware = { _, state -> navState?.activity?.get()?.startActivity(intent) } is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog) - is NavigationAction.OpenBiometricsSettings -> { - val settingsAction = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - Settings.ACTION_BIOMETRIC_ENROLL - } - else -> { - Settings.ACTION_SECURITY_SETTINGS - } - } - val intent = Intent(settingsAction).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - putExtra( - Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, - BiometricManager.Authenticators.BIOMETRIC_STRONG, - ) - } - } - activityResultCaller.activityResultLauncher?.launch(intent) - } is NavigationAction.ActivityCreated, is NavigationAction.ActivityDestroyed, -> Unit 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 372889fe91..a9f6bdca6d 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 @@ -20,6 +20,7 @@ import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet +import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.* @@ -30,9 +31,6 @@ 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.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R @@ -273,7 +271,7 @@ class DetailsMiddleware { private fun enrollBiometrics() { Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) - store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + activityResultCaller.openSystemBiometrySettings() } private fun changeAppThemeMode(appThemeMode: AppThemeMode) { 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 45eb5225a2..932c1a1965 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 @@ -8,6 +8,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet +import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding @@ -16,11 +17,7 @@ import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch @@ -88,7 +85,7 @@ internal class SaveWalletMiddleware { } private fun enrollBiometrics() { - store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + activityResultCaller.openSystemBiometrySettings() } private fun allowToUseBiometrics(state: SaveWalletState) { diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt index 2122aff4b6..af4bfc2b33 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt @@ -19,8 +19,6 @@ sealed class NavigationAction : Action { data class OpenDocument(val url: Uri) : NavigationAction() - object OpenBiometricsSettings : NavigationAction() - data class OpenDialog(val stateDialog: StateDialog) : NavigationAction() data class ActivityCreated(val activity: WeakReference) : NavigationAction() From 821be735715b7e9f8d75e692a150255d88bc13c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:21:19 +0400 Subject: [PATCH 024/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/common/DialogManager.kt | 2 +- .../java/com/tangem/tap/common/extensions/Store.kt | 2 +- .../main/java/com/tangem/tap/common/redux/AppDialog.kt | 2 +- .../com/tangem/tap/common/redux/global/GlobalAction.kt | 2 +- .../tangem/tap/common/redux/global/GlobalMiddleware.kt | 2 +- .../com/tangem/tap/common/redux/global/GlobalState.kt | 2 +- .../common/redux/navigation/NavigationMiddleware.kt | 2 -- .../java/com/tangem/tap/common/ui/ScanFailsDialog.kt | 2 +- .../tangem/tap/domain/scanCard/UseCaseScanProcessor.kt | 2 +- .../details/redux/walletconnect/WalletConnectState.kt | 2 +- .../tangem/tap/features/onboarding/OnboardingDialog.kt | 2 +- .../products/wallet/redux/OnboardingWalletState.kt | 2 +- .../tangem/tap/features/send/redux/SendScreenAction.kt | 2 +- .../tangem/tap/features/send/redux/states/SendState.kt | 2 +- .../main/java/com/tangem/tap/proxy/AppStateHolder.kt | 6 ++++++ .../com/tangem/core/navigation/NavigationAction.kt | 2 -- .../java/com/tangem/domain/redux/ReduxStateHolder.kt | 2 ++ .../main/java/com/tangem/domain/redux}/StateDialog.kt | 2 +- .../com/tangem/feature/wallet/di/WalletRouterModule.kt | 9 +++++++-- .../wallet/presentation/router/DefaultWalletRouter.kt | 10 ++++------ 20 files changed, 33 insertions(+), 26 deletions(-) rename {core/navigation/src/main/java/com/tangem/core/navigation => domain/legacy/src/main/java/com/tangem/domain/redux}/StateDialog.kt (83%) diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 6b870386be..1f9043292e 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common import android.app.Dialog import android.content.Context -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.ui.* diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index a413800a36..3d7a938cf3 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.extensions -import com.tangem.core.navigation.StateDialog import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt index fc362509c3..b896303e37 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.redux import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.TestAction import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.WalletAddressData diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 4b41139f70..4aa4c0b58b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -2,12 +2,12 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.feedback.FeedbackData import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.redux.DebugErrorAction diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index fc584f6b8e..f1d80acbf8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.models.Config import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.LogConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.send.redux.SendAction diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 234e7e1de1..50f8f85f47 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,10 +1,10 @@ package com.tangem.tap.common.redux.global -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.ConfigManager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index 78936f78fb..00f61f18db 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -3,7 +3,6 @@ package com.tangem.tap.common.redux.navigation import android.content.Intent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.openFragment import com.tangem.tap.common.extensions.popBackTo @@ -50,7 +49,6 @@ val navigationMiddleware: Middleware = { _, state -> intent.data = action.url navState?.activity?.get()?.startActivity(intent) } - is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog) is NavigationAction.ActivityCreated, is NavigationAction.ActivityDestroyed, -> Unit diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index d59f668687..33c8044a23 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -6,7 +6,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.feedback.ScanFailsEmail import com.tangem.tap.common.redux.global.GlobalAction 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 ed777bd666..bdb9786c3c 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 @@ -8,9 +8,9 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.StateDialog import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 750f4b04c5..65d11fc520 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.core.navigation.StateDialog import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt index 0134ddaf15..97f17bd9b5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 64b81fc771..6663311df8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.graphics.Bitmap import android.net.Uri -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import org.rekotlin.StateType /** 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 3c3409459e..5663b7cec7 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 @@ -7,7 +7,7 @@ import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.core.TangemSdkError -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered import com.tangem.tap.common.redux.ToastNotificationAction diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index d3d3cd7e19..115ab92034 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.isZero -import com.tangem.core.navigation.StateDialog import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.CurrencyConverter import com.tangem.tap.common.entities.IndeterminateProgressButton 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 5faa769457..0e1a98b9c7 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -6,7 +6,9 @@ import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected @@ -54,4 +56,8 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde override suspend fun onUserWalletSelected(userWallet: UserWallet) { mainStore?.onUserWalletSelected(userWallet) } + + override fun dispatchDialogShow(dialog: StateDialog) { + mainStore?.dispatchDialogShow(dialog) + } } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt index af4bfc2b33..ec50c35bc0 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt @@ -19,8 +19,6 @@ sealed class NavigationAction : Action { data class OpenDocument(val url: Uri) : NavigationAction() - data class OpenDialog(val stateDialog: StateDialog) : NavigationAction() - data class ActivityCreated(val activity: WeakReference) : NavigationAction() data class ActivityDestroyed(val activity: WeakReference) : NavigationAction() diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt index 58c0bb0dde..afa5e3bf04 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -10,4 +10,6 @@ interface ReduxStateHolder { suspend fun dispatchWithMain(action: Action) suspend fun onUserWalletSelected(userWallet: UserWallet) + + fun dispatchDialogShow(dialog: StateDialog) } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt similarity index 83% rename from core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt rename to domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt index 75aa1f0579..24571182fb 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -1,4 +1,4 @@ -package com.tangem.core.navigation +package com.tangem.domain.redux interface StateDialog { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 69ec26457f..536e255a98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.di import com.tangem.core.navigation.ReduxNavController import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module @@ -16,7 +17,11 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): WalletRouter { - return DefaultWalletRouter(reduxNavController, urlOpener) + fun provideWalletRouter( + reduxNavController: ReduxNavController, + reduxStateHolder: ReduxStateHolder, + urlOpener: UrlOpener, + ): WalletRouter { + return DefaultWalletRouter(reduxNavController, reduxStateHolder, urlOpener) } } \ 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 e8b5cc2fb9..e30b525b92 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 @@ -19,8 +19,9 @@ import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController -import com.tangem.core.navigation.StateDialog import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.onboarding.navigation.OnboardingRouter @@ -38,6 +39,7 @@ import kotlin.properties.Delegates /** Default implementation of wallet feature router */ internal class DefaultWalletRouter( private val reduxNavController: ReduxNavController, + private val reduxStateHolder: ReduxStateHolder, private val urlOpener: UrlOpener, ) : InnerWalletRouter { @@ -173,11 +175,7 @@ internal class DefaultWalletRouter( } override fun openScanFailedDialog() { - reduxNavController.navigate( - action = NavigationAction.OpenDialog( - StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN), - ), - ) + reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN)) } private companion object { From ba4ceb2e8bc1d9003b73a3747c38d38c5ec1ca08 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:24:34 +0400 Subject: [PATCH 025/223] Updated on 2026-08-14 --- .../decompose/factory/ComponentFactory.kt | 8 ++++++ .../decompose/navigation/DefaultRouter.kt | 19 ++++++++++++-- .../core/decompose/navigation/DummyRouter.kt | 26 +++++++++++++++++++ .../core/decompose/navigation/Router.kt | 19 +++++++++++++- .../core/decompose/navigation/RouterExt.kt | 11 ++++++++ .../tangem/features/details/DetailsRouter.kt | 17 +++++++----- .../component/preview/PreviewRouter.kt | 9 +++++++ .../features/details/utils/UserWalletSaver.kt | 6 ++--- 8 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt create mode 100644 core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt create mode 100644 core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt new file mode 100644 index 0000000000..b365fcea59 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt @@ -0,0 +1,8 @@ +package com.tangem.core.decompose.factory + +import com.tangem.core.decompose.context.AppComponentContext + +interface ComponentFactory { + + fun create(context: AppComponentContext, params: Params): Component +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt index 50e3840442..78e7d91727 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.popWhile import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import kotlin.reflect.KClass internal class DefaultRouter( private val navigationProvider: AppNavigationProvider, @@ -23,9 +24,23 @@ internal class DefaultRouter( navigation.pop(onComplete) } - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { navigation.popWhile( - predicate = { it != route }, + predicate = { it::class != routeClass }, + onComplete = onComplete, + ) + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + navigation.popWhile( + predicate = { it::class != routeClass }, + onComplete = onComplete, + ) + } + + override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { + navigation.popWhile( + predicate = { true }, onComplete = onComplete, ) } diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt new file mode 100644 index 0000000000..a6352229b0 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt @@ -0,0 +1,26 @@ +package com.tangem.core.decompose.navigation + +import kotlin.reflect.KClass + +class DummyRouter : Router { + + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt index 3a9b7d93ca..f2ad82f9ea 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt @@ -1,5 +1,7 @@ package com.tangem.core.decompose.navigation +import kotlin.reflect.KClass + /** * Interface for a router in the application. * It provides methods for navigating through the application. @@ -22,10 +24,25 @@ interface Router { fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) /** - * Pops routes from the navigation stack until the specified route is found. + * Pops routes from the navigation stack until the specified [route] is found. * * @param route The route to pop to. * @param onComplete The callback to be invoked when the operation is complete. */ fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the ***first*** specified [routeClass] is found. + * + * @param routeClass The route class to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops all routes from the navigation stack. + * + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun clear(onComplete: (isSuccess: Boolean) -> Unit = {}) } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt new file mode 100644 index 0000000000..777cc78199 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt @@ -0,0 +1,11 @@ +package com.tangem.core.decompose.navigation + +/** + * Pops routes from the navigation stack until the specified route [R] is found. + * + * @param R The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ +inline fun Router.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) { + popTo(R::class, onComplete) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt index 07a52559da..a7cb49988e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -10,6 +10,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.details.routing.DetailsRoute import com.tangem.features.tester.api.TesterRouter import javax.inject.Inject +import kotlin.reflect.KClass // TODO: Remove after [REDACTED_JIRA] internal class DetailsRouter @Inject constructor( @@ -46,13 +47,15 @@ internal class DetailsRouter @Inject constructor( onComplete(true) } + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + TODO("This class will be removed in future") + } + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - if (route is DetailsRoute.Screen) { - reduxNavController.popBackStack(route.screen) - onComplete(true) - } else { - reduxNavController.getBackStack() - onComplete(false) - } + TODO("This class will be removed in future") + } + + override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { + TODO("This class will be removed in future") } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt index 49366db923..adfd6a5f4e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component.preview import com.tangem.core.decompose.navigation.Route import com.tangem.core.decompose.navigation.Router +import kotlin.reflect.KClass internal class PreviewRouter : Router { override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { @@ -12,7 +13,15 @@ internal class PreviewRouter : Router { /* no-op */ } + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + /* no-op */ + } + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { /* no-op */ } + + override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { + /* no-op */ + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 243c6849d7..cd539ea937 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -6,7 +6,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.AppScreen import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference @@ -22,7 +21,6 @@ import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.details.impl.R -import com.tangem.features.details.routing.DetailsRoute import javax.inject.Inject @ComponentScoped @@ -44,7 +42,7 @@ internal class UserWalletSaver @Inject constructor( saveWallet(userWallet) - router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + // router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) }, recover = { error -> val message = error.message @@ -77,7 +75,7 @@ internal class UserWalletSaver @Inject constructor( selectWalletUseCase(userWallet.walletId).bind() } - router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + // router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { From 69aeaf5776f12b94a6f8f023bc37a249acd8b7ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:29:17 +0400 Subject: [PATCH 026/223] Updated on 2026-08-14 --- .../java/com/tangem/tap/DecomposeFragment.kt | 61 +++++++++++++++++++ .../tap/di/RootAppComponentContextModule.kt | 14 +---- .../com/tangem/tap/di/UiDependenciesModule.kt | 4 ++ ...vider.kt => ComposableContentComponent.kt} | 2 +- .../java/com/tangem/core/ui/UiDependencies.kt | 8 +++ .../core/ui/message/EventMessageHandler.kt | 2 + .../com/tangem/core/ui/res/TangemTheme.kt | 10 ++- .../tangem/core/ui/screen/ComposeScreen.kt | 1 + .../details/component/DetailsComponent.kt | 4 +- .../component/UserWalletListComponent.kt | 4 +- .../features/details/ui/DetailsScreen.kt | 8 +-- features/onboarding/build.gradle.kts | 1 + 12 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/DecomposeFragment.kt rename core/ui/src/main/java/com/tangem/core/ui/{ComposableContentProvider.kt => ComposableContentComponent.kt} (86%) diff --git a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt new file mode 100644 index 0000000000..d76bd6cd67 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt @@ -0,0 +1,61 @@ +package com.tangem.tap + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.Fragment +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.ComposableContentComponent +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.utils.Provider +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class DecomposeFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + private val component by lazy(mode = LazyThreadSafetyMode.NONE) { + requireNotNull(componentBuilder?.build()) { + "Component builder is not set, call newInstance() for DecomposeFragment creation first." + } + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + component.Content(modifier) + + EventMessageEffect( + messageHandler = uiDependencies.eventMessageHandler, + snackbarHostState = uiDependencies.globalSnackbarHostState, + ) + } + + private class ComponentBuilder>( + private val contextProvider: Provider, + private val params: P, + private val componentFactory: F, + ) { + + fun build(): C = componentFactory.create(contextProvider(), params) + } + + companion object { + + private var componentBuilder: ComponentBuilder<*, *, *>? = null + + fun > newInstance( + contextProvider: Provider, + params: P, + componentFactory: F, + ): Fragment { + this@Companion.componentBuilder = ComponentBuilder(contextProvider, params, componentFactory) + + return DecomposeFragment() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt index b23a41b8a8..1128b32708 100644 --- a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt +++ b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt @@ -7,8 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.decompose.ui.UiMessage -import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.core.ui.UiDependencies import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -16,7 +15,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped -import timber.log.Timber @Module @InstallIn(ActivityComponent::class) @@ -29,17 +27,11 @@ internal object RootAppComponentContextModule { @ActivityContext context: Context, dispatchers: CoroutineDispatcherProvider, componentBuilder: DecomposeComponent.Builder, + uiDependencies: UiDependencies, ): AppComponentContext { - // TODO: Implement message handler - val dummyMessageHandler = object : UiMessageHandler { - override fun handleMessage(message: UiMessage) { - Timber.w("Unable to handle message: $message") - } - } - return DefaultAppComponentContext( componentContext = (context as AppCompatActivity).defaultComponentContext(), - messageHandler = dummyMessageHandler, + messageHandler = uiDependencies.eventMessageHandler, dispatchers = dispatchers, hiltComponentBuilder = componentBuilder, ) diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt index 97b0f48bee..22a522e553 100644 --- a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di +import androidx.compose.material3.SnackbarHostState import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.Module import dagger.Provides @@ -19,6 +21,8 @@ internal object UiDependenciesModule { return object : UiDependencies { override val hapticManager = hapticManager override val appThemeModeHolder = appThemeModeHolder + override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState() + override val eventMessageHandler: EventMessageHandler = EventMessageHandler() } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt b/core/ui/src/main/java/com/tangem/core/ui/ComposableContentComponent.kt similarity index 86% rename from core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt rename to core/ui/src/main/java/com/tangem/core/ui/ComposableContentComponent.kt index 28868a7c18..933ae2fa9d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ComposableContentProvider.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ComposableContentComponent.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier @Stable -fun interface ComposableContentProvider { +fun interface ComposableContentComponent { @Composable @Suppress("TopLevelComposableFunctions") // TODO: Remove this check diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt index 62e19d4af6..49a90c5af6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -1,11 +1,19 @@ package com.tangem.core.ui +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Stable import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder +@Stable interface UiDependencies { val hapticManager: HapticManager val appThemeModeHolder: AppThemeModeHolder + + val globalSnackbarHostState: SnackbarHostState + + val eventMessageHandler: EventMessageHandler } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt index 0f4d117dab..782cd2a84f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.message +import androidx.compose.runtime.Stable import com.tangem.core.decompose.ui.UiMessage import com.tangem.core.decompose.ui.UiMessageHandler import com.tangem.core.ui.event.StateEvent @@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow /** * Message handler that is used to show or remove an [EventMessage] in the UI. */ +@Stable class EventMessageHandler( private val events: MutableStateFlow> = MutableStateFlow(consumedEvent()), ) : UiMessageHandler, StateFlow> by events { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 4f3287a86d..e5628c9bf5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -3,19 +3,18 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.MockHapticManager -// TODO: use isSystemInDarkTheme() for automatic color detection -internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false - @Composable fun TangemTheme( isDark: Boolean = false, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, hapticManager: HapticManager = MockHapticManager, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, content: @Composable () -> Unit, ) { val themeColors = if (isDark) darkThemeColors() else lightThemeColors() @@ -33,6 +32,7 @@ fun TangemTheme( LocalTangemShapes provides shapes, LocalIsInDarkTheme provides isDark, LocalHapticManager provides hapticManager, + LocalSnackbarHostState provides snackbarHostState, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -204,4 +204,8 @@ val LocalIsInDarkTheme = staticCompositionLocalOf { false } val LocalHapticManager = staticCompositionLocalOf { error("No HapticManager provided") +} + +val LocalSnackbarHostState = staticCompositionLocalOf { + error("No SnackbarHostState provided") } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index 4c2faae280..740eac38e1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -58,6 +58,7 @@ internal fun ComposeScreen.createComposeView(context: Context): ComposeView { TangemTheme( isDark = shouldUseDarkTheme(appThemeMode), hapticManager = uiDependencies.hapticManager, + snackbarHostState = uiDependencies.globalSnackbarHostState, ) { ScreenContent(modifier = screenModifier) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index 1f4026d32a..2aa9daebfe 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -2,10 +2,10 @@ package com.tangem.features.details.component import androidx.compose.material3.SnackbarHostState import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.ComposableContentProvider +import com.tangem.core.ui.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId -interface DetailsComponent : ComposableContentProvider { +interface DetailsComponent : ComposableContentComponent { val snackbarHostState: SnackbarHostState diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt index 8e7ffc063a..37791d71d0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt @@ -1,9 +1,9 @@ package com.tangem.features.details.component import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.ComposableContentProvider +import com.tangem.core.ui.ComposableContentComponent -interface UserWalletListComponent : ComposableContentProvider { +interface UserWalletListComponent : ComposableContentComponent { interface Factory { fun create(context: AppComponentContext): UserWalletListComponent diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index c0f66280bc..95413f3fc5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.ComposableContentProvider +import com.tangem.core.ui.ComposableContentComponent import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.res.TangemTheme @@ -36,7 +36,7 @@ private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f internal fun DetailsScreen( state: DetailsUM, snackbarHostState: SnackbarHostState, - userWalletListBlockContent: ComposableContentProvider, + userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { val backgroundColor = TangemTheme.colors.background.secondary @@ -118,7 +118,7 @@ private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, mo @Composable private fun Content( state: DetailsUM, - userWalletListBlockContent: ComposableContentProvider, + userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { LazyColumn( @@ -152,7 +152,7 @@ private fun Content( @Composable private fun Block( model: DetailsItemUM, - userWalletListBlockContent: ComposableContentProvider, + userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { Column( diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 6629611e63..9f081a463f 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { /** Compose libraries */ implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) implementation(deps.compose.ui) From e38a822f0bcffd72b139b7e2c8652811a07ba422 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 11:16:12 +0400 Subject: [PATCH 027/223] Updated on 2026-08-14 --- .../tap/common/finisher/AndroidAppFinisher.kt | 28 +++++++++++++++++++ .../java/com/tangem/tap/di/ActivityModule.kt | 10 +------ .../java/com/tangem/tap/di/UtilsModule.kt | 6 ++++ .../core/navigation/finisher/AppFinisher.kt | 8 ++++++ features/tester/impl/build.gradle.kts | 1 + .../feature/tester/ActivityClassWrapper.kt | 12 -------- .../apprestarter/DefaultAppRestarter.kt | 27 ------------------ .../feature/tester/di/RestarterModule.kt | 26 ----------------- .../tester/presentation/TesterActivity.kt | 6 ++-- .../viewmodels/FeatureTogglesViewModel.kt | 6 ++-- 10 files changed, 50 insertions(+), 80 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt delete mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt delete mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt delete mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt diff --git a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt new file mode 100644 index 0000000000..f0b9f5f66d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.common.finisher + +import android.content.Intent +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.tap.MainActivity +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal class AndroidAppFinisher : AppFinisher { + + override fun finish() { + foregroundActivityObserver.withForegroundActivity { activity -> + activity.finish() + } + } + + override fun restart() { + foregroundActivityObserver.withForegroundActivity { activity -> + activity.finish() + + val intent = Intent(activity, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + } + activity.startActivity(intent) + Runtime.getRuntime().exit(0) + } + } +} \ 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 cbf8eebb27..62f28c043e 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -6,10 +6,8 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.tap.MainActivity -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder import dagger.Module @@ -67,10 +65,4 @@ internal object ActivityModule { ): GetPolkadotCheckHasImmortalUseCase { return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository) } - - @Provides - @Singleton - fun provideActivityClassWrapper(): ActivityClassWrapper { - return ActivityClassWrapper(MainActivity::class.java) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index c8a58125d1..2fd4252342 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -1,9 +1,11 @@ package com.tangem.tap.di import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.feedback.ProxyFeedbackManager +import com.tangem.tap.common.finisher.AndroidAppFinisher import com.tangem.tap.common.share.IntentShareManager import com.tangem.tap.common.url.CustomTabsUrlOpener import dagger.Module @@ -27,4 +29,8 @@ internal object UtilsModule { @Provides @Singleton fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager() + + @Provides + @Singleton + fun provideAppFinisher(): AppFinisher = AndroidAppFinisher() } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt b/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt new file mode 100644 index 0000000000..54785819e1 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.finisher + +interface AppFinisher { + + fun finish() + + fun restart() +} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 86db03dca7..01480f6da6 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) /** Feature Apis */ implementation(projects.features.tester.api) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt deleted file mode 100644 index 22d2d4d3a9..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.feature.tester - -import android.app.Activity - -/** - * Wraps the main activity class to avoid type erasure issues during injection. - * - * @property clazz activity class - */ -class ActivityClassWrapper( - val clazz: Class, -) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt deleted file mode 100644 index 34cd927e71..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.tester.apprestarter - -import android.app.Activity -import android.content.Context -import android.content.Intent -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.features.tester.api.AppRestarter - -/** - * Entity that kills the process and restarts the main activity - * @property context Activity context - */ -internal class DefaultAppRestarter( - private val context: Context, - private val activityClassWrapper: ActivityClassWrapper, -) : AppRestarter { - - override fun restart() { - if (context !is Activity) return - - context.finish() - context.startActivity( - Intent(context, activityClassWrapper.clazz).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP }, - ) - Runtime.getRuntime().exit(0) - } -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt deleted file mode 100644 index 4891a60f12..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.feature.tester.di - -import android.content.Context -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.feature.tester.apprestarter.DefaultAppRestarter -import com.tangem.features.tester.api.AppRestarter -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 - -@Module -@InstallIn(ActivityComponent::class) -internal object RestarterModule { - - @Provides - @ActivityScoped - fun provideAppRestarter( - @ActivityContext context: Context, - activityClassWrapper: ActivityClassWrapper, - ): AppRestarter { - return DefaultAppRestarter(context, activityClassWrapper) - } -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index ef7ab21998..c94208933f 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -6,6 +6,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -18,7 +19,6 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen -import com.tangem.features.tester.api.AppRestarter import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -35,7 +35,7 @@ internal class TesterActivity : ComposeActivity() { lateinit var testerRouter: TesterRouter @Inject - lateinit var appRestarter: AppRestarter + lateinit var appFinisher: AppFinisher private val innerTesterRouter: InnerTesterRouter get() = requireNotNull(testerRouter as? InnerTesterRouter) { @@ -70,7 +70,7 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.FEATURE_TOGGLES.name) { val viewModel = hiltViewModel().apply { - setupInteractions(innerTesterRouter, appRestarter) + setupInteractions(innerTesterRouter, appFinisher) } FeatureTogglesScreen(state = viewModel.uiState) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index d3459ac31f..2d810e1ebf 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -7,10 +7,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter -import com.tangem.features.tester.api.AppRestarter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -40,10 +40,10 @@ internal class FeatureTogglesViewModel @Inject constructor( } /** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */ - fun setupInteractions(router: InnerTesterRouter, appRestarter: AppRestarter) { + fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) { uiState = uiState.copy( onBackClick = router::back, - onApplyChangesClick = appRestarter::restart, + onApplyChangesClick = appFinisher::restart, ) } From 8be6a6bf9dceb324b42dc7bf1117f9f24e10aa36 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 11:22:32 +0400 Subject: [PATCH 028/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/ApplicationEntryPoint.kt | 6 ++++++ app/src/main/java/com/tangem/tap/TangemApplication.kt | 10 +++++++++- .../java/com/tangem/tap/common/extensions/Store.kt | 10 ++++------ .../com/tangem/tap/common/share/IntentShareManager.kt | 4 +--- .../ui/RussianCardholdersWarningBottomSheetDialog.kt | 3 +-- .../com/tangem/tap/common/url/CustomTabsUrlOpener.kt | 4 +--- app/src/main/java/com/tangem/tap/di/UtilsModule.kt | 4 ++-- .../features/details/ui/details/DetailsViewModel.kt | 4 ++-- .../com/tangem/tap/proxy/redux/DaggerGraphState.kt | 6 +++++- 9 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index c86e0f9392..45dae620de 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -4,6 +4,8 @@ import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.connection.NetworkConnectionManager @@ -110,4 +112,8 @@ interface ApplicationEntryPoint { fun getDetailsFeatureToggles(): DetailsFeatureToggles fun getDetailsEntryPoint(): DetailsEntryPoint + + fun getUrlOpener(): UrlOpener + + fun getShareManager(): ShareManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 47aeee6e6f..414c48552b 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -181,7 +181,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase get() = entryPoint.getSaveBlockchainErrorUseCase() - // endregion private val detailsFeatureToggles: DetailsFeatureToggles get() = entryPoint.getDetailsFeatureToggles() @@ -189,6 +188,13 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val detailsEntryPoint: DetailsEntryPoint get() = entryPoint.getDetailsEntryPoint() + private val urlOpener + get() = entryPoint.getUrlOpener() + + private val shareManager + get() = entryPoint.getShareManager() + // endregion + override fun onCreate() { super.onCreate() @@ -276,6 +282,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, detailsFeatureToggles = detailsFeatureToggles, detailsEntryPoint = detailsEntryPoint, + urlOpener = urlOpener, + shareManager = shareManager, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 3d7a938cf3..6c7ab7e524 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -5,8 +5,6 @@ import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.common.share.shareManager -import com.tangem.tap.common.url.urlOpener import com.tangem.tap.domain.TapError import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope @@ -83,12 +81,12 @@ suspend fun dispatchOnMain(vararg actions: Action) { withMainContext { actions.forEach { store.dispatch(it) } } } -fun Store<*>.dispatchOpenUrl(url: String) { - urlOpener.openUrl(url) +fun Store.dispatchOpenUrl(url: String) { + inject(DaggerGraphState::urlOpener).openUrl(url) } -fun Store<*>.dispatchShare(url: String) { - shareManager.shareText(url) +fun Store.dispatchShare(url: String) { + inject(DaggerGraphState::shareManager).shareText(url) } inline fun Store.inject(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt index e681c2c36f..51b829de5f 100644 --- a/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt +++ b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt @@ -5,9 +5,7 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.withForegroundActivity -internal val shareManager = IntentShareManager - -internal object IntentShareManager : ShareManager { +internal class IntentShareManager : ShareManager { override fun shareText(text: String) { foregroundActivityObserver.withForegroundActivity { activity -> diff --git a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt index 05dd766362..5b0ac2e000 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt @@ -9,7 +9,6 @@ import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.common.url.urlOpener import com.tangem.tap.store import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding @@ -44,7 +43,7 @@ class RussianCardholdersWarningBottomSheetDialog( dismiss() } binding?.btnNo?.setOnClickListener { - urlOpener.openUrl(INSTRUCTION_URL) + store.dispatchOpenUrl(INSTRUCTION_URL) dismiss() } } diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index 72ec8c830c..549f514b01 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -15,9 +15,7 @@ import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R -internal val urlOpener = CustomTabsUrlOpener - -internal object CustomTabsUrlOpener : UrlOpener { +internal class CustomTabsUrlOpener : UrlOpener { override fun openUrl(url: String) { foregroundActivityObserver.withForegroundActivity { diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index 2fd4252342..fd11c7ef1a 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -20,11 +20,11 @@ internal object UtilsModule { @Provides @Singleton - fun provideShareManager(): ShareManager = IntentShareManager + fun provideShareManager(): ShareManager = IntentShareManager() @Provides @Singleton - fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener + fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener() @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index df60e9704e..e7ac985d9f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -18,11 +18,11 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.common.url.urlOpener import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction @@ -180,7 +180,7 @@ internal class DetailsViewModel( private fun handleSocialNetworkClick(link: SocialNetworkLink) { Analytics.send(Settings.ButtonSocialNetwork(link.network)) - urlOpener.openUrl(link.url) + store.dispatchOpenUrl(link.url) } private fun getSocialLinks(): ImmutableList { 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 3dc61535c0..08d6c0997f 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 @@ -3,6 +3,8 @@ package com.tangem.tap.proxy.redux import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.core.navigation.email.EmailSender +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository @@ -34,8 +36,8 @@ import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder @@ -81,4 +83,6 @@ data class DaggerGraphState( val detailsFeatureToggles: DetailsFeatureToggles? = null, val detailsEntryPoint: DetailsEntryPoint? = null, val stakingRouter: StakingRouter? = null, + val urlOpener: UrlOpener? = null, + val shareManager: ShareManager? = null, ) : StateType \ No newline at end of file From 01c263e4e18df17e7d4f90f2d5c6d269df24c4f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 11:22:01 +0400 Subject: [PATCH 029/223] Updated on 2026-08-14 --- .../decompose/navigation/DefaultRouter.kt | 20 +++++++------- .../core/decompose/navigation/DummyRouter.kt | 8 +++--- .../core/decompose/navigation/Router.kt | 17 ++++++------ .../tangem/features/details/DetailsRouter.kt | 2 +- .../preview/PreviewDetailsComponent.kt | 5 ++-- .../component/preview/PreviewRouter.kt | 27 ------------------- 6 files changed, 28 insertions(+), 51 deletions(-) delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt index 78e7d91727..86c43706a6 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt @@ -20,13 +20,22 @@ internal class DefaultRouter( navigation.pushNew(route, onComplete) } + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + val newRoutes = routes.toList() + + navigation.navigate( + transformer = { newRoutes }, + onComplete = { newStack, _ -> onComplete(newStack.size == newRoutes.size) }, + ) + } + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { navigation.pop(onComplete) } - override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { navigation.popWhile( - predicate = { it::class != routeClass }, + predicate = { it != route }, onComplete = onComplete, ) } @@ -37,11 +46,4 @@ internal class DefaultRouter( onComplete = onComplete, ) } - - override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { - navigation.popWhile( - predicate = { true }, - onComplete = onComplete, - ) - } } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt index a6352229b0..77e7a8f59d 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt @@ -8,6 +8,10 @@ class DummyRouter : Router { onComplete(true) } + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { onComplete(true) } @@ -19,8 +23,4 @@ class DummyRouter : Router { override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { onComplete(true) } - - override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { - onComplete(true) - } } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt index f2ad82f9ea..da7f859b8f 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt @@ -4,7 +4,7 @@ import kotlin.reflect.KClass /** * Interface for a router in the application. - * It provides methods for navigating through the application. + * It provides methods for navigating through the application by stack. */ interface Router { @@ -16,6 +16,14 @@ interface Router { */ fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** + * Replaces ***all*** routes in the navigation stack with the specified [routes]. + * + * @param routes The routes to replace the current stack with. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** * Pops the top route from the navigation stack. * @@ -38,11 +46,4 @@ interface Router { * @param onComplete The callback to be invoked when the operation is complete. */ fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) - - /** - * Pops all routes from the navigation stack. - * - * @param onComplete The callback to be invoked when the operation is complete. - */ - fun clear(onComplete: (isSuccess: Boolean) -> Unit = {}) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt index a7cb49988e..93e60c90ef 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -55,7 +55,7 @@ internal class DetailsRouter @Inject constructor( TODO("This class will be removed in future") } - override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { TODO("This class will be removed in future") } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 3981734a85..d91ffd5ce1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -3,6 +3,7 @@ package com.tangem.features.details.component.preview import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.decompose.navigation.DummyRouter import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -17,12 +18,12 @@ internal class PreviewDetailsComponent : DetailsComponent { private val previewBlocks = runBlocking { ItemsBuilder( - router = PreviewRouter(), + router = DummyRouter(), ).buldAll(isWalletConnectAvailable = true) } private val previewFooter = DetailsFooterUM( - socials = SocialsBuilder(PreviewRouter()).buildAll(), + socials = SocialsBuilder(DummyRouter()).buildAll(), appVersion = "1.0.0-preview", ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt deleted file mode 100644 index adfd6a5f4e..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.features.details.component.preview - -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.decompose.navigation.Router -import kotlin.reflect.KClass - -internal class PreviewRouter : Router { - override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun clear(onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } -} \ No newline at end of file From 3394ed155257990374fe11ae11ed457fd6085caa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 11:41:55 +0400 Subject: [PATCH 030/223] Updated on 2026-08-14 --- .../tap/common/finisher/AndroidAppFinisher.kt | 16 +++++++--------- .../main/java/com/tangem/tap/di/UtilsModule.kt | 4 +++- .../tangem/features/tester/api/AppRestarter.kt | 9 --------- 3 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt diff --git a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt index f0b9f5f66d..dc15eb3e95 100644 --- a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt +++ b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt @@ -1,12 +1,15 @@ package com.tangem.tap.common.finisher +import android.content.Context import android.content.Intent import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.tap.MainActivity import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.withForegroundActivity -internal class AndroidAppFinisher : AppFinisher { +internal class AndroidAppFinisher( + private val appContext: Context, +) : AppFinisher { override fun finish() { foregroundActivityObserver.withForegroundActivity { activity -> @@ -15,14 +18,9 @@ internal class AndroidAppFinisher : AppFinisher { } override fun restart() { - foregroundActivityObserver.withForegroundActivity { activity -> - activity.finish() - - val intent = Intent(activity, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - } - activity.startActivity(intent) - Runtime.getRuntime().exit(0) + val intent = Intent(appContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } + appContext.startActivity(intent) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index fd11c7ef1a..d5565743e9 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di +import android.content.Context import com.tangem.core.navigation.feedback.FeedbackManager import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.share.ShareManager @@ -11,6 +12,7 @@ import com.tangem.tap.common.url.CustomTabsUrlOpener import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -32,5 +34,5 @@ internal object UtilsModule { @Provides @Singleton - fun provideAppFinisher(): AppFinisher = AndroidAppFinisher() + fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context) } \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt deleted file mode 100644 index a64be25542..0000000000 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.tester.api - -/** - * Interface for app restarter - */ -interface AppRestarter { - - fun restart() -} \ No newline at end of file From 5f68c41da2f9ae83d76a15a5a64271315c9dfb4e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 15:06:29 +0300 Subject: [PATCH 031/223] Updated on 2026-08-14 --- .../impl/presentation/StakingFragment.kt | 5 + .../state/StakingStateController.kt | 2 +- .../presentation/state/StakingStateRouter.kt | 42 +++---- .../impl/presentation/state/StakingUiState.kt | 6 +- .../state/stub/StakingClickIntentsStub.kt | 4 + .../SetInitialDataStateTransformer.kt | 4 +- .../presentation/ui/StakingAmountContent.kt | 11 ++ .../ui/StakingInitialInfoContent.kt | 19 ++- .../ui/StakingNavigationButtons.kt | 119 ++++++++++++++++++ .../impl/presentation/ui/StakingScreen.kt | 37 +++--- .../ui/StakingValidatorAndFeeContent.kt | 11 ++ .../viewmodel/StakingClickIntents.kt | 4 + .../viewmodel/StakingViewModel.kt | 8 ++ 13 files changed, 216 insertions(+), 56 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index e96be559de..9648e0164d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter import com.tangem.features.staking.impl.presentation.ui.StakingScreen import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel @@ -31,6 +32,9 @@ internal class StakingFragment : ComposeFragment() { @Inject lateinit var router: StakingRouter + @Inject + lateinit var stateController: StakingStateController + @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @@ -48,6 +52,7 @@ internal class StakingFragment : ComposeFragment() { innerStakingRouter, StakingStateRouter( fragmentManager = WeakReference(parentFragmentManager), + stateController = stateController, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 9fdd84f082..fc51c5f7e7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -35,7 +35,7 @@ internal class StakingStateController @Inject constructor() { return StakingUiState( clickIntents = StakingClickIntentsStub, cryptoCurrencyName = "", - currentScreen = StakingUiStateType.InitialInfo, + currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = StakingStates.AmountState.Empty(), validatorState = StakingStates.ValidatorState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 8e7182f088..bc00ead8ca 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,22 +1,15 @@ package com.tangem.features.staking.impl.presentation.state import androidx.fragment.app.FragmentManager -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import java.lang.ref.WeakReference internal class StakingStateRouter( private val fragmentManager: WeakReference, + private val stateController: StakingStateController, ) { - private var mutableCurrentState: MutableStateFlow = MutableStateFlow(getInitState()) - - val currentState: StateFlow - get() = mutableCurrentState.asStateFlow() - fun clear() { - mutableCurrentState.update { getInitState() } + stateController.update { it.copy(currentStep = getInitialState()) } } fun popBackStack() { @@ -24,49 +17,48 @@ internal class StakingStateRouter( } fun onBackClick(isSuccess: Boolean = false) { - val type = currentState.value + val type = stateController.uiState.value.currentStep when { isSuccess -> popBackStack() else -> when (type) { - StakingUiStateType.Amount -> showInitial() - StakingUiStateType.ValidatorAndFee -> showAmount() + StakingStep.Amount -> showInitial() + StakingStep.ValidatorAndFee -> showAmount() else -> popBackStack() } } } fun onNextClick() { - when (currentState.value) { - StakingUiStateType.InitialInfo -> showAmount() - StakingUiStateType.Amount, - StakingUiStateType.ValidatorAndFee, - -> showConfirm() - StakingUiStateType.Confirm -> onBackClick() + when (stateController.uiState.value.currentStep) { + StakingStep.InitialInfo -> showAmount() + StakingStep.Amount -> showValidator() + StakingStep.ValidatorAndFee -> showConfirm() + StakingStep.Confirm -> onBackClick() } } fun onPrevClick() { - when (currentState.value) { - StakingUiStateType.Amount -> showInitial() + when (stateController.uiState.value.currentStep) { + StakingStep.Amount -> showInitial() else -> popBackStack() } } private fun showInitial() { - mutableCurrentState.update { StakingUiStateType.InitialInfo } + stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } fun showAmount() { - mutableCurrentState.update { StakingUiStateType.Amount } + stateController.update { it.copy(currentStep = StakingStep.Amount) } } fun showValidator() { - mutableCurrentState.update { StakingUiStateType.ValidatorAndFee } + stateController.update { it.copy(currentStep = StakingStep.ValidatorAndFee) } } fun showConfirm() { - mutableCurrentState.update { StakingUiStateType.Confirm } + stateController.update { it.copy(currentStep = StakingStep.Confirm) } } - private fun getInitState() = StakingUiStateType.InitialInfo + private fun getInitialState() = StakingStep.InitialInfo } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 3852cd6ed2..1253b63881 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -12,7 +12,7 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten internal data class StakingUiState( val clickIntents: StakingClickIntents, val cryptoCurrencyName: String, - val currentScreen: StakingUiStateType, + val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: StakingStates.AmountState, val validatorState: StakingStates.ValidatorState, @@ -94,6 +94,8 @@ internal sealed class StakingStates { sealed class ConfirmStakingState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val isSuccess: Boolean, + val isStaking: Boolean, ) : ConfirmStakingState() data class Empty( @@ -102,7 +104,7 @@ internal sealed class StakingStates { } } -enum class StakingUiStateType { +enum class StakingStep { InitialInfo, Amount, ValidatorAndFee, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 16a8763435..b09e5d9f4c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -5,4 +5,8 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} + + override fun onNextClick() {} + + override fun onPrevClick() {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 3ee1b970cd..e16098575b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -10,7 +10,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import java.math.BigDecimal @@ -24,7 +24,7 @@ internal class SetInitialDataStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( clickIntents = clickIntents, - currentScreen = StakingUiStateType.InitialInfo, + currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt new file mode 100644 index 0000000000..e1324855c8 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.runtime.Composable +import com.tangem.features.staking.impl.presentation.state.StakingStates + +@Composable +internal fun StakingAmountContent(state: StakingStates.AmountState) { + if (state !is StakingStates.AmountState.Data) return + + // TODO staking +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 7ff32c89e2..97491c4a36 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -3,9 +3,11 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.extensions.resolveReference @@ -32,19 +33,13 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { Column( modifier = Modifier // Do not put fillMaxSize() in here .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16), + .padding(TangemTheme.dimens.spacing16) + .verticalScroll(rememberScrollState()), ) { MetricsBlock(state) Spacer(modifier = Modifier.height(8.dp)) StakingDetailsRows(state) Spacer(modifier = Modifier.height(8.dp)) - PrimaryButton( - text = stringResource(id = R.string.common_stake), - modifier = Modifier.fillMaxWidth(), - onClick = { - // TODO staking - }, - ) } } @@ -185,7 +180,7 @@ private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvide override val values: Sequence get() = sequenceOf( StakingStates.InitialInfoState.Data( - isPrimaryButtonEnabled = false, + isPrimaryButtonEnabled = true, available = "15 SOL", onStake = "0 SOL", aprRange = stringReference("2.54-5.12%"), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt new file mode 100644 index 0000000000..aaceadf9c6 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -0,0 +1,119 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.StakingStep + +@Composable +internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + StakingNavigationButton( + uiState = uiState, + modifier = Modifier, + ) + } +} + +@Composable +private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + val confirmState = uiState.confirmStakingState + val isSuccess = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isSuccess ?: false + val isStaking = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isStaking ?: false + + val isButtonsVisible = uiState.currentStep != StakingStep.Confirm + val isStakingState = uiState.currentStep == StakingStep.Confirm && !isSuccess && !isStaking + + val (buttonTextId, buttonClick) = getButtonData( + currentState = uiState, + ) + val isButtonEnabled = isButtonEnabled(uiState) + val buttonIcon = if (isStakingState) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + } + + Row(modifier = modifier) { + AnimatedVisibility( + visible = isButtonsVisible, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + Row { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { uiState.clickIntents.onPrevClick() } + .padding(TangemTheme.dimens.spacing12), + ) + SpacerW12() + } + } + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled, + onClick = { + if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + buttonClick() + }, + showProgress = false, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } +} + +private fun getButtonData(currentState: StakingUiState): Pair Unit> { + return when (currentState.currentStep) { + StakingStep.InitialInfo, + StakingStep.Amount, + StakingStep.ValidatorAndFee, + StakingStep.Confirm, + -> R.string.common_next to { currentState.clickIntents.onNextClick() } + } +} + +private fun isButtonEnabled(uiState: StakingUiState): Boolean { + return when (uiState.currentStep) { + StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled + StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled + StakingStep.ValidatorAndFee -> uiState.validatorState.isPrimaryButtonEnabled + StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index c1f1bf5a95..6a0d321903 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -16,7 +16,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.StakingUiStateType +import com.tangem.features.staking.impl.presentation.state.StakingStep import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -40,25 +40,28 @@ internal fun StakingScreen(uiState: StakingUiState) { uiState = uiState, modifier = Modifier.weight(1f), ) + StakingNavigationButtons( + uiState = uiState, + ) } } @Composable private fun SendAppBar(uiState: StakingUiState) { - val titleRes = when (uiState.currentScreen) { - StakingUiStateType.InitialInfo -> stringResource(id = R.string.common_stake) - StakingUiStateType.Amount -> stringResource(id = R.string.send_amount_label) - StakingUiStateType.ValidatorAndFee -> stringResource(id = R.string.common_stake) - StakingUiStateType.Confirm -> "" + val titleRes = when (uiState.currentStep) { + StakingStep.InitialInfo -> stringResource(id = R.string.common_stake) + StakingStep.Amount -> stringResource(id = R.string.send_amount_label) + StakingStep.ValidatorAndFee -> stringResource(id = R.string.common_stake) + StakingStep.Confirm -> "" } - val backIcon = when (uiState.currentScreen) { - StakingUiStateType.Amount, - StakingUiStateType.ValidatorAndFee, - StakingUiStateType.Confirm, + val backIcon = when (uiState.currentStep) { + StakingStep.Amount, + StakingStep.ValidatorAndFee, + StakingStep.Confirm, -> { R.drawable.ic_close_24 } - StakingUiStateType.InitialInfo -> { + StakingStep.InitialInfo -> { R.drawable.ic_back_24 } } @@ -74,7 +77,7 @@ private fun SendAppBar(uiState: StakingUiState) { @OptIn(ExperimentalAnimationApi::class) @Composable private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { - val currentScreen = uiState.currentScreen + val currentScreen = uiState.currentStep var currentStateProxy by remember { mutableStateOf(currentScreen) } var isTransitionAnimationRunning by remember { mutableStateOf(false) } @@ -115,10 +118,16 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M isTransitionAnimationRunning = transition.targetState != transition.currentState when (state) { - StakingUiStateType.InitialInfo -> StakingInitialInfoContent( + StakingStep.InitialInfo -> StakingInitialInfoContent( state = uiState.initialInfoState, ) - else -> Unit + StakingStep.Amount -> StakingAmountContent( + state = uiState.amountState, + ) + StakingStep.ValidatorAndFee -> StakingValidatorAndFeeContent( + state = uiState.validatorState, + ) + else -> TODO() } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt new file mode 100644 index 0000000000..a73d0000d4 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.runtime.Composable +import com.tangem.features.staking.impl.presentation.state.StakingStates + +@Composable +internal fun StakingValidatorAndFeeContent(state: StakingStates.ValidatorState) { + if (state !is StakingStates.ValidatorState.Data) return + + // TODO staking +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 30f890ae97..8176c2ea6c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -3,4 +3,8 @@ package com.tangem.features.staking.impl.presentation.viewmodel internal interface StakingClickIntents { fun onBackClick() + + fun onNextClick() + + fun onPrevClick() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 2c94bdea46..22c0686b9c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -63,6 +63,14 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } + override fun onNextClick() { + stakingStateRouter.onNextClick() + } + + override fun onPrevClick() { + stakingStateRouter.onBackClick() + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter From 60bdbc33abeb9b52c220f8e549c466d9038c2bec Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 16:28:22 +0300 Subject: [PATCH 032/223] Updated on 2026-08-14 --- common/ui-charts/build.gradle.kts | 1 + .../tangem/common/ui/charts/MarketChart.kt | 402 +++++++++++++++++- .../ui/charts/layer/MarketChartLayer.kt | 168 +++++--- .../common/ui/charts/marker/ChartMarker.kt | 23 +- .../preview/MarketChartPreviewDataProvider.kt | 152 +++++++ .../common/ui/charts/preview/PreviewData.kt | 77 ---- .../ui/charts/state/AxisLabelFormatter.kt | 17 + .../common/ui/charts/state/MarketChartData.kt | 36 ++ .../charts/state/MarketChartDataProducer.kt | 207 +++++++++ .../common/ui/charts/state/MarketChartLook.kt | 28 ++ .../ui/charts/state/MarketChartState.kt | 151 +++++++ core/ui/build.gradle.kts | 2 +- gradle/dependencies.toml | 2 +- settings.gradle.kts | 2 +- 14 files changed, 1124 insertions(+), 144 deletions(-) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt delete mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts index 62c6c0de87..319c15eebc 100644 --- a/common/ui-charts/build.gradle.kts +++ b/common/ui-charts/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(deps.tangem.vico.compose) implementation(deps.tangem.vico.compose.m3) + implementation(deps.lifecycle.compose) implementation(deps.compose.foundation) implementation(deps.compose.material3) implementation(deps.compose.ui.tooling) diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 31f0f55f2b..9bbaeaa48b 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -1,8 +1,402 @@ package com.tangem.common.ui.charts -import androidx.compose.runtime.Composable +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontSynthesis +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.resolveAsTypeface +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis +import com.patrykandpatrick.vico.compose.common.of +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.axis.* +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.component.LineComponent +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.common.ui.charts.layer.rememberMarketChartLayer +import com.tangem.common.ui.charts.marker.rememberTangemChartMarker +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider +import com.tangem.common.ui.charts.state.* +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.math.RoundingMode + +private const val GUIDELINES_COUNT = 3 + +/** + * MarketChart ui component for representing coin prices. + * + * @param modifier The modifier to be applied to the chart. + * @param state The state of the Market Chart, which includes data and look of the chart. + * @param splitChartSegmentColor The color of the grayed by marker chart segment. + * @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor] + * @param backgroundColorAlpha The alpha of the background color of the chart. + * @param noChartContent A composable function that defines the content to be displayed when there is no data to display. + */ +@Composable +fun MarketChart( + modifier: Modifier = Modifier, + state: MarketChartState = rememberMarketChartState(), + splitChartSegmentColor: Color, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + noChartContent: @Composable BoxScope.() -> Unit, +) { + var canvasWidth by remember { mutableIntStateOf(0) } + var canvasHeight by remember { mutableIntStateOf(0) } + + val layer = rememberLayerFromState( + state = state, + splitChartSegmentColor = splitChartSegmentColor, + backgroundColorAlpha = backgroundColorAlpha, + backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha, + canvasHeight = canvasHeight, + ) + val chart = rememberCartesianChart( + layer, + startAxis = rememberMarketChartStartAxis( + yValueFormatter = state.yValueFormatter, + ), + bottomAxis = rememberMarketChartBottomAxis( + xValueFormatter = state.xValueFormatter, + ), + ) + val marker = rememberTangemChartMarker( + color = state.chartColor, + innerCircleColor = Color.White, + ) + val density = LocalDensity.current + + CartesianChartHost( + modifier = modifier.onGloballyPositioned { + with(density) { + canvasWidth = it.size.width + canvasHeight = if (it.size.height != 0) { + // FIXME get height bounded to min max chart points + it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt() + } else { + 0 + } + } + }, + chart = chart, + modelProducer = state.modelProducer, + scrollState = rememberVicoScrollState(scrollEnabled = false), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth), + diffAnimationSpec = null, + marker = marker, + placeholder = noChartContent, + ) +} @Composable -fun MarketChart() { - TODO() -} \ No newline at end of file +private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener { + val state = this + return remember(state.markerVisibilityListener, canvasWidth) { + val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f } + + object : CartesianMarkerVisibilityListener { + override fun onShown(marker: CartesianMarker, targets: List) { + state.stopDrawingAnimation() + val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX + + state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } + state.markerVisibilityListener.onShown(marker, targets) + } + + override fun onHidden(marker: CartesianMarker) { + state.markerFraction = null + state.markerVisibilityListener.onHidden(marker) + } + + override fun onUpdated(marker: CartesianMarker, targets: List) { + val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX + + state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } + state.markerVisibilityListener.onUpdated(marker, targets) + } + } + } +} + +@Composable +private fun rememberLayerFromState( + state: MarketChartState, + splitChartSegmentColor: Color, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, + canvasHeight: Int, +): LineCartesianLayer { + return rememberMarketChartLayer( + lineColor = state.chartColor, + backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha), + secondLineColor = splitChartSegmentColor, + backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha), + secondColorOnTheRightSide = state.markerHighlightRightSide.not(), + startDrawingAnimation = state.startDrawingAnimationState, + markerFraction = state.markerFraction, + axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ? + canvasHeight = canvasHeight, + ) +} + +@Composable +private fun rememberMarketChartStartAxis( + yValueFormatter: CartesianValueFormatter, +): VerticalAxis { + return rememberCustomStartAxis( + axis = null, + tick = null, + guideline = null, + labelGuideline = rememberChartAxisGuidelineComponent( + color = TangemTheme.colors.icon.inactive.copy(alpha = 0.12f), + ), + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + background = null, + padding = Dimensions.of( + start = TangemTheme.dimens.spacing4, + end = TangemTheme.dimens.spacing4, + ), + textSize = TangemTheme.typography.caption2.fontSize, + typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + ), + horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside, + verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center, + itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false), + valueFormatter = yValueFormatter, + ) +} + +@Composable +fun rememberMarketChartBottomAxis( + xValueFormatter: CartesianValueFormatter, +): HorizontalAxis { + return rememberBottomAxis( + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + textSize = TangemTheme.typography.caption2.fontSize, + padding = Dimensions.of(top = TangemTheme.dimens.spacing20), + typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + ), + tick = null, + axis = null, + guideline = null, + sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ? + itemPlacer = remember { + AxisItemPlacer.Horizontal.default( + spacing = 25, // FIXME ? + offset = 60, // FIXME ? + shiftExtremeTicks = false, + addExtremeLabelPadding = false, + ) + }, + valueFormatter = xValueFormatter, + ) +} + +@Composable +private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent { + return rememberAxisGuidelineComponent( + color = color, + shape = Shape.Rectangle, + margins = Dimensions( + startDp = TangemTheme.dimens.spacing4.value, + endDp = TangemTheme.dimens.spacing4.value, + topDp = 0f, + bottomDp = 0f, + ), + thickness = TangemTheme.dimens.size2, + ) +} + +@Composable +internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface { + val resolver = LocalFontFamilyResolver.current + return remember(resolver, this) { + resolver.resolveAsTypeface( + fontFamily = this.fontFamily, + fontWeight = this.fontWeight ?: FontWeight.Normal, + fontStyle = this.fontStyle ?: FontStyle.Normal, + fontSynthesis = this.fontSynthesis ?: FontSynthesis.All, + ) + }.value +} + +// region Preview + +@Suppress("LongMethod") +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun MarketChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val y = previewData.second + val x = previewData.first + + val dataProducer = remember { + MarketChartDataProducer.build { + chartLook = MarketChartLook( + type = MarketChartLook.Type.Growing, + markerHighlightRightSide = true, + animationOnDataChange = true, + ) + } + } + + LaunchedEffect(key1 = Unit) { + dataProducer.runTransactionSuspend { + chartData = MarketChartData.Data( + x = x, + y = y, + ) + updateLook { + it.copy( + xAxisFormatter = { value -> + value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) + }, + yAxisFormatter = { value -> + value.setScale(3, RoundingMode.HALF_UP).toPlainString() + }, + ) + } + } + } + var markerPoint by remember { + mutableStateOf(Pair(null, null)) + } + + val coroutineScope = rememberCoroutineScope() + val look by dataProducer.lookState.collectAsState() + + TangemThemePreview { + val growingColor = TangemTheme.colors.icon.accent + val fallingColor = TangemTheme.colors.icon.warning + + val chartState = rememberMarketChartState( + dataProducer = dataProducer, + onMarkerShown = { x, y -> + markerPoint = Pair(x, y) + }, + colorMapper = { + when (it) { + MarketChartLook.Type.Growing -> growingColor + MarketChartLook.Type.Falling -> fallingColor + } + }, + ) + + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) { + Text(text = "Point: ${markerPoint.first}, ${markerPoint.second}") + + MarketChart( + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary) + .height(173.dp), + state = chartState, + splitChartSegmentColor = TangemTheme.colors.icon.inactive, + backgroundSplitChartSegmentColorAlpha = 0.24f, + backgroundColorAlpha = 0.24f, + noChartContent = { }, + ) + SpacerH16() + + Button(onClick = { chartState.startDrawingAnimation() }) { + Text("Start drawing animation") + } + Button( + onClick = { + dataProducer.runTransaction { + updateLook { + it.copy(markerHighlightRightSide = !it.markerHighlightRightSide) + } + } + }, + ) { + Text( + text = "Change marker highlight side", + ) + } + Button(onClick = { + coroutineScope.launch { + dataProducer.runTransactionSuspend { + updateData { + MarketChartData.Data( + x = it.x, + y = it.y.reversed(), + ) + } + } + } + },) { + Text("Change Data") + } + Button(onClick = { + dataProducer.runTransaction { + updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) } + } + },) { + Text("Change animationOnDataChange = ${look.animationOnDataChange}") + } + + Button(onClick = { + dataProducer.runTransaction { + updateLook { + it.copy( + type = if (it.type == MarketChartLook.Type.Growing) { + MarketChartLook.Type.Falling + } else { + MarketChartLook.Type.Growing + }, + ) + } + } + },) { + Text("Change color type") + } + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt index 239f0f0665..b224a1838d 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.fullWidth @@ -27,13 +28,15 @@ import com.patrykandpatrick.vico.compose.common.shader.BrushShader import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Zoom import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.shader.ColorShader import com.patrykandpatrick.vico.core.common.shader.DynamicShader -import com.tangem.common.ui.charts.preview.getPreviewBigCartData +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import java.math.BigDecimal /** * Creates and remembers a LineCartesianLayer for a chart with specific characteristics. @@ -51,19 +54,21 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Suppress("LongParameterList") @Composable -fun rememberMarketChartLayer( +internal fun rememberMarketChartLayer( lineColor: Color, backgroundLineColor: Color, secondLineColor: Color, backgroundSecondLineColor: Color, startDrawingAnimation: MutableState, axisValueOverrider: AxisValueOverrider, - secondColorOnTheRightSide: Boolean = false, - @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, + secondColorOnTheRightSide: Boolean, + @FloatRange(from = 0.0, to = 1.0) markerFraction: Float?, + canvasHeight: Int, ): LineCartesianLayer { var animationFraction: Float? by remember { mutableStateOf(null) } LaunchedEffect(startDrawingAnimation.value) { + animationFraction = null if (startDrawingAnimation.value) { animate( initialValue = 0f, @@ -89,9 +94,11 @@ fun rememberMarketChartLayer( secondColorOnTheRightSide = secondColorOnTheRightSide, markerFraction = markerFraction, animationFraction = animationFraction, + canvasHeight = canvasHeight, ) } +@Suppress("LongParameterList") @Composable private fun rememberRawMarketChartLayer( lineColor: Color, @@ -99,6 +106,7 @@ private fun rememberRawMarketChartLayer( secondLineColor: Color, backgroundSecondLineColor: Color, axisValueOverrider: AxisValueOverrider, + canvasHeight: Int, secondColorOnTheRightSide: Boolean = false, @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, @FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null, @@ -109,38 +117,36 @@ private fun rememberRawMarketChartLayer( val markerSet = markerFraction != null val animationRunning = animationFraction != null && animationFraction != 1f - val layerColors = remember(animationRunning, markerSet, secondColorOnTheRightSide) { - when { - !animationRunning && markerSet && secondColorOnTheRightSide -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - lineColorRight = secondLineColor, - backLineColorRight = backgroundSecondLineColorGradient, - ) - } - !animationRunning && markerSet && !secondColorOnTheRightSide -> { - LayerColors( - lineColor = secondLineColor, - backLineColor = backgroundSecondLineColorGradient, - lineColorRight = lineColor, - backLineColorRight = backgroundColorLineGradient, - ) - } - animationRunning -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - lineColorRight = Color.Transparent, - backLineColorRight = listOf(Color.Transparent, Color.Transparent), - ) - } - else -> { - LayerColors( - lineColor = lineColor, - backLineColor = backgroundColorLineGradient, - ) - } + val layerColors = when { + !animationRunning && markerSet && secondColorOnTheRightSide -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = secondLineColor, + backLineColorRight = backgroundSecondLineColorGradient, + ) + } + !animationRunning && markerSet && !secondColorOnTheRightSide -> { + LayerColors( + lineColor = secondLineColor, + backLineColor = backgroundSecondLineColorGradient, + lineColorRight = lineColor, + backLineColorRight = backgroundColorLineGradient, + ) + } + animationRunning -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = Color.Transparent, + backLineColorRight = listOf(Color.Transparent, Color.Transparent), + ) + } + else -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + ) } } @@ -148,6 +154,7 @@ private fun rememberRawMarketChartLayer( fractionValue = animationFraction ?: markerFraction, axisValueOverrider = axisValueOverrider, layerColors = layerColors, + canvasHeight = canvasHeight, ) } @@ -163,14 +170,26 @@ private fun rememberLayer( fractionValue: Float?, axisValueOverrider: AxisValueOverrider, layerColors: LayerColors, + canvasHeight: Int, ): LineCartesianLayer { + val endGradientColorPosition = if (canvasHeight != 0) { + canvasHeight * END_GRADIENT_COLOR_POSITION_PERCENTAGE + } else { + Float.POSITIVE_INFINITY + } + return rememberLineCartesianLayer( listOf( if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) { rememberLineSpec( shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) }, - backgroundShader = remember(layerColors.backLineColor) { - BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor)) + backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColor, + endY = endGradientColorPosition, + ), + ) }, ) } else { @@ -181,11 +200,21 @@ private fun rememberLayer( positions = floatArrayOf(fractionValue, fractionValue), ) }, - backgroundShaderFirst = remember(layerColors.backLineColor) { - BrushShader(brush = Brush.verticalGradient(layerColors.backLineColor)) + backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColor, + endY = endGradientColorPosition, + ), + ) }, - backgroundShaderSecond = remember(layerColors.backLineColorRight) { - BrushShader(brush = Brush.verticalGradient(layerColors.backLineColorRight)) + backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColorRight, + endY = endGradientColorPosition, + ), + ) }, xSplitFraction = fractionValue, ) @@ -195,14 +224,23 @@ private fun rememberLayer( ) } +private const val END_GRADIENT_COLOR_POSITION_PERCENTAGE = 0.9f + // region Preview +@Suppress("LongMethod") @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun LayerChartPreview() { - val xListKey = ExtraStore.Key>() - val model = getPreviewBigCartData(xListKey) +private fun LayerChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val y = previewData.second.map { it.toFloat() } + val x = List(y.size) { it.toFloat() } + val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) + var lineColor by remember { + mutableStateOf(Color.Blue) + } TangemThemePreview { Column( @@ -213,12 +251,13 @@ private fun LayerChartPreview() { modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( rememberRawMarketChartLayer( - lineColor = Color.Blue, - backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), secondColorOnTheRightSide = true, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, ), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), @@ -230,13 +269,14 @@ private fun LayerChartPreview() { modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( rememberRawMarketChartLayer( - lineColor = Color.Blue, - backgroundLineColor = Color.Blue.copy(alpha = 0.24f), + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), secondLineColor = Color.Gray, backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), markerFraction = 0.35f, secondColorOnTheRightSide = true, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, ), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), @@ -248,14 +288,34 @@ private fun LayerChartPreview() { modifier = Modifier.fillMaxWidth(), chart = rememberCartesianChart( rememberRawMarketChartLayer( - lineColor = Color.Blue, - backgroundLineColor = Color.Blue.copy(alpha = 0.24f), - secondLineColor = Color.Blue, - backgroundSecondLineColor = Color.Blue.copy(alpha = 0.24f), + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = false, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = lineColor, + backgroundSecondLineColor = lineColor.copy(alpha = 0.24f), markerFraction = 0.35f, secondColorOnTheRightSide = true, animationFraction = 0.7f, axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, ), ), zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt index 4675085e2a..e27ce63a70 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.remember 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.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost import com.patrykandpatrick.vico.compose.cartesian.fullWidth @@ -24,16 +25,18 @@ import com.patrykandpatrick.vico.compose.common.shader.color import com.patrykandpatrick.vico.compose.common.shape.dashed import com.patrykandpatrick.vico.core.cartesian.* import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker import com.patrykandpatrick.vico.core.common.Dimensions import com.patrykandpatrick.vico.core.common.component.TextComponent -import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.shader.DynamicShader import com.patrykandpatrick.vico.core.common.shape.Shape -import com.tangem.common.ui.charts.preview.getPreviewBigCartData +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import java.math.BigDecimal /** * @param color The color of the indicator and guideline. @@ -53,7 +56,11 @@ internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): C ) val indicatorRearComponent = rememberShapeComponent( shape = Shape.Pill, - color = color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA), + color = if (color == Color.Transparent) { + Color.Transparent + } else { + color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA) + }, ) val indicator = rememberLayeredComponent( rear = indicatorRearComponent, @@ -105,10 +112,14 @@ private const val INDICATOR_REAR_COLOR_ALPHA = .24f @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TangemChartMarkerPreview() { +private fun TangemChartMarkerPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { val marker = rememberTangemChartMarker(Color.Red, Color.White) - val xListKey = ExtraStore.Key>() - val model = getPreviewBigCartData(xListKey) + val y = previewData.second.map { it.toFloat() } + val x = List(y.size) { it.toFloat() } + val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) + val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt new file mode 100644 index 0000000000..662cb41bc6 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt @@ -0,0 +1,152 @@ +@file:Suppress("MagicNumber") + +package com.tangem.common.ui.charts.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import java.math.BigDecimal + +internal class MarketChartPreviewDataProvider : PreviewParameterProvider, List>> { + override val values: Sequence, List>> + get() { + val bitcoinPrice = listOf( + 59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917, + 63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680, + 63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183, + 62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065, + 62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158, + 61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372, + 62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540, + 61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838, + 65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845, + 67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273, + 66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720, + 69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714, + 69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517, + 68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821, + 68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630, + 68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567, + 68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121, + 67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709, + ).map { it.toBigDecimal() } + val bitcoinTimestamps = listOf( + 1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000, + 1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000, + 1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000, + 1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000, + 1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000, + 1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000, + 1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000, + 1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000, + 1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000, + 1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000, + 1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000, + 1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000, + 1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000, + 1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000, + 1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000, + 1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000, + 1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000, + 1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000, + 1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000, + 1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000, + 1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000, + 1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000, + 1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000, + 1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000, + 1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000, + 1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000, + 1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000, + 1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000, + 1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000, + 1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000, + 1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000, + ).map { it.toBigDecimal() } + val notcoinPrice = listOf( + "0.02026708", "0.02033274", "0.02112643", "0.02090579", + "0.02047231", "0.0215645", "0.0089055", "0.00674459", + "0.00733586", "0.00758165", "0.0068592", "0.00680505", + "0.00685493", "0.00702113", "0.00725545", "0.00696515", + "0.00681556", "0.00677416", "0.00669383", "0.00659057", + "0.00668081", "0.0066211", "0.0065232", "0.00612542", + "0.00600531", "0.00570974", "0.00559875", "0.00555247", + "0.005495", "0.00547841", "0.00545514", "0.00553865", + "0.00563302", "0.00568475", "0.00562512", "0.00551265", + "0.005398", "0.00560001", "0.00572701", "0.00563223", + "0.00555209", "0.00549261", "0.00524999", "0.00531381", + "0.00539702", "0.00530647", "0.00533963", "0.00527143", + "0.00525981", "0.00495323", "0.00481584", "0.0048854", + "0.00480298", "0.00471316", "0.00473191", "0.00476389", + "0.00476992", "0.00484649", "0.00471095", "0.00501396", + "0.00495481", "0.00545029", "0.0053947", "0.00533251", + "0.00518064", "0.00504481", "0.00507209", "0.00516952", + "0.00524886", "0.00542661", "0.00544231", "0.00579898", + "0.00681472", "0.00720634", "0.00824023", "0.00856427", + "0.00821144", "0.00818103", "0.00960254", "0.00911172", + "0.00888481", "0.00925911", "0.0091132", "0.009285", + "0.00886256", "0.00938211", "0.00936566", "0.0104387", + "0.01088452", "0.01200575", "0.01217514", "0.011921", + "0.01293908", "0.0126124", "0.01224851", "0.01191149", + "0.01179783", "0.01164533", "0.01175718", "0.01169137", + "0.01212768", "0.01215952", "0.01300256", "0.01589522", + "0.01588223", "0.01780629", "0.01919794", "0.01922187", + "0.02165268", "0.02400163", "0.02290975", "0.02383495", + "0.02088105", "0.02373489", "0.02269442", "0.02226249", + "0.02148038", "0.0232045", "0.02623129", "0.02378975", + "0.02438442", "0.02417507", "0.02269891", "0.02236198", + "0.02209164", "0.02169842", "0.02137774", "0.02188837", + "0.0216619", "0.02238334", "0.02186701", "0.02186521", + "0.0217595", "0.02099916", "0.02129634", "0.02143028", + "0.02192513", "0.02172005", "0.02184525", "0.01873974", + "0.01899164", "0.01957115", "0.0204723", "0.0199247", + "0.01935441", "0.01886976", "0.01856944", "0.0179975", + "0.0178625", "0.01796981", + ).map { BigDecimal(it) } + val notcoinTimestamps = listOf( + 1715428800000, 1715443200000, 1715457600000, 1715472000000, + 1715486400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, + 1715961600000, 1715976000000, 1715990400000, 1716004800000, + 1716019200000, 1716033600000, 1716048000000, 1716062400000, + 1716076800000, 1716091200000, 1716105600000, 1716120000000, + 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, + 1716249600000, 1716264000000, 1716278400000, 1716292800000, + 1716307200000, 1716321600000, 1716336000000, 1716350400000, + 1716364800000, 1716379200000, 1716393600000, 1716408000000, + 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, + 1716537600000, 1716552000000, 1716566400000, 1716580800000, + 1716595200000, 1716609600000, 1716624000000, 1716638400000, + 1716652800000, 1716667200000, 1716681600000, 1716696000000, + 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, + 1716825600000, 1716840000000, 1716854400000, 1716868800000, + 1716883200000, 1716897600000, 1716912000000, 1716926400000, + 1716940800000, 1716955200000, 1716969600000, 1716984000000, + 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, + 1717113600000, 1717128000000, 1717142400000, 1717156800000, + 1717171200000, 1717185600000, 1717200000000, 1717214400000, + 1717228800000, 1717243200000, 1717257600000, 1717272000000, + 1717286400000, 1717300800000, 1717315200000, 1717329600000, + 1717344000000, 1717358400000, 1717372800000, 1717387200000, + 1717401600000, 1717416000000, 1717430400000, 1717444800000, + 1717459200000, 1717473600000, 1717488000000, 1717502400000, + 1717516800000, 1717531200000, 1717545600000, 1717560000000, + 1717574400000, 1717588800000, 1717603200000, 1717617600000, + 1717632000000, 1717646400000, 1717660800000, 1717675200000, + 1717689600000, 1717704000000, 1717718400000, 1717732800000, + 1717747200000, 1717761600000, 1717776000000, 1717790400000, + 1717804800000, 1717819200000, 1717833600000, 1717848000000, + 1717862400000, 1717876800000, 1717891200000, 1717905600000, + 1717920000000, 1717934400000, + ).map { it.toBigDecimal() } + + return sequenceOf(bitcoinTimestamps to bitcoinPrice, notcoinTimestamps to notcoinPrice) + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt deleted file mode 100644 index 0b90226888..0000000000 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/PreviewData.kt +++ /dev/null @@ -1,77 +0,0 @@ -@file:Suppress("MagicNumber", "LongMethod") - -package com.tangem.common.ui.charts.preview - -import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel -import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.common.data.ExtraStore -import com.patrykandpatrick.vico.core.common.data.MutableExtraStore - -internal fun getPreviewBigCartData(xListKey: ExtraStore.Key>): CartesianChartModel { - val y = listOf( - 59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917, - 63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680, - 63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183, - 62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065, - 62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158, - 61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372, - 62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540, - 61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838, - 65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845, - 67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273, - 66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720, - 69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714, - 69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517, - 68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821, - 68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630, - 68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567, - 68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121, - 67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709, - ) - val timestamps: List = listOf( - 1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000, - 1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000, - 1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000, - 1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000, - 1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000, - 1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000, - 1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000, - 1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000, - 1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000, - 1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000, - 1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000, - 1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000, - 1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000, - 1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000, - 1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000, - 1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000, - 1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000, - 1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000, - 1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000, - 1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000, - 1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000, - 1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000, - 1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000, - 1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000, - 1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000, - 1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000, - 1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000, - 1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000, - 1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000, - 1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000, - 1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000, - 1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000, - 1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000, - 1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000, - 1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000, - 1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000, - ) - - val x = List(y.size) { index -> index } - - return CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }).copy( - extraStore = MutableExtraStore().apply { - set(xListKey, timestamps) - }, - ) -} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt new file mode 100644 index 0000000000..e01681994c --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt @@ -0,0 +1,17 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Stable +import java.math.BigDecimal + +/** + * Used for formatting the axis labels in a chart. + * It takes a BigDecimal value and returns a CharSequence that represents the formatted label. + * + * @param value The value to be formatted. + * @return The formatted label as a CharSequence. + */ +@Stable +fun interface AxisLabelFormatter { + + fun format(value: BigDecimal): CharSequence +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt new file mode 100644 index 0000000000..7e4887fedc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt @@ -0,0 +1,36 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Immutable +import java.math.BigDecimal + +@Immutable +sealed interface MarketChartData { + + /** + * This interface represents the state when there is no data for the Market Chart. + */ + @Immutable + sealed interface NoData : MarketChartData { + @Immutable + data object Empty : NoData + + @Immutable + data object Loading : NoData + + @Immutable + data object ErrorAndRetry : NoData + } + + /** + * This data class represents the data for the Market Chart. + * It includes properties for x and y values. + * + * @property x List of x values. + * @property y List of y values. + */ + @Immutable + data class Data( + val x: List = listOf(), + val y: List = listOf(), + ) : MarketChartData +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt new file mode 100644 index 0000000000..da62140e59 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -0,0 +1,207 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Stable +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +/** + * This class represents a transaction for updating the state and look of a Market Chart. + * + * @property chartLook The updated look of the Market Chart. + * @property chartData The updated state of the Market Chart. + */ +class Transaction( + private val currentData: MarketChartData, + private val currentLook: MarketChartLook, +) { + var chartLook: MarketChartLook? = null + var chartData: MarketChartData.NoData? = null + + fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { + chartLook = block(currentLook) + } + + fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) { + chartData = block(currentData) + } +} + +/** + * This class represents a transaction for updating the state and look of a Market Chart. + * It extends the Transaction class and allows update state by data. + * + * @property chartData The updated state of the Market Chart. + * @property chartLook The updated look of the Market Chart. + */ +class TransactionSuspend( + private val currentData: MarketChartData, + private val currentLook: MarketChartLook, +) { + internal var nonSuspendTransaction: Transaction? = null + var chartData: MarketChartData? = null + var chartLook: MarketChartLook? + get() = nonSuspendTransaction?.chartLook + set(value) { + if (nonSuspendTransaction == null) { + nonSuspendTransaction = Transaction(currentData, currentLook) + } + nonSuspendTransaction?.chartLook = value + } + + fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { + chartLook = block(currentLook) + } + + internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) { + chartData = block(currentData) + } + + internal fun updateData(block: (prev: MarketChartData.Data) -> MarketChartData.Data) { + chartData = when (val currentState = currentData) { + is MarketChartData.Data -> block(currentState) + else -> currentState + } + } +} + +@Stable +class MarketChartDataProducer private constructor( + initialData: MarketChartData, + initialLook: MarketChartLook, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default, +) { + internal val startDrawingAnimation = MutableSharedFlow() + internal val dataState = MutableStateFlow(initialData) + internal val lookState = MutableStateFlow(initialLook) + internal val entries = MutableStateFlow>(emptyList()) + + internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher) + + /** + * This function runs a suspending transaction block to update the state and look of the Market Chart. + */ + suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = + handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block)) + + /** + * This function runs a non-suspending transaction block to update the state and look of the Market Chart. + */ + fun runTransaction(block: Transaction.() -> Unit) = + handleTransaction(transaction = Transaction(dataState.value, lookState.value).apply(block)) + + private suspend fun handleTransactionSuspend(transaction: TransactionSuspend) { + val nonSuspendTransaction = transaction.nonSuspendTransaction + val chartData = transaction.chartData + val oldData = dataState.value + + if (chartData != null) { + dataState.value = chartData + } + + if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) { + if (lookState.value.animationOnDataChange) { + startDrawingAnimation.emit(Unit) + } + withContext(dispatcher) { + val minX = chartData.x.min() + val minY = chartData.y.min() + + val normY = chartData.y.map { normalize(it, minY) } + val normX = chartData.x.map { normalize(it, minX) } + + val entriesLocal = normX.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, normY[index]) } + entries.value = entriesLocal + + modelProducer.runTransaction { + add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal))) + + updateExtras { + it[entriesKey] = entriesLocal + it[xKey] = chartData.x + it[yKey] = chartData.y + } + }.await() + } + } + + nonSuspendTransaction?.let { handleTransaction(it) } + } + + private fun handleTransaction(transaction: Transaction) { + transaction.chartData?.let { + dataState.value = it + } + transaction.chartLook?.let { + lookState.value = it + } + } + + companion object { + internal val entriesKey = ExtraStore.Key>() + internal val xKey = ExtraStore.Key>() + internal val yKey = ExtraStore.Key>() + + private val initialData: MarketChartData = MarketChartData.NoData.Empty + private val initialLook: MarketChartLook = MarketChartLook() + + /** + * This function builds a MarketChartDataProducer with the given parameters. + * It runs a suspending transaction block to initialize the data and look of the Market Chart. + * + * @param dispatcher The dispatcher to be used for data updates. + * @param block The transaction block to be run. + * @return A MarketChartDataProducer. + */ + suspend fun buildSuspend( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: TransactionSuspend.() -> Unit, + ): MarketChartDataProducer { + val transaction = TransactionSuspend(initialData, initialLook).apply(block) + + return MarketChartDataProducer( + initialData = initialData, + initialLook = initialLook, + dispatcher = dispatcher, + ).apply { + handleTransactionSuspend(transaction) + } + } + + /** + * This function builds a MarketChartDataProducer with the given parameters. + * It runs a non-suspending transaction block to initialize the data and look of the Market Chart. + * + * @param dispatcher The dispatcher to be used for data updates. + * @param block The transaction block to be run. + * @return A MarketChartDataProducer. + */ + fun build( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: Transaction.() -> Unit, + ): MarketChartDataProducer { + val transaction = Transaction(initialData, initialLook).apply(block) + + return MarketChartDataProducer( + initialData = transaction.chartData ?: initialData, + initialLook = transaction.chartLook ?: initialLook, + dispatcher = dispatcher, + ) + } + } +} + +private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float { + val n = value - min + return if (scale > 2) { + n.movePointRight(scale - 2).toFloat() + } else { + n.toFloat() + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt new file mode 100644 index 0000000000..c9ecaae2c0 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -0,0 +1,28 @@ +package com.tangem.common.ui.charts.state + +/** + * This class represents the look and feel of a Market Chart. + * It includes properties for type, marker highlight, animation on data change, animate data appearance, + * and formatters for x and y axis. + * + * @property type The type of the chart, can be either Growing or Falling. + * @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart. + * @property animationOnDataChange A boolean indicating whether to animate on data change. + * @property animateDataAppearance A boolean indicating whether to animate data appearance. + * @property xAxisFormatter A formatter for the x-axis labels. + * @property yAxisFormatter A formatter for the y-axis labels. + */ +data class MarketChartLook( + val type: Type = Type.Growing, + val markerHighlightRightSide: Boolean = true, + val animationOnDataChange: Boolean = false, + val animateDataAppearance: Boolean = false, + val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, + val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, +) { + + enum class Type { + Growing, + Falling, + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt new file mode 100644 index 0000000000..110161db69 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -0,0 +1,151 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import java.math.BigDecimal + +/** + * MarketChartState used for MarketChart ui component. + * + * @param dataProducer The producer of the data for the Market Chart. + * @param colorMapper A function that maps a MarketChartLook.Type to a Color. + * @param onMarkerShown A callback function that is called when the marker is shown, hidden, or updated. + * @return A MarketChartState. + */ +@Composable +fun rememberMarketChartState( + dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} }, + colorMapper: (MarketChartLook.Type) -> Color = { + when (it) { + MarketChartLook.Type.Growing -> Color.Green + MarketChartLook.Type.Falling -> Color.Red + } + }, + onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> }, +): MarketChartState { + val lookState = dataProducer.lookState.collectAsStateWithLifecycle() + + val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) { + MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown) + } + + LaunchedEffect(Unit) { + dataProducer.startDrawingAnimation.collect { + state.startDrawingAnimation() + } + } + + return state +} + +/** + * Represents the state of a Market Chart. + * + * @property dataProducer The producer of the data for the Market Chart. + * @property lookState The look state of the Market Chart. + * @property colorMapper A function that maps a MarketChartLook.Type to a Color. + * @property markerCallback A callback function that is called when the marker is shown, hidden, or updated. + * @property isDrawingAnimationInProgress A boolean indicating whether the drawing animation is in progress. + */ +@Stable +class MarketChartState internal constructor( + private val dataProducer: MarketChartDataProducer, + private val lookState: State, + private val colorMapper: (MarketChartLook.Type) -> Color, + private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit, +) { + internal val startDrawingAnimationState = mutableStateOf(false) + internal val modelProducer = dataProducer.modelProducer + + internal val chartColor by derivedStateOf { + colorMapper(lookState.value.type) + } + + internal val markerHighlightRightSide by derivedStateOf { + lookState.value.markerHighlightRightSide + } + + internal val xValueFormatter by derivedStateOf { + CartesianValueFormatter { value, _, _ -> + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + val sMin = state.x.min() + val scale = sMin.scale() + val bVal = if (scale > 2) { + value.toBigDecimal().movePointLeft(scale - 2) + sMin + } else { + value.toBigDecimal() + sMin + } + + lookState.value.xAxisFormatter.format(bVal) + } + } + + internal val yValueFormatter by derivedStateOf { + CartesianValueFormatter { value, _, _ -> + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + val sMin = state.y.min() + val scale = sMin.scale() + val bVal = if (scale > 2) { + value.toBigDecimal().movePointLeft(scale - 2) + sMin + } else { + value.toBigDecimal() + sMin + } + + lookState.value.yAxisFormatter.format(bVal) + } + } + + internal var markerFraction: Float? by mutableStateOf(null) + + internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener { + override fun onShown(marker: CartesianMarker, targets: List) { + val point = getPoint(targets) ?: run { + markerCallback(null, null) + return + } + markerCallback(point.first, point.second) + } + + override fun onHidden(marker: CartesianMarker) { + markerCallback(null, null) + } + + override fun onUpdated(marker: CartesianMarker, targets: List) { + val point = getPoint(targets) ?: run { + markerCallback(null, null) + return + } + markerCallback(point.first, point.second) + } + } + + val isDrawingAnimationInProgress: Boolean by derivedStateOf { + startDrawingAnimationState.value + } + + private fun getPoint(targets: List): Pair? { + val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry + val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null + val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null + val x = state.x.getOrNull(entryIndex) ?: return null + val y = state.y.getOrNull(entryIndex) ?: return null + return x to y + } + + fun startDrawingAnimation() { + startDrawingAnimationState.value = true + } + + fun stopDrawingAnimation() { + startDrawingAnimationState.value = false + } +} \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index eb2b8502e1..d4f811162d 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) - implementation(deps.jodatime) + api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7fb3f2a8f0..b549e9bf7b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -91,7 +91,7 @@ tangemBlockchainSdk = "develop-654" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-360" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.21-tangem13" +tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem diff --git a/settings.gradle.kts b/settings.gradle.kts index 82e7f8bef8..914e2ac058 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,11 +35,11 @@ dependencyResolutionManagement { mavenLocal { content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeGroupAndSubgroups("com.tangem.vico") includeModule("com.tangem", "blstlib") includeModule("com.tangem", "blockchain") includeModule("com.tangem", "wallet-core-proto") includeModule("com.tangem", "wallet-core") - includeModule("com.tangem", "vico") } } maven { From 922a8f67068b8a3f7623a66e39b7e9bad0383bf7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 17:15:15 +0300 Subject: [PATCH 033/223] Updated on 2026-08-14 --- .../ui/OnboardingSeedPhraseStateHandler.kt | 20 +++- .../wallet/ui/OnboardingWalletFragment.kt | 4 +- core/ui/build.gradle.kts | 6 ++ .../TravalaNotificationWithBackground.kt | 3 +- .../components/snackbar/CopiedTextSnackbar.kt | 3 +- .../com/tangem/core/ui/res/TangemTheme.kt | 8 ++ .../tangem/core/ui/res/TangemThemePreview.kt | 17 ++-- .../tangem/core/ui/screen/ComposeActivity.kt | 2 +- .../ui/screen/ComposeBottomSheetFragment.kt | 2 +- .../tangem/core/ui/screen/ComposeFragment.kt | 2 +- .../tangem/core/ui/screen/ComposeScreen.kt | 6 +- .../tangem/core/ui/windowsize/WindowSize.kt | 99 +++++++++++++++++++ .../api/OnboardingSeedPhraseScreen.kt | 9 +- gradle/dependencies.toml | 3 + 14 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt index e92ebc5ca3..475a6d64b9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt @@ -1,6 +1,10 @@ package com.tangem.tap.features.onboarding.products.wallet.ui +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.collectAsState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.windowsize.rememberWindowSize import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen @@ -16,6 +20,7 @@ import com.tangem.wallet.R */ internal class OnboardingSeedPhraseStateHandler( private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(), + private val activity: Activity, ) { fun newState( @@ -49,11 +54,16 @@ internal class OnboardingSeedPhraseStateHandler( val subScreen = viewModel.currentScreen.collectAsState().value setMainScreenToolbarTitle(walletFragment, subScreen) - onboardingSeedPhraseApi.ScreenContent( - uiState = viewModel.uiState, - subScreen = subScreen, - progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress, - ) + TangemTheme( + isDark = isSystemInDarkTheme(), + windowSize = rememberWindowSize(activity = activity), + ) { + onboardingSeedPhraseApi.ScreenContent( + uiState = viewModel.uiState, + subScreen = subScreen, + progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress, + ) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 5c7b1e8589..b2d51cca3a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -68,7 +68,8 @@ class OnboardingWalletFragment : private val canSkipBackup by lazy { arguments?.getBoolean(OnboardingRouter.CAN_SKIP_BACKUP) ?: true } - private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler() + private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler + private val seedPhraseViewModel by viewModels() private lateinit var cardsWidget: WalletCardsWidget @@ -79,6 +80,7 @@ class OnboardingWalletFragment : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + seedPhraseStateHandler = OnboardingSeedPhraseStateHandler(activity = requireActivity()) val newSeedPhraseRouter = makeSeedPhraseRouter() seedPhraseRouter = newSeedPhraseRouter diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index eb2b8502e1..56ad01ef0a 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -22,6 +22,12 @@ dependencies { implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.paging.runtime) implementation(deps.androidx.palette) + implementation(deps.androidx.windowManager) { + exclude( + deps.kotlin.coroutines.android.get().module.group, + deps.kotlin.coroutines.android.get().module.name + ) + } /** Compose */ implementation(deps.compose.constraintLayout) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt index 9e6ba3db1d..6fc2f27ebd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt @@ -36,6 +36,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette.White import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Travala notification with image background @@ -192,7 +193,7 @@ private fun formatSubtitle(subtitle: String): AnnotatedString { @Preview @Composable private fun TravalaNotificationWithBackgroundPreview() { - TangemTheme { + TangemThemePreview { TravalaNotificationWithBackground( config = NotificationConfig( title = resourceReference( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt index 35cc8f6903..eea8a09bae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Snackbar to inform the user about copying text to the clipboard @@ -100,7 +101,7 @@ private fun MessageText(text: TextReference, modifier: Modifier = Modifier) { private fun Preview_CopiedTextSnackbar( @PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference, ) { - TangemTheme(isDark = false) { + TangemThemePreview { CopiedTextSnackbar(message = message) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index e5628c9bf5..968b635c5d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -8,9 +8,12 @@ import androidx.compose.runtime.* import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.MockHapticManager +import com.tangem.core.ui.windowsize.WindowSize + @Composable fun TangemTheme( isDark: Boolean = false, + windowSize: WindowSize, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, hapticManager: HapticManager = MockHapticManager, @@ -33,6 +36,7 @@ fun TangemTheme( LocalIsInDarkTheme provides isDark, LocalHapticManager provides hapticManager, LocalSnackbarHostState provides snackbarHostState, + LocalWindowSize provides windowSize, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -208,4 +212,8 @@ val LocalHapticManager = staticCompositionLocalOf { val LocalSnackbarHostState = staticCompositionLocalOf { error("No SnackbarHostState provided") +} + +val LocalWindowSize = staticCompositionLocalOf { + error("No WindowSize provided") } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 2a9e0caf65..48e3237b47 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -1,7 +1,9 @@ package com.tangem.core.ui.res import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.runtime.Composable +import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable fun TangemThemePreview( @@ -12,10 +14,13 @@ fun TangemThemePreview( ) { val isDarkTheme = isDark ?: isSystemInDarkTheme() - TangemTheme( - isDark = isDarkTheme, - typography = typography, - dimens = dimens, - content = content, - ) + BoxWithConstraints { + TangemTheme( + isDark = isDarkTheme, + typography = typography, + dimens = dimens, + windowSize = rememberWindowSizePreview(maxWidth, maxHeight), + content = content, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt index fe4f115dcd..9d4acd5fc0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt @@ -12,6 +12,6 @@ abstract class ComposeActivity : ComponentActivity(), ComposeScreen { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(createComposeView(context = this)) + setContentView(createComposeView(context = this, activity = this)) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt index b437a39cd5..75dc4e8a98 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt @@ -52,7 +52,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), Compose override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView(inflater.context) + return createComposeView(inflater.context, requireActivity()) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt index fb811871af..51d450ed02 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt @@ -17,7 +17,7 @@ abstract class ComposeFragment : Fragment(), ComposeScreen { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions() - return createComposeView(inflater.context).also { + return createComposeView(inflater.context, requireActivity()).also { it.isTransitionGroup = isTransitionsInflated } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index 740eac38e1..d6b1869ec5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.screen +import android.app.Activity import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize @@ -10,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.windowsize.rememberWindowSize import com.tangem.domain.apptheme.model.AppThemeMode /** @@ -50,13 +52,15 @@ internal interface ComposeScreen { * @param context The context. * @return A [ComposeView] instance with the defined screen content. */ -internal fun ComposeScreen.createComposeView(context: Context): ComposeView { +internal fun ComposeScreen.createComposeView(context: Context, activity: Activity): ComposeView { return ComposeView(context).apply { setContent { val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode + val windowSize = rememberWindowSize(activity = activity) TangemTheme( isDark = shouldUseDarkTheme(appThemeMode), + windowSize = windowSize, hapticManager = uiDependencies.hapticManager, snackbarHostState = uiDependencies.globalSnackbarHostState, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt b/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt new file mode 100644 index 0000000000..a8928b86f8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.windowsize + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.window.layout.WindowMetricsCalculator + +/** + * The preferred way to determine the window size is because it provides the actual size of the window as opposed to + * [LocalConfiguration.current.screenHeightDp] + * + * @property width actual window width + * @property height actual window height + * @property widthSizeType window size type based on width + * @property heightSizeType window size type based on height + * @property smallestSize smallest size of width and height + */ +data class WindowSize( + val width: Dp, + val height: Dp, +) { + val widthSizeType: WindowSizeType = getWidthWindowSizeType(width) + val heightSizeType: WindowSizeType = getHeightWindowSizeType(height) + val smallestSize: Dp = minOf(width, height) + + fun widthAtLeast(type: WindowSizeType): Boolean = this.widthSizeType.ordinal >= type.ordinal + + fun widthAtLeast(value: Dp): Boolean = this.width >= value + + fun heightAtLeast(type: WindowSizeType): Boolean = this.heightSizeType.ordinal >= type.ordinal + + fun heightAtLeast(value: Dp): Boolean = this.height >= value + + fun widthAtMost(type: WindowSizeType): Boolean = this.widthSizeType.ordinal <= type.ordinal + + fun widthAtMost(value: Dp): Boolean = this.width <= value + + fun heightAtMost(type: WindowSizeType): Boolean = this.heightSizeType.ordinal <= type.ordinal + + fun heightAtMost(value: Dp): Boolean = this.height <= value + + private fun getWidthWindowSizeType(windowDp: Dp): WindowSizeType = when { + windowDp < 300.dp -> WindowSizeType.ExtraSmall + windowDp < 380.dp -> WindowSizeType.Small + windowDp < 540.dp -> WindowSizeType.Normal + windowDp < 700.dp -> WindowSizeType.Large + else -> WindowSizeType.ExtraLarge + } + + private fun getHeightWindowSizeType(heightDp: Dp): WindowSizeType = when { + heightDp < 480.dp -> WindowSizeType.ExtraSmall + heightDp < 640.dp -> WindowSizeType.Small + heightDp < 860.dp -> WindowSizeType.Normal + heightDp < 1100.dp -> WindowSizeType.Large + else -> WindowSizeType.ExtraLarge + } +} + +enum class WindowSizeType { + ExtraSmall, Small, Normal, Large, ExtraLarge +} + +@Composable +fun rememberWindowSize(activity: Activity): WindowSize { + val windowBoundsSize = rememberWindowBoundsSize(activity) + val windowDpSize = with(LocalDensity.current) { + windowBoundsSize.toDpSize() + } + + return WindowSize( + width = windowDpSize.width, + height = windowDpSize.height, + ) +} + +@Composable +internal fun rememberWindowSizePreview(width: Dp, height: Dp): WindowSize { + return remember(width, height) { + WindowSize( + width = width, + height = height, + ) + } +} + +@Composable +private fun rememberWindowBoundsSize(activity: Activity): Size { + val configuration = LocalConfiguration.current + val windowMetrics = remember(configuration) { + WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity) + } + return windowMetrics.bounds.toComposeRect().size +} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt index 978f1ea01e..6db1217109 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt @@ -1,7 +1,6 @@ package com.tangem.feature.onboarding.api import androidx.activity.compose.BackHandler -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.LinearProgressIndicator import androidx.compose.runtime.Composable @@ -19,11 +18,9 @@ class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi { @Composable override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { BackHandler(onBack = uiState.onBackClick) - TangemTheme(isDark = isSystemInDarkTheme()) { - Column { - ProgressIndicator(progress) - Content(subScreen, uiState) - } + Column { + ProgressIndicator(progress) + Content(subScreen, uiState) } } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7fb3f2a8f0..ea3f92b2dc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -21,6 +21,7 @@ androidxLifecycle = "2.5.1" androidx-paging = "3.1.1" androidx-palette = "1.0.0" androidx-datastore = "1.0.0" +androidxWindowManager = "1.3.0" # endregion AndroidX # region Compose @@ -145,6 +146,7 @@ androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" } androidx-swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayout" } androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } +androidx-windowManager = { group = "androidx.window", name = "window", version.ref = "androidxWindowManager" } lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } @@ -217,6 +219,7 @@ armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" } +kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" } kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" } desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" } googlePlay-core = { module = "com.google.android.play:core", version.ref = "googlePlayCore" } From e8a2b10e6f974d4c32cc8a62ba19f23c001e76db Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 15:11:40 +0500 Subject: [PATCH 034/223] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 6 +- .../events/ScanFailsDialogAnalytics.kt | 17 ++++ .../tangem/tap/common/ui/ScanFailsDialog.kt | 65 +++++++++++--- app/src/main/res/layout/dialog_scan_fails.xml | 84 +++++++++++++++++++ core/res/src/main/res/values-ru/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 2 + .../com/tangem/domain/redux/StateDialog.kt | 2 +- .../router/DefaultWalletRouter.kt | 4 +- .../presentation/router/InnerWalletRouter.kt | 2 +- .../intents/WalletWarningsClickIntents.kt | 5 +- 10 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt create mode 100644 app/src/main/res/layout/dialog_scan_fails.xml diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 1f9043292e..936230d205 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -46,7 +46,11 @@ class DialogManager : StoreSubscriber { dialog = when (state.dialog) { is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context) - is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context, state.dialog.source) + is StateDialog.ScanFailsDialog -> ScanFailsDialog.create( + context = context, + source = state.dialog.source, + onTryAgain = state.dialog.onTryAgain, + ) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt new file mode 100644 index 0000000000..392f340405 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSources) : AnalyticsEvent( + category = "Cant Scan The Card", + event = button.event, + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), +) { + enum class Buttons(val event: String) { + TRY_AGAIN("Try again button"), + HOW_TO_SCAN("Button blog"), + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index 33c8044a23..ed7c3c4c90 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -1,37 +1,76 @@ package com.tangem.tap.common.ui import android.content.Context +import android.view.View +import android.widget.TextView import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import androidx.core.view.isVisible import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.feedback.ScanFailsEmail import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.home.LocaleRegionProvider +import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.store import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -object ScanFailsDialog { - fun create(context: Context, source: StateDialog.ScanFailsSource): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.common_warning)) - setMessage(R.string.alert_troubleshooting_scan_card_title) - setPositiveButton(R.string.alert_button_request_support) { _, _ -> - val sourceAnalytics = when (source) { - StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main - StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn - StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings - StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro +internal object ScanFailsDialog { + + private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/" + private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/" + + fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog { + return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { + val customView = View.inflate(context, R.layout.dialog_scan_fails, null) + val sourceAnalytics = when (source) { + StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main + StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn + StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings + StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro + } + val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button) + if (onTryAgain != null) { + tryAgainBtn?.isVisible = true + tryAgainBtn?.setOnClickListener { + store.dispatchDialogHide() + Analytics.send( + ScanFailsDialogAnalytics( + button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN, + source = sourceAnalytics, + ), + ) + onTryAgain() } + } else { + tryAgainBtn?.isVisible = false + } + customView.findViewById(R.id.how_to_scan_button)?.setOnClickListener { + Analytics.send( + ScanFailsDialogAnalytics( + button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN, + source = sourceAnalytics, + ), + ) + val locale = LocaleRegionProvider().getRegion() + val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK + store.dispatchOpenUrl(link) + } + customView.findViewById(R.id.request_support_button)?.setOnClickListener { Analytics.send(Basic.ButtonSupport(sourceAnalytics)) store.dispatch(GlobalAction.SendEmail(ScanFailsEmail())) } - setNeutralButton(R.string.common_cancel) { _, _ -> } + customView.findViewById(R.id.cancel_button)?.setOnClickListener { + store.dispatchDialogHide() + } + setView(customView) setOnDismissListener { store.dispatchDialogHide() } }.create() } diff --git a/app/src/main/res/layout/dialog_scan_fails.xml b/app/src/main/res/layout/dialog_scan_fails.xml new file mode 100644 index 0000000000..dd5924f8c5 --- /dev/null +++ b/app/src/main/res/layout/dialog_scan_fails.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index afda2fa213..9e421c5384 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -4,7 +4,9 @@ Добавить токен Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. + Как сканировать Обратиться в поддержку + Попробовать снова Эта функция недоступна в демонстрационном режиме Причина: %s Не могу отправить транзакцию diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8ef0f52ec8..49aad9383e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,7 +4,9 @@ Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. + How to scan Request support + Try again This feature is disabled in Demo mode Reason: %s Can\'t send a transaction diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt index 24571182fb..e4beb677bd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -2,7 +2,7 @@ package com.tangem.domain.redux interface StateDialog { - data class ScanFailsDialog(val source: ScanFailsSource) : StateDialog + data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog enum class ScanFailsSource { MAIN, SIGN_IN, SETTINGS, INTRO; 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 e30b525b92..535062c3ef 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 @@ -174,8 +174,8 @@ internal class DefaultWalletRouter( reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.ManageTokens)) } - override fun openScanFailedDialog() { - reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN)) + override fun openScanFailedDialog(onTryAgain: () -> Unit) { + reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } 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 bf8aba542f..d3f0095b29 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 @@ -59,5 +59,5 @@ internal interface InnerWalletRouter : WalletRouter { fun openManageTokensScreen() /** Open scan failed dialog */ - fun openScanFailedDialog() + fun openScanFailedDialog(onTryAgain: () -> Unit) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 21982d8010..087ec87d35 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -165,7 +165,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onScanToUnlockWalletClick() { analyticsEventHandler.send(MainScreen.UnlockWithCardScan) + openScanCardDialog() + } + private fun openScanCardDialog() { viewModelScope.launch(dispatchers.main) { scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) .onLeft { error -> @@ -175,7 +178,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), ) } - ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog() + ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog(::openScanCardDialog) } } } From c1900053f79cc04ef9cfc17f6e10a7c9a1c3d08d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:29:44 +0300 Subject: [PATCH 035/223] Updated on 2026-08-14 --- .../ui/amountScreen/AmountScreenContent.kt | 6 +- .../converters/AmountCurrencyTransformer.kt | 15 +++-- .../AmountPastedTriggerDismissTransformer.kt | 6 +- .../converters/AmountReduceByTransformer.kt | 14 +++-- .../converters/AmountReduceToTransformer.kt | 14 +++-- .../converters/AmountStateConverter.kt | 2 +- .../field/AmountFieldChangeTransformer.kt | 16 +++--- .../field/AmountFieldMaxAmountTransformer.kt | 12 ++-- .../ui/amountScreen/models/AmountState.kt | 56 ++++++++++-------- .../preview/AmountStatePreviewData.kt | 2 +- .../amountScreen/ui/AmountFieldContainer.kt | 2 +- .../tangem/utils/transformer/Transformer.kt | 7 ++- features/staking/impl/build.gradle.kts | 6 +- .../state/StakingStateController.kt | 7 ++- .../impl/presentation/state/StakingUiState.kt | 16 +----- .../state/stub/StakingClickIntentsStub.kt | 10 ++++ .../HideBalanceStateTransformer.kt | 3 +- .../SetInitialDataStateTransformer.kt | 27 ++++++++- .../StakingScreenStateTransformer.kt | 8 --- .../amount/AmountChangeStateTransformer.kt | 18 ++++++ .../AmountCurrencyChangeStateTransformer.kt | 17 ++++++ .../amount/AmountMaxValueStateTransformer.kt | 16 ++++++ .../AmountPasteDismissStateTransformer.kt | 14 +++++ .../presentation/ui/StakingAmountContent.kt | 11 ---- .../impl/presentation/ui/StakingScreen.kt | 9 ++- .../viewmodel/StakingClickIntents.kt | 6 +- .../viewmodel/StakingViewModel.kt | 57 ++++++++++++++++++- 27 files changed, 270 insertions(+), 107 deletions(-) delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index 0285b03b87..64615bc792 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -9,11 +9,11 @@ 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.PreviewParameterProvider -import com.tangem.common.ui.amountScreen.ui.amountField -import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.common.ui.amountScreen.ui.amountField +import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -25,6 +25,8 @@ import com.tangem.core.ui.res.TangemThemePreview */ @Composable fun AmountScreenContent(amountState: AmountState, isBalanceHiding: Boolean, clickIntents: AmountScreenClickIntents) { + if (amountState !is AmountState.Data) return + // Do not put fillMaxSize() in here LazyColumn( modifier = Modifier diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt index d4264afd5c..cdacdd3ce7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -5,21 +5,24 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer /** * Selected currency change from crypto currency to app currency and vice versa * - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value is crypto currency or app currency */ class AmountCurrencyTransformer( - private val cryptoCurrencyStatusProvider: Provider, -) : Transformer { - override fun transform(prevState: AmountState, value: Boolean): AmountState { + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: Boolean, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + val amountTextField = prevState.amountTextField - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() val isDoneActionEnabled = prevState.isPrimaryButtonEnabled diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt index 4946b2d297..ebf69ed80d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt @@ -6,8 +6,10 @@ import com.tangem.utils.transformer.Transformer /** * Dismisses indication on pasted value */ -class AmountPastedTriggerDismissTransformer : Transformer { - override fun transform(prevState: AmountState, value: Boolean): AmountState { +class AmountPastedTriggerDismissTransformer : Transformer { + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + return prevState.copy( amountTextField = prevState.amountTextField.copy( isValuePasted = false, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index a10cc83951..78ce58f37d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -8,7 +8,6 @@ import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -16,13 +15,16 @@ import java.math.BigDecimal /** * Reduces amount by specific value * - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value reduced by value */ class AmountReduceByTransformer( - private val cryptoCurrencyStatusProvider: Provider, -) : Transformer { - override fun transform(prevState: AmountState, value: ReduceByData): AmountState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: ReduceByData, +) : Transformer { + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + val amountTextField = prevState.amountTextField val cryptoDecimals = amountTextField.cryptoAmount.decimals val fiatDecimals = amountTextField.fiatAmount.decimals diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index e49b22924e..aed8332b0b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -8,7 +8,6 @@ import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -16,13 +15,16 @@ import java.math.BigDecimal /** * Reduces amount to specific value * - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value reduced to value */ class AmountReduceToTransformer( - private val cryptoCurrencyStatusProvider: Provider, -) : Transformer { - override fun transform(prevState: AmountState, value: BigDecimal): AmountState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: BigDecimal, +) : Transformer { + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + val amountTextField = prevState.amountTextField val cryptoDecimals = amountTextField.cryptoAmount.decimals val fiatDecimals = amountTextField.fiatAmount.decimals diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 31be99af9a..0fe994f885 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -52,7 +52,7 @@ class AmountStateConverter( val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) val noFeeRate = status.value.fiatRate.isNullOrZero() - return AmountState( + return AmountState.Data( walletName = userWallet.name, walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), tokenIconState = iconStateConverter.convert(status), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index d8c937246f..f0be223891 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -9,7 +9,6 @@ import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -17,14 +16,17 @@ import java.math.BigDecimal /** * Amount value change * - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value amount value */ class AmountFieldChangeTransformer( - private val cryptoCurrencyStatusProvider: Provider, -) : Transformer { + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: String, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState - override fun transform(prevState: AmountState, value: String): AmountState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val amountTextField = prevState.amountTextField if (value.isEmpty()) return prevState.emptyState() @@ -67,7 +69,7 @@ class AmountFieldChangeTransformer( ) } - private fun AmountState.emptyState(): AmountState { + private fun AmountState.Data.emptyState(): AmountState.Data { return copy( isPrimaryButtonEnabled = false, amountTextField = amountTextField.copy( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt index d18b31c41a..05a8705750 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.RoundingMode @@ -14,14 +13,15 @@ import java.math.RoundingMode /** * Selects maximum amount value * - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property cryptoCurrencyStatus current cryptocurrency status */ class AmountFieldMaxAmountTransformer( - private val cryptoCurrencyStatusProvider: Provider, -) : Transformer { + private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState - override fun transform(prevState: AmountState, value: Unit): AmountState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val amountTextField = prevState.amountTextField val cryptoDecimals = amountTextField.cryptoAmount.decimals diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index a420689714..045ec94609 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -5,28 +5,36 @@ import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.PersistentList -/** - * Model for amount state - * - * @param isPrimaryButtonEnabled indicates if next state button enabled - * @param walletName user wallet name - * @param walletBalance user crypto currency balance in wallet - * @param tokenIconState crypto currency icon state - * @param segmentedButtonConfig currency switcher config - * @param selectedButton selected currency index - * @param isSegmentedButtonsEnabled indicates if currency switches is enabled - * @param amountTextField amount field state - * @param appCurrencyCode app currency code - */ +/** Model for amount state */ @Stable -data class AmountState( - val isPrimaryButtonEnabled: Boolean, - val walletName: String, - val walletBalance: TextReference, - val tokenIconState: TokenIconState, - val segmentedButtonConfig: PersistentList, - val selectedButton: Int, - val isSegmentedButtonsEnabled: Boolean, - val amountTextField: AmountFieldModel, - val appCurrencyCode: String, -) \ No newline at end of file +sealed class AmountState { + + abstract val isPrimaryButtonEnabled: Boolean + + /** + * @param isPrimaryButtonEnabled indicates if next state button enabled + * @param walletName user wallet name + * @param walletBalance user crypto currency balance in wallet + * @param tokenIconState crypto currency icon state + * @param segmentedButtonConfig currency switcher config + * @param selectedButton selected currency index + * @param isSegmentedButtonsEnabled indicates if currency switches is enabled + * @param amountTextField amount field state + * @param appCurrencyCode app currency code + */ + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val walletName: String, + val walletBalance: TextReference, + val tokenIconState: TokenIconState, + val segmentedButtonConfig: PersistentList, + val selectedButton: Int, + val isSegmentedButtonsEnabled: Boolean, + val amountTextField: AmountFieldModel, + val appCurrencyCode: String, + ) : AmountState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : AmountState() +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index fa1ce561e4..c597d024b4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -15,7 +15,7 @@ import java.math.BigDecimal object AmountStatePreviewData { - val amountState = AmountState( + val amountState = AmountState.Data( isPrimaryButtonEnabled = false, walletName = "Family Wallet", walletBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 28d9782293..09b81b4420 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -21,7 +21,7 @@ import com.tangem.utils.Strings.STARS private const val AMOUNT_FIELD_KEY = "amountFieldKey" internal fun LazyListScope.amountField( - amountState: AmountState, + amountState: AmountState.Data, isBalanceHiding: Boolean, modifier: Modifier = Modifier, ) { diff --git a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt index e4e35c4299..9c271d0920 100644 --- a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt +++ b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt @@ -1,5 +1,8 @@ package com.tangem.utils.transformer -interface Transformer { - fun transform(prevState: S, value: V): S +/** + * Transforms state to updated state. + */ +interface Transformer { + fun transform(prevState: S): S } \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index e608669082..072e1bcd7b 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -56,9 +56,13 @@ dependencies { implementation(projects.domain.staking) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.legacy) + implementation(projects.domain.models) /** Common */ - implementation(projects.common) + implementation(projects.common.ui) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index fc51c5f7e7..74f5a5bff2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.presentation.state +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.event.consumedEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.state.transformers.StakingScreenStateTransformer +import com.tangem.utils.transformer.Transformer import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -23,7 +24,7 @@ internal class StakingStateController @Inject constructor() { mutableUiState.update(function = function) } - fun update(transformer: StakingScreenStateTransformer) { + fun update(transformer: Transformer) { mutableUiState.update(function = transformer::transform) } @@ -37,7 +38,7 @@ internal class StakingStateController @Inject constructor() { cryptoCurrencyName = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), - amountState = StakingStates.AmountState.Empty(), + amountState = AmountState.Empty(), validatorState = StakingStates.ValidatorState.Empty(), confirmStakingState = StakingStates.ConfirmStakingState.Empty(), isBalanceHidden = false, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 1253b63881..ebea79fcbc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @@ -14,7 +15,7 @@ internal data class StakingUiState( val cryptoCurrencyName: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, - val amountState: StakingStates.AmountState, + val amountState: AmountState, val validatorState: StakingStates.ValidatorState, val confirmStakingState: StakingStates.ConfirmStakingState, val isBalanceHidden: Boolean, @@ -23,7 +24,7 @@ internal data class StakingUiState( fun copyWrapped( initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, - amountState: StakingStates.AmountState = this.amountState, + amountState: AmountState = this.amountState, validatorState: StakingStates.ValidatorState = this.validatorState, confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState, ): StakingUiState = copy( @@ -57,17 +58,6 @@ internal sealed class StakingStates { ) : InitialInfoState() } - /** Amount state */ - sealed class AmountState : StakingStates() { - data class Data( - override val isPrimaryButtonEnabled: Boolean, - ) : AmountState() - - data class Empty( - override val isPrimaryButtonEnabled: Boolean = false, - ) : AmountState() - } - /** Validator state */ sealed class ValidatorState : StakingStates() { data class Data( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index b09e5d9f4c..7c01094036 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -9,4 +9,14 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onNextClick() {} override fun onPrevClick() {} + + override fun onAmountValueChange(value: String) {} + + override fun onAmountPasteTriggerDismiss() {} + + override fun onMaxValueClick() {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onAmountNext() {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt index 90afd05bb9..84128a62a1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt @@ -1,10 +1,11 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer internal class HideBalanceStateTransformer( private val isBalanceHidden: Boolean, -) : StakingScreenStateTransformer { +) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy(isBalanceHidden = isBalanceHidden) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e16098575b..dcbe461b4f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -1,31 +1,52 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer import java.math.BigDecimal internal class SetInitialDataStateTransformer( private val clickIntents: StakingClickIntents, private val yield: Yield, private val cryptoCurrencyStatusProvider: Provider, -) : StakingScreenStateTransformer { + private val userWalletProvider: Provider, + private val appCurrencyProvider: Provider, +) : Transformer { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { + AmountStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + userWalletProvider = userWalletProvider, + iconStateConverter = iconStateConverter, + ) + } override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), + amountState = createInitialAmountState(), ) } @@ -50,6 +71,10 @@ internal class SetInitialDataStateTransformer( ) } + private fun createInitialAmountState(): AmountState { + return amountStateConverter.convert("") + } + private fun getAprRange(): TextReference { val aprValues = yield.validators.mapNotNull { it.apr } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt deleted file mode 100644 index 05d13135f0..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/StakingScreenStateTransformer.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.features.staking.impl.presentation.state.StakingUiState - -internal interface StakingScreenStateTransformer { - - fun transform(prevState: StakingUiState): StakingUiState -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt new file mode 100644 index 0000000000..110de484fc --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountChangeStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: String, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountFieldChangeTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt new file mode 100644 index 0000000000..0410bf8745 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountCurrencyChangeStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: Boolean, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt new file mode 100644 index 0000000000..3d7dc9125d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountMaxValueStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt new file mode 100644 index 0000000000..b2d7752952 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountPasteDismissStateTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountPastedTriggerDismissTransformer().transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt deleted file mode 100644 index e1324855c8..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingAmountContent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.staking.impl.presentation.ui - -import androidx.compose.runtime.Composable -import com.tangem.features.staking.impl.presentation.state.StakingStates - -@Composable -internal fun StakingAmountContent(state: StakingStates.AmountState) { - if (state !is StakingStates.AmountState.Data) return - - // TODO staking -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 6a0d321903..ec18cf17e0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -12,11 +12,12 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -121,8 +122,10 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M StakingStep.InitialInfo -> StakingInitialInfoContent( state = uiState.initialInfoState, ) - StakingStep.Amount -> StakingAmountContent( - state = uiState.amountState, + StakingStep.Amount -> AmountScreenContent( + amountState = uiState.amountState, + isBalanceHiding = uiState.isBalanceHidden, + clickIntents = uiState.clickIntents, ) StakingStep.ValidatorAndFee -> StakingValidatorAndFeeContent( state = uiState.validatorState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 8176c2ea6c..27cd56dced 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,10 +1,14 @@ package com.tangem.features.staking.impl.presentation.viewmodel -internal interface StakingClickIntents { +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents + +internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() fun onNextClick() fun onPrevClick() + + override fun onAmountNext() = onNextClick() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 22c0686b9c..7144e4d5ed 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -4,19 +4,28 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateController -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -25,12 +34,15 @@ import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates +@Suppress("LongParameterList") @HiltViewModel internal class StakingViewModel @Inject constructor( private val stateController: StakingStateController, private val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -53,6 +65,8 @@ internal class StakingViewModel @Inject constructor( private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var innerRouter: InnerStakingRouter by Delegates.notNull() + private var userWallet: UserWallet by Delegates.notNull() + private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() init { subscribeOnBalanceHiding() @@ -71,6 +85,22 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } + override fun onAmountValueChange(value: String) { + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value)) + } + + override fun onAmountPasteTriggerDismiss() { + stateController.update(AmountPasteDismissStateTransformer()) + } + + override fun onMaxValueClick() { + stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus)) + } + + override fun onCurrencyChangeClick(isFiat: Boolean) { + stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat)) + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -78,6 +108,14 @@ internal class StakingViewModel @Inject constructor( private fun subscribeOnCurrencyStatusUpdates() { viewModelScope.launch { + getUserWalletUseCase(userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + }, + ifLeft = { + // TODO staking error + }, + ) getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( ifRight = { cryptoCurrencyStatus = it @@ -86,6 +124,8 @@ internal class StakingViewModel @Inject constructor( clickIntents = this@StakingViewModel, yield = yield, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, ), ) }, @@ -106,4 +146,19 @@ internal class StakingViewModel @Inject constructor( .flowOn(dispatchers.main) .launchIn(viewModelScope) } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .conflate() + .distinctUntilChanged() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .flowOn(dispatchers.main) + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } } \ No newline at end of file From 72ddf5ae12ae35eb6cd5162b0f53c6037e4c948c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:39:15 +0400 Subject: [PATCH 036/223] Updated on 2026-08-14 --- domain/tokens/models/build.gradle.kts | 3 +- .../domain/tokens/model/CryptoCurrency.kt | 25 ++++++++----- .../com/tangem/domain/tokens/model/Network.kt | 37 +++++++++++-------- domain/wallets/models/build.gradle.kts | 5 +++ .../domain/wallets/models/UserWalletId.kt | 5 ++- 5 files changed, 47 insertions(+), 28 deletions(-) diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index 948bad320b..a243ada498 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) - id("kotlin-parcelize") + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -10,6 +10,7 @@ android { } dependencies { + implementation(deps.kotlin.serialization) implementation(projects.domain.txhistory.models) implementation(projects.core.analytics.models) implementation(deps.tangem.blockchain) { diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index db909d08f3..e650d2b8ea 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable /** * Represents a generic cryptocurrency. @@ -14,8 +13,8 @@ import kotlinx.parcelize.Parcelize * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. * @property isCustom Indicates whether the currency is a custom user-added currency or not. */ -@Parcelize -sealed class CryptoCurrency : Parcelable { +@Serializable +sealed class CryptoCurrency { abstract val id: ID abstract val network: Network @@ -28,6 +27,7 @@ sealed class CryptoCurrency : Parcelable { /** * Represents a native coin in the blockchain network. */ + @Serializable data class Coin( override val id: ID, override val network: Network, @@ -48,6 +48,7 @@ sealed class CryptoCurrency : Parcelable { * * @property contractAddress Address of the contract managing the token. */ + @Serializable data class Token( override val id: ID, override val network: Network, @@ -75,12 +76,12 @@ sealed class CryptoCurrency : Parcelable { * @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if * its ID of the custom token. */ - @Parcelize + @Serializable data class ID( private val prefix: Prefix, private val body: Body, private val suffix: Suffix, - ) : Parcelable { + ) { val value: String get() = buildString { @@ -121,12 +122,13 @@ sealed class CryptoCurrency : Parcelable { * * The body can be either a raw network ID or a raw network ID with a network derivation path. */ - @Parcelize - sealed class Body : Parcelable { + @Serializable + sealed class Body { /** The value of the body. */ abstract val value: String + @Serializable /** Represents a raw network ID. */ data class NetworkId(val rawId: String) : Body() { override val value: String get() = rawId @@ -137,6 +139,7 @@ sealed class CryptoCurrency : Parcelable { * * Should be used for a cryptocurrencies with custom derivation path. * */ + @Serializable data class NetworkIdWithDerivationPath( val rawId: String, val derivationPath: String, @@ -155,13 +158,14 @@ sealed class CryptoCurrency : Parcelable { * * The suffix can either be a raw ID or a contract address. */ - @Parcelize - sealed class Suffix : Parcelable { + @Serializable + sealed class Suffix { /** The value of the suffix, which could be either a raw ID or a contract address. */ abstract val value: String /** Represents a raw ID suffix. */ + @Serializable data class RawID(val rawId: String, val contractAddress: String? = null) : Suffix() { override val value: String get() = buildString { @@ -174,6 +178,7 @@ sealed class CryptoCurrency : Parcelable { } /** Represents a contract address suffix. */ + @Serializable data class ContractAddress(val contractAddress: String) : Suffix() { override val value: String get() = contractAddress } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 5dcdb275ef..59d8d3a517 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable /** * Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type. @@ -20,7 +19,7 @@ import kotlinx.parcelize.Parcelize * that cannot be represented in a fiat currency. * (For those blockchains that have FeeResource instead of a standard type of fee) */ -@Parcelize +@Serializable data class Network( val id: ID, val backendId: String, @@ -30,7 +29,7 @@ data class Network( val isTestnet: Boolean, val standardType: StandardType, val hasFiatFeeRate: Boolean, -) : Parcelable { +) { init { require(name.isNotBlank()) { "Network name must not be blank" } @@ -42,8 +41,8 @@ data class Network( * @property value The string representation of the network ID. */ @JvmInline - @Parcelize - value class ID(val value: String) : Parcelable { + @Serializable + value class ID(val value: String) { init { require(value.isNotBlank()) { "Network ID must not be blank" } @@ -56,8 +55,8 @@ data class Network( * This class represents such paths in a generic manner, allowing for predefined card-based paths, * custom paths, or even no derivation path at all. */ - @Parcelize - sealed class DerivationPath : Parcelable { + @Serializable + sealed class DerivationPath { /** The actual derivation path value, if any. */ abstract val value: String? @@ -67,6 +66,7 @@ data class Network( * * @property value The derivation path string. */ + @Serializable data class Card(override val value: String) : DerivationPath() /** @@ -74,12 +74,14 @@ data class Network( * * @property value The derivation path string. */ + @Serializable data class Custom(override val value: String) : DerivationPath() /** * Represents a lack of derivation path. */ - object None : DerivationPath() { + @Serializable + data object None : DerivationPath() { override val value: String? get() = null } } @@ -93,31 +95,36 @@ data class Network( * * @property name The human-readable name of the standard type. */ - @Parcelize - sealed class StandardType : Parcelable { + @Serializable + sealed class StandardType { abstract val name: String /** Represents the ERC20 token standard, common on the Ethereum network. */ - object ERC20 : StandardType() { + @Serializable + data object ERC20 : StandardType() { override val name: String get() = "ERC20" } /** Represents the TRC20 token standard, common on the TRON network. */ - object TRC20 : StandardType() { + @Serializable + data object TRC20 : StandardType() { override val name: String get() = "TRC20" } /** Represents the BEP20 token standard, common on the Binance Smart Chain network. */ - object BEP20 : StandardType() { + @Serializable + data object BEP20 : StandardType() { override val name: String get() = "BEP20" } /** Represents the BEP2 token standard, common on the Binance Chain network. */ - object BEP2 : StandardType() { + @Serializable + data object BEP2 : StandardType() { override val name: String get() = "BEP2" } /** Represents a network that does not adhere to a predefined standard type. */ + @Serializable data class Unspecified(override val name: String) : StandardType() } } \ No newline at end of file diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 537d724d84..1b3ce4dfaf 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -11,4 +12,8 @@ dependencies { // region Domain modules implementation(project(":domain:models")) // endregion + + // region Other libraries + implementation(deps.kotlin.serialization) + // endregion } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt index 970ee3ad9f..4ea8ab824c 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt @@ -2,9 +2,10 @@ package com.tangem.domain.wallets.models import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import java.io.Serializable +import kotlinx.serialization.Serializable -data class UserWalletId(val stringValue: String) : Serializable { +@Serializable +data class UserWalletId(val stringValue: String) { val value = stringValue.hexToBytes() From 084d4282f7c20c162408f5e9a26c646743d2f77a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:40:27 +0400 Subject: [PATCH 037/223] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + common/routing/.gitignore | 1 + common/routing/build.gradle.kts | 25 +++ .../com/tangem/common/routing/AppRoute.kt | 164 ++++++++++++++++++ .../com/tangem/common/routing/AppRouter.kt | 42 +++++ .../common/routing/bundle/BundleDecoder.kt | 108 ++++++++++++ .../common/routing/bundle/BundleEncoder.kt | 107 ++++++++++++ .../common/routing/bundle/BundleUtils.kt | 60 +++++++ .../routing/bundle/RouteBundleParams.kt | 8 + .../routing/entity/SerializableBundle.kt | 24 +++ .../routing/entity/SerializableIntent.kt | 51 ++++++ .../common/routing/utils/AppRouterExt.kt | 16 ++ settings.gradle.kts | 1 + 13 files changed, 608 insertions(+) create mode 100644 common/routing/.gitignore create mode 100644 common/routing/build.gradle.kts create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt create mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 72424b337b..3362aa5bbb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -76,6 +76,7 @@ dependencies { implementation(projects.domain.walletConnect) implementation(projects.common) + implementation(projects.common.routing) implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) diff --git a/common/routing/.gitignore b/common/routing/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/common/routing/.gitignore @@ -0,0 +1 @@ +/build diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts new file mode 100644 index 0000000000..980934bf31 --- /dev/null +++ b/common/routing/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.common.routing" +} + +dependencies { + /* Core */ + implementation(projects.core.decompose) + + /* Domain */ + implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + /* Libs - Other */ + implementation(deps.androidx.core.ktx) + implementation(deps.kotlin.serialization) + implementation(deps.timber) +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt new file mode 100644 index 0000000000..2a337dbeaa --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -0,0 +1,164 @@ +package com.tangem.common.routing + +import android.os.Bundle +import com.tangem.common.routing.bundle.RouteBundleParams +import com.tangem.common.routing.bundle.bundle +import com.tangem.common.routing.entity.SerializableIntent +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +sealed class AppRoute(val path: String) : Route, RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + @Serializable + data object Initial : AppRoute(path = "/initial") + + @Serializable + data object Home : AppRoute(path = "/home") + + @Serializable + data class Welcome( + val intent: SerializableIntent? = null, + ) : AppRoute(path = "/welcome") { + + companion object { + const val INITIAL_INTENT_KEY = "intent" + } + } + + @Serializable + data object Disclaimer : AppRoute(path = "/disclaimer") + + @Serializable + data object OnboardingNote : AppRoute(path = "/onboarding/note") + + @Serializable + data class OnboardingWallet( + val canSkipBackup: Boolean = true, + ) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}") { + + companion object { + const val CAN_SKIP_BACKUP_KEY = "canSkipBackup" + } + } + + @Serializable + data object OnboardingTwins : AppRoute(path = "/onboarding/twins") + + @Serializable + data object OnboardingOther : AppRoute(path = "/onboarding/other") + + @Serializable + data object Wallet : AppRoute(path = "/wallet") + + @Serializable + data class CurrencyDetails( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") { + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + const val CRYPTO_CURRENCY_KEY = "currency" + } + } + + @Serializable + data class Send( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + val transactionId: String? = null, + val amount: String? = null, + val tag: String? = null, + val destinationAddress: String? = null, + ) : AppRoute( + path = "/send/${userWalletId.stringValue}/${currency.id.value}?" + + "&$transactionId" + + "&$amount" + + "&$tag" + + "&$destinationAddress", + ) { + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + const val CRYPTO_CURRENCY_KEY = "currency" + const val TRANSACTION_ID_KEY = "transactionId" + const val AMOUNT_KEY = "amount" + const val TAG_KEY = "tag" + const val DESTINATION_ADDRESS_KEY = "destinationAddress" + } + } + + @Serializable + data class Details( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/details/${userWalletId.stringValue}") + + @Serializable + data object DetailsSecurity : AppRoute(path = "/details/security") + + @Serializable + data object CardSettings : AppRoute(path = "/card_settings") + + @Serializable + data object AppSettings : AppRoute(path = "/app_settings") + + @Serializable + data object ResetToFactory : AppRoute(path = "/reset_to_factory") + + @Serializable + data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery") + + @Serializable + data object ManageTokens : AppRoute(path = "/manage_tokens") + + @Serializable + data object AddCustomToken : AppRoute(path = "/add_custom_token") + + @Serializable + data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions") + + @Serializable + data class QrScanning( + val source: SourceType, + val networkName: String? = null, + ) : AppRoute(path = "/qr_scanning") { + + companion object { + const val SOURCE_KEY = "source" + const val NETWORK_KEY = "networkName" + } + } + + @Serializable + data object ReferralProgram : AppRoute(path = "/referral_program") + + @Serializable + data class Swap( + val currency: CryptoCurrency, + ) : AppRoute(path = "/swap") { + + companion object { + const val CURRENCY_BUNDLE_KEY = "currency" + } + } + + @Serializable + data object TesterMenu : AppRoute(path = "/tester_menu") + + @Serializable + data object SaveWallet : AppRoute(path = "/save_wallet") + + @Serializable + data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") + + @Serializable + data object ModalNotification : AppRoute(path = "/modal_notification") + + @Serializable + data object Staking : AppRoute(path = "/staking") +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt new file mode 100644 index 0000000000..9e81df0ec5 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt @@ -0,0 +1,42 @@ +package com.tangem.common.routing + +import kotlin.reflect.KClass + +/** + * Interface for a router in the application. + * It provides methods for navigating through the application. + * + * Same as [com.tangem.core.decompose.navigation.Router] but without Decompose dependency. + * + * ***Must be removed after Decompose migration.*** + */ +interface AppRouter { + + /** + * The current navigation stack. + */ + val backStack: List + + /** + * Pushes a new route to the navigation stack. + * + * @param route The route to push. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops the top route from the navigation stack. + * + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the specified route class is found. + * + * @param routeClass The route class to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt new file mode 100644 index 0000000000..6a51004489 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt @@ -0,0 +1,108 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.SerializersModule + +@ExperimentalSerializationApi +internal class BundleDecoder( + private val bundle: Bundle, + private val elementsCount: Int = -1, + private val isInitializer: Boolean = true, + override val serializersModule: SerializersModule, +) : AbstractDecoder() { + + private var index = -1 + private var elementKey: String? = null + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + if (++index >= elementsCount) { + return CompositeDecoder.DECODE_DONE + } + + elementKey = descriptor.getElementName(index) + return index + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + val b = if (isInitializer) { + bundle + } else { + requireNotNull(bundle.getBundle(elementKey)) { + "Bundle is missing for key $elementKey while decoding" + } + } + + val count = when (descriptor.kind) { + StructureKind.MAP, + StructureKind.LIST, + -> b.getInt("\$size") + else -> descriptor.elementsCount + } + + return BundleDecoder( + bundle = b, + elementsCount = count, + isInitializer = false, + serializersModule = serializersModule, + ) + } + + override fun endStructure(descriptor: SerialDescriptor) { + /* no-op */ + } + + override fun decodeBoolean(): Boolean { + return bundle.getBoolean(elementKey) + } + + override fun decodeByte(): Byte { + return bundle.getByte(elementKey) + } + + override fun decodeChar(): Char { + return bundle.getChar(elementKey) + } + + override fun decodeDouble(): Double { + return bundle.getDouble(elementKey) + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + return bundle.getInt(elementKey) + } + + override fun decodeFloat(): Float { + return bundle.getFloat(elementKey) + } + + override fun decodeInt(): Int { + return bundle.getInt(elementKey) + } + + override fun decodeLong(): Long { + return bundle.getLong(elementKey) + } + + override fun decodeNotNullMark(): Boolean { + return bundle.containsKey(elementKey) + } + + override fun decodeNull(): Nothing? { + return null + } + + override fun decodeShort(): Short { + return bundle.getShort(elementKey) + } + + override fun decodeString(): String { + return requireNotNull(bundle.getString(elementKey)) { + "String is missing for key $elementKey while decoding" + } + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt new file mode 100644 index 0000000000..e0be7b8aa3 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt @@ -0,0 +1,107 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.modules.SerializersModule + +@ExperimentalSerializationApi +internal class BundleEncoder( + private val bundle: Bundle, + private val parentBundle: Bundle? = null, + private val keyInParent: String? = null, + private val isInitializer: Boolean = true, + override val serializersModule: SerializersModule, +) : AbstractEncoder() { + + private var elementKey: String? = null + + override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + elementKey = descriptor.getElementName(index) + return super.encodeElement(descriptor, index) + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + return if (isInitializer) { + BundleEncoder( + bundle = bundle, + parentBundle = null, + keyInParent = elementKey, + isInitializer = false, + serializersModule = serializersModule, + ) + } else { + BundleEncoder( + bundle = Bundle(), + parentBundle = bundle, + keyInParent = elementKey, + isInitializer = false, + serializersModule = serializersModule, + ) + } + } + + override fun endStructure(descriptor: SerialDescriptor) { + if (descriptor.kind in arrayOf(StructureKind.LIST, StructureKind.MAP)) { + val size = elementKey?.toIntOrNull()?.let { it + 1 } ?: 0 + bundle.putInt("\$size", size) + } + + if (keyInParent.isNullOrBlank()) { + return + } + + parentBundle?.putBundle(keyInParent, bundle) + } + + override fun encodeBoolean(value: Boolean) { + bundle.putBoolean(elementKey, value) + } + + override fun encodeByte(value: Byte) { + bundle.putByte(elementKey, value) + } + + override fun encodeChar(value: Char) { + bundle.putChar(elementKey, value) + } + + override fun encodeDouble(value: Double) { + bundle.putDouble(elementKey, value) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + bundle.putInt(elementKey, index) + } + + override fun encodeFloat(value: Float) { + bundle.putFloat(elementKey, value) + } + + override fun encodeInt(value: Int) { + bundle.putInt(elementKey, value) + } + + override fun encodeLong(value: Long) { + bundle.putLong(elementKey, value) + } + + override fun encodeNull() { + /* no-op */ + } + + override fun encodeShort(value: Short) { + bundle.putShort(elementKey, value) + } + + override fun encodeString(value: String) { + bundle.putString(elementKey, value) + } + + override fun encodeNotNullMark() { + /* no-op */ + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt new file mode 100644 index 0000000000..4c638fde65 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt @@ -0,0 +1,60 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +val defaultSerializersModule: SerializersModule = EmptySerializersModule() + +/** + * Deserialize this bundle into an object of type [T]. + * + * @receiver [Bundle] to deserialize. + * @param deserializer [DeserializationStrategy] of the [T] class. + * + * @return Object of type T deserialized from bundle. + */ +@OptIn(ExperimentalSerializationApi::class) +fun Bundle.unbundle( + deserializer: DeserializationStrategy, + serializersModule: SerializersModule = defaultSerializersModule, +): T { + val decoder = BundleDecoder( + bundle = this, + elementsCount = -1, + isInitializer = true, + serializersModule = serializersModule, + ) + + return deserializer.deserialize(decoder) +} + +/** + * Serialize [T] into a bundle. + * + * @receiver Object to serialize. + * @param serializer [SerializationStrategy] of the [T] class. + * + * @return bundle serialized from value + */ +@OptIn(ExperimentalSerializationApi::class) +fun T.bundle( + serializer: SerializationStrategy, + serializersModule: SerializersModule = defaultSerializersModule, +): Bundle { + val bundle = Bundle(serializer.descriptor.elementsCount) + val encoder = BundleEncoder( + bundle = bundle, + parentBundle = null, + keyInParent = null, + isInitializer = true, + serializersModule = serializersModule, + ) + + serializer.serialize(encoder, value = this) + + return bundle +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt new file mode 100644 index 0000000000..12ae0795d3 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt @@ -0,0 +1,8 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle + +interface RouteBundleParams { + + fun getBundle(): Bundle +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt new file mode 100644 index 0000000000..fcac1b0422 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt @@ -0,0 +1,24 @@ +package com.tangem.common.routing.entity + +import android.os.Bundle +import kotlinx.serialization.Serializable + +@Serializable +data class SerializableBundle( + val map: Map, +) { + + constructor(bundle: Bundle) : this( + map = bundle.keySet().mapNotNull { key -> + bundle.getString(key)?.let { key to it } + }.toMap(), + ) + + fun toBundle(): Bundle { + return Bundle().apply { + map.forEach { (key, value) -> + putString(key, value) + } + } + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt new file mode 100644 index 0000000000..38ef6f2096 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt @@ -0,0 +1,51 @@ +package com.tangem.common.routing.entity + +import android.content.ComponentName +import android.content.Intent +import android.net.Uri +import kotlinx.serialization.Serializable + +@Serializable +data class SerializableIntent( + val action: String?, + val dataString: String?, + val categories: Set?, + val type: String?, + val packageValue: String?, + val component: String?, + val flags: Int, + val extras: SerializableBundle?, +) { + + constructor(intent: Intent) : this( + action = intent.action, + dataString = intent.dataString, + categories = intent.categories, + type = intent.type, + packageValue = intent.`package`, + component = intent.component?.flattenToString(), + flags = intent.flags, + extras = intent.extras?.let(::SerializableBundle), + ) + + fun toIntent(): Intent { + val intent = Intent() + + intent.action = action + intent.setDataAndType( + dataString?.let { Uri.parse(it) }, + type, + ) + categories?.let { categories -> + for (category in categories) { + intent.addCategory(category) + } + } + intent.`package` = packageValue + intent.component = component?.let { ComponentName.unflattenFromString(it) } + intent.flags = flags + extras?.let { intent.putExtras(it.toBundle()) } + + return intent + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt new file mode 100644 index 0000000000..1bc85f941f --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt @@ -0,0 +1,16 @@ +package com.tangem.common.routing.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter + +/** + * Pops routes from the navigation stack until the specified route [R] is found. + * + * ***Must be removed after Decompose migration.*** + * + * @param R The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ +inline fun AppRouter.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) { + popTo(R::class, onComplete) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 914e2ac058..40841c6327 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -113,6 +113,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") include(":common") include(":common:ui-charts") +include(":common:routing") // region Core modules include(":core:analytics") From 7a10191880a5b72e3558f881785e635df7fa5830 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:52:15 +0400 Subject: [PATCH 038/223] Updated on 2026-08-14 --- .../tap/di/routing/RoutingComponentModule.kt | 18 ++ .../tangem/tap/routing/RoutingComponent.kt | 33 ++++ .../routing/impl/DefaultRoutingComponent.kt | 49 ++++++ .../tangem/tap/routing/utils/ChildFactory.kt | 155 ++++++++++++++++++ .../tangem/tap/routing/utils/RouteMappers.kt | 39 +++++ .../details/component/DetailsComponent.kt | 5 +- 6 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt diff --git a/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt new file mode 100644 index 0000000000..aca0f1678a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.routing + +import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.impl.DefaultRoutingComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal interface RoutingComponentModule { + + @Binds + @ActivityScoped + fun bindRoutingComponentFactory(factory: DefaultRoutingComponent.Factory): RoutingComponent.Factory +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt new file mode 100644 index 0000000000..a7f5148b19 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.routing + +import android.content.Intent +import androidx.fragment.app.Fragment +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.value.Value +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.navigation.Router +import com.tangem.utils.Provider + +internal interface RoutingComponent { + + val router: Router + + val stack: Value> + + sealed class Child { + + data object Initial : Child() + + data class LegacyFragment( + val name: String, + val fragmentProvider: Provider, + ) : Child() + + data class LegacyIntent(val intent: Intent) : Child() + } + + interface Factory { + fun create(context: AppComponentContext): RoutingComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt new file mode 100644 index 0000000000..ece22bfeb5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt @@ -0,0 +1,49 @@ +package com.tangem.tap.routing.impl + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.backhandler.BackCallback +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.getOrCreateTyped +import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.tap.routing.utils.ChildFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("LongParameterList") +internal class DefaultRoutingComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + private val childFactory: ChildFactory, +) : RoutingComponent, AppComponentContext by context { + + private val backCallback = BackCallback(priority = Int.MIN_VALUE, onBack = router::pop) + + override val stack: Value> = childStack( + source = navigationProvider.getOrCreateTyped(), + serializer = AppRoute.serializer(), + initialConfiguration = getInitialRoute(), + handleBackButton = false, + childFactory = ::child, + ) + + init { + backHandler.register(backCallback) + } + + private fun getInitialRoute(): AppRoute = AppRoute.Initial + + private fun child(route: AppRoute, context: ComponentContext): Child { + return childFactory.createChild(route, { childByContext(context) }) + } + + @AssistedFactory + interface Factory : RoutingComponent.Factory { + override fun create(context: AppComponentContext): DefaultRoutingComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt new file mode 100644 index 0000000000..6e3ac96e92 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -0,0 +1,155 @@ +package com.tangem.tap.routing.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.feature.referral.ReferralFragment +import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.features.wallet.navigation.WalletRouter +import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment +import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment +import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment +import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment +import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment +import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment +import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment +import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment +import com.tangem.tap.features.home.HomeFragment +import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment +import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment +import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment +import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment +import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment +import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment +import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment +import com.tangem.tap.features.welcome.ui.WelcomeFragment +import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.utils.Provider +import dagger.hilt.android.scopes.ActivityScoped +import java.util.WeakHashMap +import javax.inject.Inject + +@ActivityScoped +@Suppress("LongParameterList") +internal class ChildFactory @Inject constructor( + private val detailsComponentFactory: DetailsComponent.Factory, + private val sendRouter: SendRouter, + private val tokenDetailsRouter: TokenDetailsRouter, + private val walletRouter: WalletRouter, + private val qrScanningRouter: QrScanningRouter, + private val stakingRouter: StakingRouter, +) { + + @Suppress("LongMethod", "CyclomaticComplexMethod") + fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { + componentContexts[route] = contextFactory(route) + + return when (route) { + is AppRoute.Initial -> { + Child.Initial + } + is AppRoute.AccessCodeRecovery -> { + route.asFragmentChild(Provider { AccessCodeRecoveryFragment() }) + } + is AppRoute.AddCustomToken -> { + route.asFragmentChild(Provider { AddCustomTokenFragment() }) + } + is AppRoute.AppCurrencySelector -> { + route.asFragmentChild(Provider { AppCurrencySelectorFragment() }) + } + is AppRoute.ModalNotification -> { + route.asFragmentChild(Provider { ModalNotificationBottomSheetFragment() }) + } + is AppRoute.SaveWallet -> { + route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() }) + } + is AppRoute.Send -> { + route.asFragmentChild(Provider { sendRouter.getEntryFragment() }) + } + is AppRoute.AppSettings -> { + route.asFragmentChild(Provider { AppSettingsFragment() }) + } + is AppRoute.CardSettings -> { + route.asFragmentChild(Provider { CardSettingsFragment() }) + } + is AppRoute.Details -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DetailsComponent.Params(route.userWalletId), + componentFactory = detailsComponentFactory, + ) + } + is AppRoute.DetailsSecurity -> { + route.asFragmentChild(Provider { SecurityModeFragment() }) + } + is AppRoute.Disclaimer -> { + route.asFragmentChild(Provider { DisclaimerFragment() }) + } + is AppRoute.Home -> { + route.asFragmentChild(Provider { HomeFragment() }) + } + is AppRoute.ManageTokens -> { + route.asFragmentChild(Provider { TokensListFragment() }) + } + is AppRoute.OnboardingNote -> { + route.asFragmentChild(Provider { OnboardingNoteFragment() }) + } + is AppRoute.OnboardingOther -> { + route.asFragmentChild(Provider { OnboardingOtherCardsFragment() }) + } + is AppRoute.OnboardingTwins -> { + route.asFragmentChild(Provider { OnboardingTwinsFragment() }) + } + is AppRoute.OnboardingWallet -> { + route.asFragmentChild(Provider { OnboardingWalletFragment() }) + } + is AppRoute.QrScanning -> { + route.asFragmentChild(Provider { qrScanningRouter.getEntryFragment() }) + } + is AppRoute.ReferralProgram -> { + route.asFragmentChild(Provider { ReferralFragment() }) + } + is AppRoute.ResetToFactory -> { + route.asFragmentChild(Provider { ResetCardFragment() }) + } + is AppRoute.Swap -> { + route.asFragmentChild(Provider { SwapFragment() }) + } + is AppRoute.Wallet -> { + route.asFragmentChild(Provider { walletRouter.getEntryFragment() }) + } + is AppRoute.WalletConnectSessions -> { + route.asFragmentChild(Provider { WalletConnectFragment() }) + } + is AppRoute.CurrencyDetails -> { + route.asFragmentChild(Provider { tokenDetailsRouter.getEntryFragment() }) + } + is AppRoute.Welcome -> { + route.asFragmentChild(Provider { WelcomeFragment() }) + } + is AppRoute.TesterMenu -> { + TODO("Will be implemented later") + } + is AppRoute.Staking -> { + route.asFragmentChild(Provider { stakingRouter.getEntryFragment() }) + } + } + } + + private fun contextProvider( + appRoute: AppRoute, + contextFactory: (route: AppRoute) -> AppComponentContext, + ): Provider = Provider { + componentContexts.getOrPut(appRoute) { contextFactory(appRoute) } + } + + private companion object { + + val componentContexts = WeakHashMap() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt new file mode 100644 index 0000000000..c1203a4758 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.routing.utils + +import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.RouteBundleParams +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.ComposableContentComponent +import com.tangem.tap.DecomposeFragment +import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.utils.Provider + +internal fun AppRoute.asFragmentChild(fragmentProvider: Provider): Child { + val provider = Provider { + val bundle = (this as? RouteBundleParams)?.getBundle() + + fragmentProvider().apply { + arguments = bundle + } + } + + return Child.LegacyFragment(path, provider) +} + +internal fun > AppRoute.asComponentChild( + contextProvider: Provider, + params: P, + componentFactory: F, +): Child { + val fragmentProvider = Provider { + DecomposeFragment.newInstance( + contextProvider = contextProvider, + params = params, + componentFactory = componentFactory, + ) + } + + return Child.LegacyFragment(path, fragmentProvider) +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index 2aa9daebfe..da63581436 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.component import androidx.compose.material3.SnackbarHostState import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId @@ -9,9 +10,9 @@ interface DetailsComponent : ComposableContentComponent { val snackbarHostState: SnackbarHostState - interface Factory { + interface Factory : ComponentFactory { - fun create(context: AppComponentContext, params: Params): DetailsComponent + override fun create(context: AppComponentContext, params: Params): DetailsComponent } data class Params( From e4e18ffddd525ee6f2bbfddfd982cce8bed3eaa7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 15:54:46 +0400 Subject: [PATCH 039/223] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 + .../main/java/com/tangem/tap/MainActivity.kt | 41 +++++++++++++ .../java/com/tangem/tap/TangemApplication.kt | 5 ++ .../tap/common/extensions/Navigation.kt | 44 +++++++++++++ .../tangem/tap/di/routing/AppRouterModule.kt | 26 ++++++++ .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../com/tangem/tap/routing/ProxyAppRouter.kt | 61 +++++++++++++++++++ .../routing/configurator/AppRouterConfig.kt | 12 ++++ .../configurator/MutableAppRouterConfig.kt | 12 ++++ common/routing/build.gradle.kts | 3 +- .../com/tangem/common/routing/AppRouter.kt | 20 +++++- 11 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt create mode 100644 app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 45dae620de..6f45b6b45b 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -2,6 +2,7 @@ package com.tangem.tap import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.navigation.share.ShareManager @@ -116,4 +117,6 @@ interface ApplicationEntryPoint { fun getUrlOpener(): UrlOpener fun getShareManager(): ShareManager + + fun getAppRouter(): AppRouter } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 70192d4bb6..dfd4c4cc91 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -27,9 +27,13 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding +import com.arkivanov.decompose.value.observe +import com.arkivanov.essenty.lifecycle.asEssentyLifecycle import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction @@ -60,6 +64,7 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -72,6 +77,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction +import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.wallet.BuildConfig import com.tangem.wallet.R @@ -162,6 +169,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var stakingRouter: StakingRouter + @Inject + @RootAppComponentContext + internal lateinit var rootComponentContext: AppComponentContext + + @Inject + internal lateinit var appRouterConfig: AppRouterConfig + + @Inject + internal lateinit var routingComponentFactory: RoutingComponent.Factory + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -188,6 +205,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac observeAppThemeModeUpdates() setContentView(R.layout.activity_main) + installRouting() initContent() checkForNotificationPermission() @@ -199,6 +217,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } + private fun installRouting() { + val routingComponent = routingComponentFactory.create( + context = rootComponentContext, + ) + + appRouterConfig.routerScope = lifecycleScope + appRouterConfig.componentRouter = routingComponent.router + + routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack -> + appRouterConfig.baclStack = childStack.backStack.map { it.configuration } + + when (val child = childStack.active.instance) { + is RoutingComponent.Child.Initial -> Unit + is RoutingComponent.Child.LegacyFragment -> { + supportFragmentManager.showFragmentAllowingStateLoss(child.name, child.fragmentProvider) + } + is RoutingComponent.Child.LegacyIntent -> { + startActivity(child.intent) + } + } + } + } + private fun observeStateUpdates() { viewModel.state .flowWithLifecycle(lifecycle) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 414c48552b..5ecb0622ca 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -13,6 +13,7 @@ import com.tangem.LogFormat import com.tangem.TangemSdkLogger import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager @@ -193,6 +194,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val shareManager get() = entryPoint.getShareManager() + + private val appRouter: AppRouter + get() = entryPoint.getAppRouter() // endregion override fun onCreate() { @@ -284,6 +288,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { detailsEntryPoint = detailsEntryPoint, urlOpener = urlOpener, shareManager = shareManager, + appRouter = appRouter, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 70631857fc..8282d27be6 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -28,9 +28,53 @@ import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import timber.log.Timber +fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider) { + Timber.d("Showing $name route") + + val isPoppedBack = popBackStackImmediate(name, 0) + + if (!isPoppedBack) { + val fragment = fragmentProvider() + + if (fragment is DialogFragment) { + fragment.showDialog(fragmentManager = this, name) + } else { + fragment.showFragment(fragmentManager = this, name) + } + + Timber.d("Route $name is shown") + } else { + Timber.d("Route $name is found in backstack and shown") + } +} + +private fun DialogFragment.showDialog(fragmentManager: FragmentManager, name: String) { + val transaction = fragmentManager.beginTransaction() + + try { + transaction.addToBackStack(name) + show(transaction, name) + } catch (e: IllegalStateException) { + transaction.add(this, name) + transaction.addToBackStack(name) + + transaction.commitAllowingStateLoss() + } +} + +private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) { + val transaction = fragmentManager.beginTransaction() + + transaction.replace(R.id.fragment_container, this, name) + transaction.addToBackStack(name) + + transaction.commitAllowingStateLoss() +} + fun FragmentActivity.openFragment( screen: AppScreen, addToBackstack: Boolean, diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt new file mode 100644 index 0000000000..4f4aa7e79b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.di.routing + +import com.tangem.common.routing.AppRouter +import com.tangem.tap.routing.ProxyAppRouter +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.tap.routing.configurator.MutableAppRouterConfig +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 AppRouterModule { + + @Provides + @Singleton + fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter = + ProxyAppRouter(config, dispatchers) + + @Provides + @Singleton + fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig() +} \ No newline at end of file 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 08d6c0997f..08744d3939 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.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.email.EmailSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener @@ -85,4 +86,5 @@ data class DaggerGraphState( val stakingRouter: StakingRouter? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, + val appRouter: AppRouter? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt new file mode 100644 index 0000000000..aca4ce8efb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -0,0 +1,61 @@ +package com.tangem.tap.routing + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.decompose.navigation.Router +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + +internal class ProxyAppRouter( + private val config: AppRouterConfig, + private val dispatchers: CoroutineDispatcherProvider, +) : AppRouter { + + private val routerScope: CoroutineScope + get() = requireNotNull(config.routerScope) { + "Router scope is not set in config" + } + + private val innerRouter: Router + get() = requireNotNull(config.componentRouter) { + "Inner router is not set in config" + } + + override val stack: List + get() = requireNotNull(config.stack) { + "Stack is not set in config" + } + + override fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.push(route, onComplete) + } + } + + override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.replaceAll(*routes, onComplete = onComplete) + } + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.pop(onComplete) + } + } + + override fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.popTo(route, onComplete) + } + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.popTo(routeClass, onComplete) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt b/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt new file mode 100644 index 0000000000..470c85f674 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.routing.configurator + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.navigation.Router +import kotlinx.coroutines.CoroutineScope + +internal interface AppRouterConfig { + + var routerScope: CoroutineScope? + var componentRouter: Router? + var stack: List? +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt b/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt new file mode 100644 index 0000000000..ae724ac04e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.routing.configurator + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.navigation.Router +import kotlinx.coroutines.CoroutineScope + +internal class MutableAppRouterConfig : AppRouterConfig { + + override var routerScope: CoroutineScope? = null + override var componentRouter: Router? = null + override var stack: List? = null +} \ No newline at end of file diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index 980934bf31..ae4552a1f0 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -17,9 +17,10 @@ dependencies { implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.staking.models) /* Libs - Other */ + api(deps.kotlin.serialization) implementation(deps.androidx.core.ktx) - implementation(deps.kotlin.serialization) implementation(deps.timber) } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt index 9e81df0ec5..cb0f21a793 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt @@ -15,7 +15,7 @@ interface AppRouter { /** * The current navigation stack. */ - val backStack: List + val stack: List /** * Pushes a new route to the navigation stack. @@ -25,6 +25,14 @@ interface AppRouter { */ fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** + * Replaces ***all*** routes in the navigation stack with the specified [routes]. + * + * @param routes The routes to replace the current stack with. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** * Pops the top route from the navigation stack. * @@ -33,7 +41,15 @@ interface AppRouter { fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) /** - * Pops routes from the navigation stack until the specified route class is found. + * Pops routes from the navigation stack until the specified [route] is found. + * + * @param route The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the ***first*** specified [routeClass] is found. * * @param routeClass The route class to pop to. * @param onComplete The callback to be invoked when the operation is complete. From e5e35519b94981bd54c14b6413358969733af705 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 12:29:33 +0400 Subject: [PATCH 040/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 19 +++---- .../com/tangem/tap/common/extensions/Store.kt | 5 ++ .../domain/scanCard/LegacyScanProcessor.kt | 12 ++-- .../domain/scanCard/UseCaseScanProcessor.kt | 9 ++- .../chains/CheckForOnboardingChain.kt | 8 +-- .../domain/scanCard/chains/DisclaimerChain.kt | 4 +- .../scanCard/chains/ScanChainException.kt | 4 +- .../com/tangem/tap/features/BaseFragment.kt | 5 +- .../routers/DefaultCustomTokenRouter.kt | 12 ++-- .../details/redux/DetailsMiddleware.kt | 33 ++++++----- .../features/details/redux/DetailsReducer.kt | 6 +- .../walletconnect/WalletConnectMiddleware.kt | 17 ++---- .../AppCurrencySelectorViewModel.kt | 9 +-- .../ui/appsettings/AppSettingsFragment.kt | 6 +- .../ui/appsettings/AppSettingsViewModel.kt | 7 ++- .../ui/cardsettings/CardSettingsFragment.kt | 6 +- .../AccessCodeRecoveryFragment.kt | 5 +- .../details/ui/details/DetailsFragment.kt | 5 +- .../details/ui/details/DetailsViewModel.kt | 22 ++++---- .../details/ui/resetcard/ResetCardFragment.kt | 5 +- .../ui/resetcard/ResetCardViewModel.kt | 10 ++-- .../ui/securitymode/SecurityModeFragment.kt | 5 +- .../ui/walletconnect/WalletConnectFragment.kt | 5 +- .../dialogs/ClipboardOrScanQrDialog.kt | 17 ++---- .../disclaimer/redux/DisclaimerAction.kt | 3 +- .../disclaimer/redux/DisclaimerMiddleware.kt | 11 ++-- .../disclaimer/redux/DisclaimerReducer.kt | 2 +- .../disclaimer/redux/DisclaimerSource.kt | 5 ++ .../disclaimer/redux/DisclaimerState.kt | 4 +- .../disclaimer/ui/DisclaimerFragment.kt | 12 ++-- .../tangem/tap/features/home/HomeFragment.kt | 7 ++- .../tap/features/home/redux/HomeMiddleware.kt | 9 ++- .../tangem/tap/features/main/MainViewModel.kt | 10 ++-- .../features/onboarding/OnboardingHelper.kt | 20 +++---- .../note/redux/OnboardingNoteMiddleware.kt | 5 +- .../twins/redux/TwinCardsMiddleware.kt | 19 ++++--- .../redux/OnboardingWalletMiddleware.kt | 20 +++---- .../wallet/ui/OnboardingWalletFragment.kt | 4 +- .../saveWallet/redux/SaveWalletMiddleware.kt | 30 +++++----- .../send/redux/middlewares/SendMiddleware.kt | 6 +- .../tap/features/send/ui/SendFragment.kt | 21 +++---- .../tap/features/send/ui/SendViewModel.kt | 4 +- .../router/DefaultTokensListRouter.kt | 12 ++-- .../viewmodels/TokensListMigration.kt | 8 +-- .../viewmodels/TokensListViewModel.kt | 8 +-- .../middlewares/TradeCryptoMiddleware.kt | 56 +++++++++---------- .../welcome/redux/WelcomeMiddleware.kt | 11 ++-- .../features/welcome/ui/WelcomeFragment.kt | 4 -- .../features/welcome/ui/WelcomeViewModel.kt | 10 +++- 49 files changed, 266 insertions(+), 271 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index dfd4c4cc91..ed2a8d35af 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -19,7 +19,6 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle @@ -31,11 +30,12 @@ import com.arkivanov.decompose.value.observe import com.arkivanov.essenty.lifecycle.asEssentyLifecycle import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent @@ -64,6 +64,7 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.sdk.TangemSdkManager @@ -74,7 +75,6 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.RoutingComponent @@ -518,16 +518,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { - store.dispatch( - NavigationAction.NavigateTo( - screen = AppScreen.Welcome, - bundle = intentWhichStartedActivity?.let { - bundleOf(WelcomeFragment.INITIAL_INTENT_KEY to it) - }, - ), - ) + store.dispatchNavigationAction { + push(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) + } } else { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) + store.dispatchNavigationAction { push(AppRoute.Home) } lifecycleScope.launch { intentProcessor.handleIntent(intentWhichStartedActivity, false) } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 6c7ab7e524..27fc3c3bc3 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.extensions +import com.tangem.common.routing.AppRouter import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet @@ -89,6 +90,10 @@ fun Store.dispatchShare(url: String) { inject(DaggerGraphState::shareManager).shareText(url) } +fun Store.dispatchNavigationAction(action: AppRouter.() -> Unit) { + inject(DaggerGraphState::appRouter).action() +} + inline fun Store.inject(getDependency: DaggerGraphState.() -> T?): T { return requireNotNull(state.daggerGraphState.getDependency()) { "${T::class.simpleName} isn't initialized " 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 ce649ece46..0bbdc3f5f4 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 @@ -5,12 +5,11 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned @@ -21,6 +20,7 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep @@ -118,7 +118,7 @@ internal object LegacyScanProcessor { disclaimerWillShow() store.dispatchWithMain( DisclaimerAction.Show( - fromScreen = AppScreen.Home, + from = DisclaimerSource.Home, callback = DisclaimerCallback( onAccept = { scope.launch(Dispatchers.Main) { @@ -170,7 +170,7 @@ internal object LegacyScanProcessor { if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) - navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) } + navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } } else { delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(scanResponse) @@ -178,9 +178,9 @@ internal object LegacyScanProcessor { } } - private suspend inline fun navigateTo(screen: AppScreen, onProgressStateChange: (showProgress: Boolean) -> Unit) { + private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) { delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + store.dispatchNavigationAction { push(route) } onProgressStateChange(false) } } \ No newline at end of file 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 bdb9786c3c..10646e2bfa 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,16 +3,15 @@ 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.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.scanCard.chains.* import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter @@ -132,8 +131,8 @@ internal object UseCaseScanProcessor { action() } - private suspend inline fun navigateTo(screen: AppScreen) { + private suspend inline fun navigateTo(route: AppRoute) { delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + store.dispatchNavigationAction { push(route) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 66f708e28d..72d0b6ee59 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -2,8 +2,8 @@ package com.tangem.tap.domain.scanCard.chains import arrow.core.left import arrow.core.right +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen import com.tangem.domain.card.ScanCardException import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned @@ -55,8 +55,8 @@ class CheckForOnboardingChain( canSkipBackup = previousChainResult.card.canSkipBackup, ), ) - val appScreen = OnboardingHelper.whereToNavigate(previousChainResult) - ScanChainException.OnboardingNeeded(appScreen).left() + val route = OnboardingHelper.whereToNavigate(previousChainResult) + ScanChainException.OnboardingNeeded(route).left() } else -> { Analytics.setContext(previousChainResult) @@ -69,7 +69,7 @@ class CheckForOnboardingChain( store.dispatchOnMain( TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), ) - ScanChainException.OnboardingNeeded(AppScreen.OnboardingTwins).left() + ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() } else { delay(DELAY_SDK_DIALOG_CLOSE) previousChainResult.right() diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt index 35ad2efbab..8aa003b952 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt @@ -2,7 +2,6 @@ package com.tangem.tap.domain.scanCard.chains import arrow.core.left import arrow.core.right -import com.tangem.core.navigation.AppScreen import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain import com.tangem.domain.core.chain.ResultChain @@ -13,6 +12,7 @@ import com.tangem.tap.features.disclaimer.Disclaimer import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import kotlinx.coroutines.suspendCancellableCoroutine import org.rekotlin.Store import kotlin.coroutines.resume @@ -50,7 +50,7 @@ internal class DisclaimerChain( return suspendCancellableCoroutine { continuation -> store.dispatchOnMain( DisclaimerAction.Show( - fromScreen = AppScreen.Home, + from = DisclaimerSource.Home, callback = DisclaimerCallback( onAccept = { if (continuation.isActive) { diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt index ae5e9f8ad3..b36fb89f0e 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt @@ -1,6 +1,6 @@ package com.tangem.tap.domain.scanCard.chains -import com.tangem.core.navigation.AppScreen +import com.tangem.common.routing.AppRoute import com.tangem.domain.card.ScanCardException sealed class ScanChainException : ScanCardException.ChainException() { @@ -19,5 +19,5 @@ sealed class ScanChainException : ScanCardException.ChainException() { * * @param onboardingRoute route where to navigate * */ - data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException() + data class OnboardingNeeded(val onboardingRoute: AppRoute) : ScanChainException() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index d9e668bf04..6321957ee1 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -14,7 +14,8 @@ import androidx.lifecycle.Lifecycle import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.store import com.tangem.wallet.R @@ -36,7 +37,7 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP } override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt index 6391cc840f..858d49c31c 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt @@ -1,9 +1,11 @@ package com.tangem.tap.features.customtoken.impl.presentation.routers import com.tangem.blockchain.common.Blockchain -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.utils.popTo import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -12,11 +14,11 @@ import com.tangem.wallet.R internal class DefaultCustomTokenRouter : CustomTokenRouter { override fun popBackStack() { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } override fun openWalletScreen() { - store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } override fun openUnsupportedNetworkAlert(blockchain: Blockchain) { @@ -32,7 +34,7 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter { val alert = AppDialog.SimpleOkDialogRes( headerId = R.string.common_error, messageId = R.string.common_unknown_error, - onOk = { store.dispatch(NavigationAction.PopBackTo()) }, + onOk = { store.dispatchNavigationAction(AppRouter::pop) }, ) store.dispatchDialogShow(alert) } 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 a9f6bdca6d..d0ce3c2f74 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 @@ -5,9 +5,11 @@ import com.tangem.common.* import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -70,7 +72,8 @@ class DetailsMiddleware { is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) is DetailsAction.ReCreateTwinsWallet -> { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) + + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } } is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) is DetailsAction.ScanCard -> scanCard(state) @@ -90,7 +93,7 @@ class DetailsMiddleware { store.dispatch(DetailsAction.ReCreateTwinsWallet) return } else { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory)) + store.dispatchNavigationAction { push(AppRoute.ResetToFactory) } } } is DetailsAction.ResetToFactory.Proceed -> { @@ -131,15 +134,15 @@ class DetailsMiddleware { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync if (selectedUserWallet != null) { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } store.onUserWalletSelected(selectedUserWallet) } else { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { popTo() } } else { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } } @@ -163,7 +166,7 @@ class DetailsMiddleware { fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) { when (action) { is DetailsAction.ManageSecurity.OpenSecurity -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) + store.dispatchNavigationAction { push(AppRoute.DetailsSecurity) } } is DetailsAction.ManageSecurity.SaveChanges -> { val cardSettingsState = detailsState.cardSettingsState @@ -182,7 +185,7 @@ class DetailsMiddleware { is CompletionResult.Success -> { Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption)) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success) } is CompletionResult.Failure -> { @@ -392,7 +395,7 @@ class DetailsMiddleware { deleteSavedAccessCodes() store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } return CompletionResult.Success(Unit) } @@ -431,7 +434,7 @@ class DetailsMiddleware { when (action) { is DetailsAction.AccessCodeRecovery.Open -> { Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery)) + store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) } } is DetailsAction.AccessCodeRecovery.SaveChanges -> { scope.launch { @@ -443,7 +446,7 @@ class DetailsMiddleware { AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled), ), ) - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.dispatchOnMain( DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled), ) @@ -502,7 +505,7 @@ class DetailsMiddleware { store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) }, disclaimerWillShow = { - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, onSuccess = { scanResponse -> createUserWallet(scanResponse) @@ -546,7 +549,7 @@ class DetailsMiddleware { store.onUserWalletSelected(userWallet) store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } .doOnFailure { error -> if (error is UserWalletsListError.WalletAlreadySaved) { @@ -554,7 +557,7 @@ class DetailsMiddleware { store.onUserWalletSelected(userWallet) store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } else { Timber.e(error, "Unable to create user wallet") handleError(error, prevUseBiometricsForAccessCode) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index d8f6637f7f..d89b862c1b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.details.redux -import com.tangem.core.navigation.AppScreen +import com.tangem.common.routing.AppRoute import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver @@ -69,10 +69,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { } private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState { + val router = store.inject(DaggerGraphState::appRouter) + return DetailsState( scanResponse = action.scanResponse, // If the current screen is ResetToFactory, we must save the ResetToFactory's state - cardSettingsState = if (store.state.navigationState.backStack.lastOrNull() == AppScreen.ResetToFactory) { + cardSettingsState = if (router.backStack.lastOrNull() is AppRoute.ResetToFactory) { state.detailsState.cardSettingsState } else { null 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 b4917ceabb..f55288968a 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 @@ -1,19 +1,17 @@ package com.tangem.tap.features.details.redux.walletconnect -import androidx.core.os.bundleOf import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject 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.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError @@ -62,14 +60,7 @@ class WalletConnectMiddleware { // TODO check store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { - store.dispatchOnMain( - NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT, - ), - ), - ) + store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) } } } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt index 7b07e7d471..41d27d02a9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt @@ -2,8 +2,9 @@ package com.tangem.tap.features.details.ui.appcurrency import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.ReduxNavController + import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase @@ -20,7 +21,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase, private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase, - private val reduxNavController: ReduxNavController, + private val router: AppRouter, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), AppCurrencySelectorIntents { @@ -34,7 +35,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor( val uiState: StateFlow = stateController.stateFlow override fun onBackClick() { - reduxNavController.popBackStack() + router.pop() } override fun onSearchClick() { @@ -52,7 +53,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor( analyticsEventHandler.send( event = Settings.AppSettings.MainCurrencyChanged(currencyType = currency.name), ) - reduxNavController.popBackStack() + router.pop() } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 2efc912cca..2d6e9d554c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -6,10 +6,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter + import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -36,7 +38,7 @@ internal class AppSettingsFragment : ComposeFragment() { state = state, onBackClick = { store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 1a473c49ec..60f7de63fd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -4,13 +4,14 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.details.redux.AppSetting @@ -128,7 +129,7 @@ internal class AppSettingsViewModel @Inject constructor( } private fun showAppCurrencySelector() { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppCurrencySelector)) + store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) } } private fun showThemeModeSelector(selectedMode: AppThemeMode) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index c81e2d819e..6fc835cc9c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -4,9 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.viewModels -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter + import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -29,7 +31,7 @@ internal class CardSettingsFragment : ComposeFragment() { state = viewModel.screenState.value, onBackClick = { store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt index 7a365126b6..3beea0503a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt @@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -28,7 +29,7 @@ class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber() } } else { - store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index c44b9ca3fa..f61e54fd78 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -29,7 +30,7 @@ internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber
- store.dispatch( - NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT, - ), - ), - ) + store.dispatchNavigationAction { + push(AppRoute.QrScanning(source = SourceType.WALLET_CONNECT)) + } } setOnDismissListener { store.dispatch(GlobalAction.HideDialog) diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt index 52e3057f46..9611dc80b5 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.disclaimer.redux -import com.tangem.core.navigation.AppScreen import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.features.disclaimer.Disclaimer import org.rekotlin.Action @@ -10,7 +9,7 @@ sealed class DisclaimerAction : Action { data class SetDisclaimer(val disclaimer: Disclaimer) : DisclaimerAction() data class Show( - val fromScreen: AppScreen, + val from: DisclaimerSource, val callback: DisclaimerCallback? = null, ) : DisclaimerAction() diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index 3e152187ea..20c8df7fa9 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -1,7 +1,8 @@ package com.tangem.tap.features.disclaimer.redux -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.mainScope import com.tangem.tap.store @@ -25,17 +26,17 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { when (action) { is DisclaimerAction.Show -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + store.dispatchNavigationAction { push(AppRoute.Disclaimer) } } is DisclaimerAction.AcceptDisclaimer -> { mainScope.launch { state.disclaimer.accept() - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) state.callback?.onAccept?.invoke() } } is DisclaimerAction.OnBackPressed -> { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) state.callback?.onDismiss?.invoke() } } diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt index 6c2205ca01..9b655697ba 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt @@ -17,7 +17,7 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState { disclaimer = action.disclaimer, ) is DisclaimerAction.Show -> disclaimerState.copy( - showedFromScreen = action.fromScreen, + showedFrom = action.from, callback = action.callback, ) is DisclaimerAction.AcceptDisclaimer, is DisclaimerAction.OnBackPressed -> disclaimerState.copy( diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt new file mode 100644 index 0000000000..cdfcbbdb90 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.disclaimer.redux + +enum class DisclaimerSource { + Home, Details +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt index 8d37c385eb..c5ccfc7cfc 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.disclaimer.redux import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.AppScreen + import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.features.disclaimer.Disclaimer import com.tangem.tap.features.disclaimer.DummyDisclaimer @@ -9,7 +9,7 @@ import org.rekotlin.StateType data class DisclaimerState( val disclaimer: Disclaimer = DummyDisclaimer(), - val showedFromScreen: AppScreen = AppScreen.Home, + val showedFrom: DisclaimerSource = DisclaimerSource.Home, val callback: DisclaimerCallback? = null, val progressState: ProgressState? = null, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index d6767e13ac..9d217e6111 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -7,7 +7,7 @@ import android.webkit.WebView import androidx.lifecycle.lifecycleScope import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.core.navigation.AppScreen + import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.common.extensions.beginDelayedTransition @@ -18,6 +18,7 @@ import com.tangem.tap.features.BaseFragment import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.disclaimer.Disclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.store import com.tangem.wallet.R @@ -76,17 +77,14 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs override fun configureTransitions() { val inflater = TransitionInflater.from(requireContext()) - when (store.state.disclaimerState.showedFromScreen) { - AppScreen.Home -> { + when (store.state.disclaimerState.showedFrom) { + DisclaimerSource.Home -> { enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom) exitTransition = inflater.inflateTransition(android.R.transition.slide_top) } - AppScreen.Details -> { + DisclaimerSource.Details -> { super.configureTransitions() } - else -> { - /* no-op */ - } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 8e081474fb..e27f58f4a3 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -9,14 +9,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction @@ -86,7 +87,7 @@ class HomeFragment : ComposeFragment(), StoreSubscriber { }, onSearchTokensClick = { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.ManageTokens)) + store.dispatchNavigationAction { push(AppRoute.ManageTokens) } store.dispatch(TokensAction.SetArgs.ReadAccess) }, ) 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 17e8d7dd0a..ad934cdebd 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 @@ -4,11 +4,10 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder @@ -119,7 +118,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { scope.launch { store.onUserWalletSelected(userWallet = userWallet) } } .doOnResult { - navigateTo(AppScreen.Wallet) + navigateTo(AppRoute.Wallet) } } @@ -143,8 +142,8 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { } } -private suspend fun navigateTo(appScreen: AppScreen) { - store.dispatchOnMain(NavigationAction.NavigateTo(appScreen)) +private suspend fun navigateTo(route: AppRoute) { + store.dispatchNavigationAction { push(route) } delay(HIDE_PROGRESS_DELAY) store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index c900ea3b50..8d567fc5a5 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -3,11 +3,11 @@ package com.tangem.tap.features.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController + import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -36,7 +36,7 @@ import javax.inject.Inject internal class MainViewModel @Inject constructor( private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, - private val reduxNavController: ReduxNavController, + private val router: AppRouter, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, @@ -152,7 +152,7 @@ internal class MainViewModel @Inject constructor( if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) { listenToFlipsUseCase.changeUpdateEnabled(false) stateHolder.updateWithHiddenBalancesNotification() - reduxNavController.navigate(NavigationAction.NavigateTo(AppScreen.ModalNotification)) + router.push(AppRoute.ModalNotification) } } .launchIn(viewModelScope) 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 f03dc73425..f8fb8f2ba8 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 @@ -3,10 +3,10 @@ package com.tangem.tap.features.onboarding import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.CardDTO @@ -56,18 +56,18 @@ object OnboardingHelper { } } - fun whereToNavigate(scanResponse: ScanResponse): AppScreen { + fun whereToNavigate(scanResponse: ScanResponse): AppRoute { return when (val type = scanResponse.productType) { - ProductType.Note -> AppScreen.OnboardingNote + ProductType.Note -> AppRoute.OnboardingNote ProductType.Wallet, ProductType.Wallet2, ProductType.Ring, -> if (scanResponse.card.settings.isBackupAllowed) { - AppScreen.OnboardingWallet + AppRoute.OnboardingWallet() } else { - AppScreen.OnboardingOther + AppRoute.OnboardingOther } - ProductType.Twins -> AppScreen.OnboardingTwins + ProductType.Twins -> AppRoute.OnboardingTwins ProductType.Start2Coin, ProductType.Visa, -> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported") @@ -111,14 +111,14 @@ object OnboardingHelper { backupCardsIds = backupCardsIds?.toSet(), ), ) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } delay(timeMillis = 1_800) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet)) + store.dispatchNavigationAction { push(AppRoute.SaveWallet) } } // If device has no biometry and save wallet screen has been shown, then go through old scenario else -> { proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } } } } 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 ef5296a6b4..ef320383b2 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 @@ -2,8 +2,9 @@ package com.tangem.tap.features.onboarding.products.note.redux import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.NavigationAction + import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext @@ -204,7 +205,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch OnboardingDialog.InterruptOnboarding( onOk = { OnboardingHelper.onInterrupted() - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ), ) 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 033b790e84..2bc6f07ce1 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 @@ -3,9 +3,10 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.extensions.Result import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext @@ -39,6 +40,7 @@ import kotlinx.coroutines.runBlocking import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware +import kotlin.reflect.KClass object TwinCardsMiddleware { val handler = twinsWalletMiddleware @@ -328,7 +330,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { if (walletsRepository.shouldSaveUserWalletsSync()) { OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } else { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } } @@ -352,7 +354,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { store.dispatch(TwinCardsAction.CardsManager.Release) action.shouldResetTwinCardsWidget(shouldReturnCardBack) { - store.dispatchOnMain(NavigationAction.PopBackTo(getPopBackScreen())) + store.dispatchNavigationAction { popTo(routeClass = getPopBackScreen()) } } } store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback)) @@ -362,18 +364,19 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } } -private fun getPopBackScreen(): AppScreen { +private fun getPopBackScreen(): KClass { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) return if (userWalletsListManager.hasUserWallets) { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) + if (isLocked) { - AppScreen.Welcome + AppRoute.Welcome::class } else { - AppScreen.Wallet + AppRoute.Wallet::class } } else { - AppScreen.Home + AppRoute.Home::class } } \ No newline at end of file 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 925dc9bcc2..efa987c847 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 @@ -7,16 +7,16 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.common.extensions.ifNotNull import com.tangem.common.extensions.toHexString +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction 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.wallets.models.Artwork import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.Artwork import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator @@ -25,10 +25,7 @@ import com.tangem.operations.backup.BackupService import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -164,7 +161,7 @@ private fun handleWalletAction(action: Action) { store.dispatch(GlobalAction.Onboarding.Stop) if (scanResponse == null) { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.dispatch(HomeAction.ReadCard(scope = action.scope)) } else { val backupState = store.state.onboardingWalletState.backupState @@ -529,7 +526,8 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) addedBackupCardsCount = backupService.addedBackupCardsCount, ), ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + + store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) } } is BackupAction.SkipBackup -> { Analytics.send(Onboarding.Backup.Skipped()) @@ -614,7 +612,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) { } BackupStep.Finished -> { OnboardingHelper.onInterrupted() - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } } } @@ -625,7 +623,7 @@ private fun showInterruptOnboardingDialog() { onOk = { OnboardingHelper.onInterrupted() store.dispatch(BackupAction.DiscardBackup) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index b2d51cca3a..b66984164b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -21,13 +21,13 @@ import com.google.android.material.tabs.TabLayoutMediator import com.tangem.common.CardIdFormatter import com.tangem.common.CompletionResult import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse -import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter @@ -66,7 +66,7 @@ class OnboardingWalletFragment : internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer } - private val canSkipBackup by lazy { arguments?.getBoolean(OnboardingRouter.CAN_SKIP_BACKUP) ?: true } + private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true } private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler 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 932c1a1965..541ceefdcc 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 @@ -3,26 +3,22 @@ package com.tangem.tap.features.saveWallet.redux import com.tangem.common.* import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch import org.rekotlin.Middleware -import org.rekotlin.Store import timber.log.Timber internal class SaveWalletMiddleware { @@ -80,7 +76,7 @@ internal class SaveWalletMiddleware { Timber.e(error, "Unable to save user wallet") } .doOnSuccess { mainScope.launch { store.onUserWalletSelected(userWallet) } } - .doOnResult { store.navigateToWallet() } + .doOnResult { navigateToWallet() } } } @@ -130,7 +126,7 @@ internal class SaveWalletMiddleware { ) store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } private fun dismiss(state: SaveWalletState) { @@ -159,13 +155,13 @@ internal class SaveWalletMiddleware { } } - private suspend fun Store.navigateToWallet() { - dispatchWithMain( - if (store.state.navigationState.backStack.contains(AppScreen.Wallet)) { - NavigationAction.PopBackTo(AppScreen.Wallet) - } else { - NavigationAction.NavigateTo(AppScreen.Wallet) - }, - ) + private fun navigateToWallet() { + store.dispatchNavigationAction { + popTo { isSuccess -> + if (!isSuccess) { + clear { push(AppRoute.Wallet) } + } + } + } } } \ No newline at end of file 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 9067bffe39..b1be6e7b39 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 @@ -14,10 +14,10 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.demo.DemoTransactionSender @@ -270,7 +270,7 @@ private fun sendTransaction( ), ) Analytics.sendSelectedCurrencyEvent(mainCurrencyType) - dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } } is SimpleResult.Failure -> { @@ -326,7 +326,7 @@ private fun sendTransaction( AppDialog.SimpleOkDialogRes( headerId = R.string.common_done, messageId = R.string.alert_demo_feature_disabled, - onOk = { dispatch(NavigationAction.PopBackTo()) }, + onOk = { store.dispatchNavigationAction(AppRouter::pop) }, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index fd6d7105d3..34650ad800 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -8,7 +8,6 @@ import android.text.method.DigitsKeyListener import android.view.View import android.view.inputmethod.EditorInfo import android.widget.EditText -import androidx.core.os.bundleOf import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener import androidx.fragment.app.viewModels @@ -20,19 +19,18 @@ import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.textfield.TextInputEditText import com.tangem.Message +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.common.extensions.setOnImeActionListener import com.tangem.tap.common.recyclerView.SpaceItemDecoration @@ -162,14 +160,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { imvQrCode.setOnClickListener { Analytics.send(Token.Send.ButtonQRCode()) - store.dispatchOnMain( - NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.SEND, - ), - ), - ) + store.dispatchNavigationAction { + push(AppRoute.QrScanning(source = SourceType.SEND)) + } } } @@ -370,7 +363,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun handleOnBackPressed() { val externalTransactionData = store.state.sendState.externalTransactionData if (externalTransactionData == null) { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } else { store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index 9f59533686..881e9f6df2 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase @@ -12,7 +13,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.di.DelayedWork import com.tangem.tap.features.send.redux.AddressActionUi @@ -52,7 +52,7 @@ internal class SendViewModel @Inject constructor( .launchIn(viewModelScope) } - private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + private val cryptoCurrency: CryptoCurrency? = savedStateHandle[AppRoute.Send.CRYPTO_CURRENCY_KEY] override fun onCreate(owner: LifecycleOwner) { getBalanceHidingSettingsUseCase() diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 27418f3d01..a62f01e74b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -1,9 +1,11 @@ package com.tangem.tap.features.tokens.impl.presentation.router import com.tangem.blockchain.common.Blockchain -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter + import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.store @@ -18,11 +20,11 @@ import com.tangem.wallet.R internal class DefaultTokensListRouter : TokensListRouter { override fun popBackStack() { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } override fun openAddCustomTokenScreen() { - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + store.dispatchNavigationAction { push(AppRoute.AddCustomToken) } } override fun showAddressCopiedNotification() { @@ -54,7 +56,7 @@ internal class DefaultTokensListRouter : TokensListRouter { val alert = AppDialog.SimpleOkDialogRes( headerId = R.string.common_error, messageId = R.string.common_unknown_error, - onOk = { store.dispatch(NavigationAction.PopBackTo()) }, + onOk = { store.dispatchNavigationAction(AppRouter::pop) }, ) store.dispatchDialogShow(alert) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index 050dc5f876..b10741f96c 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels import arrow.core.Either import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.common.util.derivationStyleProvider @@ -16,7 +16,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store @@ -126,7 +126,7 @@ internal class TokensListMigration( val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { store.dispatchDebugErrorNotification(message = "Nothing to save") - store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } return } 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 53867da367..f5dc7f92b4 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 @@ -11,9 +11,9 @@ import androidx.lifecycle.viewModelScope import androidx.paging.* import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getGreyedOutIconRes import com.tangem.domain.card.DerivePublicKeysUseCase @@ -26,7 +26,7 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType @@ -326,7 +326,7 @@ internal class TokensListViewModel @Inject constructor( ) uiState = state.copy(isSavingInProgress = false) - store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } } 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 39a0168df7..a4233d6ea8 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 @@ -1,18 +1,16 @@ package com.tangem.tap.features.wallet.redux.middlewares -import androidx.core.os.bundleOf import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.* @@ -150,7 +148,7 @@ object TradeCryptoMiddleware { } private fun openReceiptUrl(transactionId: String) { - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.state.globalState.exchangeManager.getSellCryptoReceiptUrl( action = CurrencyExchangeManager.Action.Sell, transactionId = transactionId, @@ -158,11 +156,7 @@ object TradeCryptoMiddleware { } private fun openSwap(currency: CryptoCurrency) { - val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to currency, - ) - - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) + store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) } } private fun handleSendToken(action: TradeCryptoAction.SendToken) { @@ -214,11 +208,12 @@ object TradeCryptoMiddleware { ) } - val bundle = bundleOf( - SendRouter.CRYPTO_CURRENCY_KEY to currency, - SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + val route = AppRoute.Send( + currency = currency, + userWalletId = action.userWallet.walletId, ) - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + + store.dispatchNavigationAction { push(route) } } } @@ -285,17 +280,17 @@ object TradeCryptoMiddleware { ) } - val bundle = bundleOf( - SendRouter.CRYPTO_CURRENCY_KEY to currency, - SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + val route = AppRoute.Send( + currency = currency, + userWalletId = action.userWallet.walletId, ) - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + store.dispatchNavigationAction { push(route) } } } private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { handleNewSend( - userWalletId = action.userWallet.walletId.stringValue, + userWalletId = action.userWallet.walletId, txInfo = action.transactionInfo, currency = action.tokenCurrency, ) @@ -303,25 +298,26 @@ object TradeCryptoMiddleware { private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) { handleNewSend( - userWalletId = action.userWallet.walletId.stringValue, + userWalletId = action.userWallet.walletId, txInfo = action.transactionInfo, currency = action.coinStatus.currency, ) } private fun handleNewSend( - userWalletId: String, + userWalletId: UserWalletId, txInfo: TradeCryptoAction.TransactionInfo?, currency: CryptoCurrency, ) { - val bundle = bundleOf( - SendRouter.CRYPTO_CURRENCY_KEY to currency, - SendRouter.USER_WALLET_ID_KEY to userWalletId, - SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId, - SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress, - SendRouter.AMOUNT_KEY to txInfo?.amount, - SendRouter.TAG_KEY to txInfo?.tag, + val route = AppRoute.Send( + currency = currency, + userWalletId = userWalletId, + transactionId = txInfo?.transactionId, + destinationAddress = txInfo?.destinationAddress, + amount = txInfo?.amount, + tag = txInfo?.tag, ) - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + + store.dispatchNavigationAction { push(route) } } } \ No newline at end of file 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 abfc205397..2a564e8a0d 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 @@ -6,11 +6,12 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.flatMap +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder @@ -98,7 +99,7 @@ internal class WelcomeMiddleware { signInType = Basic.SignedIn.SignInType.Biometric, ) - store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) store.onUserWalletSelected(userWallet = selectedUserWallet) @@ -130,7 +131,7 @@ internal class WelcomeMiddleware { .doOnSuccess { sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card) - store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) store.onUserWalletSelected(userWallet = userWallet) @@ -170,7 +171,7 @@ internal class WelcomeMiddleware { } .doOnResult { store.dispatchWithMain(WelcomeAction.CloseError) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 60ab61900f..41ef679b6a 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -88,8 +88,4 @@ internal class WelcomeFragment : ComposeFragment() { } } } - - internal companion object { - const val INITIAL_INTENT_KEY = "intent" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index bc672b7c2f..bb885005f9 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -1,8 +1,11 @@ package com.tangem.tap.features.welcome.ui -import android.content.Intent +import android.os.Bundle import androidx.lifecycle.* import com.tangem.common.core.TangemError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.Analytics import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.tap.common.analytics.events.SignIn @@ -26,7 +29,8 @@ internal class WelcomeViewModel @Inject constructor( StoreSubscriber, DefaultLifecycleObserver { - private val initialIntent: Intent? = savedStateHandle[WelcomeFragment.INITIAL_INTENT_KEY] + private val initialIntent: SerializableIntent? = savedStateHandle.get(AppRoute.Welcome.INITIAL_INTENT_KEY) + ?.let { it.unbundle(SerializableIntent.serializer()) } private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal @@ -38,7 +42,7 @@ internal class WelcomeViewModel @Inject constructor( initGlobalState() val welcomeAction = if (initialIntent != null) { - WelcomeAction.ProceedWithIntent(initialIntent) + WelcomeAction.ProceedWithIntent(initialIntent.toIntent()) } else { WelcomeAction.ProceedWithBiometrics() } From 5ea04db5d873f915dc330bc683e3e22e7e918a8b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 12:52:44 +0400 Subject: [PATCH 041/223] Updated on 2026-08-14 --- features/wallet/impl/build.gradle.kts | 1 + .../feature/wallet/di/WalletRouterModule.kt | 8 +-- .../router/DefaultWalletRouter.kt | 58 +++++++------------ .../presentation/router/InnerWalletRouter.kt | 4 +- .../presentation/wallet/ui/WalletScreen.kt | 3 +- .../intents/WalletCardClickIntents.kt | 17 ++---- 6 files changed, 36 insertions(+), 55 deletions(-) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 1c2c062dbd..7022b31c98 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.common.routing) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 536e255a98..4e031b0729 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.redux.ReduxStateHolder import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter @@ -18,10 +18,10 @@ internal object WalletRouterModule { @Provides @ActivityScoped fun provideWalletRouter( - reduxNavController: ReduxNavController, - reduxStateHolder: ReduxStateHolder, + appRouter: AppRouter, urlOpener: UrlOpener, + reduxStateHolder: ReduxStateHolder, ): WalletRouter { - return DefaultWalletRouter(reduxNavController, reduxStateHolder, urlOpener) + return DefaultWalletRouter(appRouter, urlOpener, reduxStateHolder) } } \ 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 535062c3ef..69906449ee 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 @@ -6,7 +6,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -16,31 +15,27 @@ 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.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.managetokens.navigation.ExpandableState import com.tangem.features.managetokens.navigation.ManageTokensUi -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates /** Default implementation of wallet feature router */ internal class DefaultWalletRouter( - private val reduxNavController: ReduxNavController, - private val reduxStateHolder: ReduxStateHolder, + private val router: AppRouter, private val urlOpener: UrlOpener, + private val reduxStateHolder: ReduxStateHolder, ) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() @@ -99,7 +94,7 @@ internal class DefaultWalletRouter( } @SuppressLint("RestrictedApi") - override fun popBackStack(screen: AppScreen?) { + override fun popBackStack() { /* * It's hack that avoid issue with closing the wallet screen. * We are using NavGraph only inside feature so first backstack's element is entry of NavGraph and @@ -107,11 +102,7 @@ internal class DefaultWalletRouter( * If backstack contains only NavGraph entry and wallet screen entry then we close the wallet fragment. */ if (navController.currentBackStack.value.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) { - if (screen != null) { - reduxNavController.navigate(action = NavigationAction.PopBackTo(screen)) - } else { - onFinish.invoke() - } + onFinish.invoke() } else { navController.popBackStack() } @@ -122,22 +113,16 @@ internal class DefaultWalletRouter( } override fun openDetailsScreen(selectedWalletId: UserWalletId) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.Details, - bundle = bundleOf( - DetailsEntryPoint.USER_WALLET_ID_KEY to selectedWalletId.stringValue, - ), + router.push( + AppRoute.Details( + userWalletId = selectedWalletId, ), ) } override fun openOnboardingScreen() { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.OnboardingWallet, - bundle = bundleOf(OnboardingRouter.CAN_SKIP_BACKUP to false), - ), + router.push( + AppRoute.OnboardingWallet(canSkipBackup = false), ) } @@ -148,30 +133,29 @@ internal class DefaultWalletRouter( override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { val networkAddress = currencyStatus.value.networkAddress if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currencyStatus.currency, - ), + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currencyStatus.currency, ), ) } } override fun openStoriesScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + router.push(AppRoute.Home) } override fun openSaveUserWalletScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.SaveWallet)) + router.push(AppRoute.SaveWallet) } - override fun isWalletLastScreen(): Boolean = reduxNavController.getBackStack().lastOrNull() == AppScreen.Wallet + override fun isWalletLastScreen(): Boolean { + return router.stack.lastOrNull() is AppRoute.Wallet + } override fun openManageTokensScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.ManageTokens)) + router.push(AppRoute.ManageTokens) } override fun openScanFailedDialog(onTryAgain: () -> Unit) { 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 d3f0095b29..6b05b626ce 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 @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import com.tangem.core.navigation.AppScreen + import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.navigation.ManageTokensUi @@ -29,7 +29,7 @@ internal interface InnerWalletRouter : WalletRouter { fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi) /** Pop back stack */ - fun popBackStack(screen: AppScreen? = null) + fun popBackStack() /** Open organize tokens screen */ fun openOrganizeTokensScreen(userWalletId: UserWalletId) 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 c930c63241..eccfc5ea55 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 @@ -154,7 +154,8 @@ private fun WalletContent( horizontalAlignment = Alignment.CenterHorizontally, ) { item( - key = state.wallets.map { it.walletCardState.id }, + // !!! Type of the key should be saveable via Bundle on Android !!! + key = state.wallets.map { it.walletCardState.id.stringValue }, contentType = state.wallets.map { it.walletCardState.id }, ) { WalletsList( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 5b4bf78f5d..7c128ca00d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,21 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.wallets.usecase.GetWalletNamesUseCase -import com.tangem.domain.wallets.usecase.RenameWalletUseCase -import com.tangem.feature.wallet.impl.R import com.tangem.domain.card.DeleteSavedAccessCodesUseCase -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.DeleteWalletUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -52,7 +47,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val reduxStateHolder: ReduxStateHolder, - private val reduxNavController: ReduxNavController, + private val appRouter: AppRouter, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { @@ -123,7 +118,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( reduxStateHolder.onUserWalletSelected(selectedWallet) } else { stateHolder.clear() - reduxNavController.navigate(NavigationAction.PopBackTo(AppScreen.Home)) + appRouter.replaceAll(AppRoute.Home) } } } From f895968639fae902366e4e73f154e95d5828c924 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 12:54:24 +0400 Subject: [PATCH 042/223] Updated on 2026-08-14 --- .../navigation/TokenDetailsRouter.kt | 5 ----- features/tokendetails/impl/build.gradle.kts | 2 ++ .../di/TokenDetailsRouterModule.kt | 6 ++--- .../router/DefaultTokenDetailsRouter.kt | 22 +++++++------------ .../viewmodels/TokenDetailsViewModel.kt | 16 +++++++++----- 5 files changed, 23 insertions(+), 28 deletions(-) diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index a0f630c125..a361d75438 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -5,9 +5,4 @@ import androidx.fragment.app.Fragment interface TokenDetailsRouter { fun getEntryFragment(): Fragment - - companion object { - const val USER_WALLET_ID_KEY = "token_details_user_wallet_id" - const val CRYPTO_CURRENCY_KEY = "token_details_crypto_currency" - } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 514d108158..73f39eae8c 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -38,12 +38,14 @@ dependencies { implementation(deps.tangem.card.core) implementation(deps.timber) implementation(deps.lifecycle.compose) + implementation(deps.kotlin.serialization) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) /** Core modules */ + implementation(projects.common.routing) implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) 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 186a4fcc7b..94147e5592 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,6 +1,6 @@ package com.tangem.feature.tokendetails.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter @@ -18,10 +18,10 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped fun provideTokenDetailsRouter( - reduxNavController: ReduxNavController, + appRouter: AppRouter, urlOpener: UrlOpener, shareManager: ShareManager, ): TokenDetailsRouter { - return DefaultTokenDetailsRouter(reduxNavController, urlOpener, shareManager) + return DefaultTokenDetailsRouter(appRouter, urlOpener, shareManager) } } \ 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 16b2a547ca..880d92fba4 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,19 +1,16 @@ package com.tangem.feature.tokendetails.presentation.router -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultTokenDetailsRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, private val urlOpener: UrlOpener, private val shareManager: ShareManager, ) : InnerTokenDetailsRouter { @@ -21,7 +18,7 @@ internal class DefaultTokenDetailsRouter( override fun getEntryFragment(): Fragment = TokenDetailsFragment() override fun popBackStack() { - reduxNavController.navigate(NavigationAction.PopBackTo()) + router.pop() } override fun openUrl(url: String) { @@ -33,13 +30,10 @@ internal class DefaultTokenDetailsRouter( } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, - ), + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, ), ) } 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 48640d3c90..8542738099 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels +import android.os.Bundle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -8,6 +9,8 @@ import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink @@ -19,8 +22,8 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.haptic.HapticManager import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -70,7 +73,6 @@ import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -126,12 +128,14 @@ internal class TokenDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { - private val userWalletId: UserWalletId = savedStateHandle.get(TokenDetailsRouter.USER_WALLET_ID_KEY) - ?.let { stringValue -> UserWalletId(stringValue) } + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY) + ?.let { it.unbundle(UserWalletId.serializer()) } ?: error("This screen can't open without `UserWalletId`") - private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] - ?: error("This screen can't open without `CryptoCurrency`") + private val cryptoCurrency: CryptoCurrency = + savedStateHandle.get(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) + ?.let { it.unbundle(CryptoCurrency.serializer()) } + ?: error("This screen can't open without `CryptoCurrency`") private val userWallet: UserWallet From 87987689d36e15d8368e4b2fe7607f19e142a263 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 12:55:37 +0400 Subject: [PATCH 043/223] Updated on 2026-08-14 --- .../send/api/navigation/SendRouter.kt | 9 ---- features/send/impl/build.gradle.kts | 2 + .../features/send/impl/di/SendRouterModule.kt | 6 +-- .../send/impl/navigation/DefaultSendRouter.kt | 41 ++++++++----------- .../send/impl/presentation/SendFragment.kt | 10 +++-- .../impl/presentation/state/StateRouter.kt | 7 ++-- .../presentation/viewmodel/SendViewModel.kt | 19 +++++---- 7 files changed, 43 insertions(+), 51 deletions(-) diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index 4a2940f972..286f908ad8 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -5,13 +5,4 @@ import androidx.fragment.app.Fragment interface SendRouter { fun getEntryFragment(): Fragment - - companion object { - const val CRYPTO_CURRENCY_KEY = "send_crypto_currency" - const val USER_WALLET_ID_KEY = "send_user_wallet_id" - const val TRANSACTION_ID_KEY = "send_transaction_id" - const val AMOUNT_KEY = "send_amount" - const val TAG_KEY = "send_tag" - const val DESTINATION_ADDRESS_KEY = "send_destination_address" - } } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 7d61a5c6a0..3937749519 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(deps.jodatime) implementation(deps.timber) implementation(deps.reKotlin) + implementation(deps.kotlin.serialization) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -52,6 +53,7 @@ dependencies { /** Common */ implementation(projects.common.ui) + implementation(projects.common.routing) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 352d2aaf2b..b99742c742 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.features.send.impl.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter @@ -19,7 +19,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(reduxNavController: ReduxNavController, urlOpener: UrlOpener): SendRouter { - return DefaultSendRouter(reduxNavController, urlOpener) + fun provideSendRouter(appRouter: AppRouter, urlOpener: UrlOpener): SendRouter { + return DefaultSendRouter(appRouter, urlOpener) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 59eade45d0..ed9f1f7b14 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -1,20 +1,16 @@ package com.tangem.features.send.impl.navigation -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.send.impl.presentation.SendFragment -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultSendRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, private val urlOpener: UrlOpener, ) : InnerSendRouter { @@ -25,26 +21,23 @@ internal class DefaultSendRouter( } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { - reduxNavController.popBackStack() - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, - ), - ), - ) + router.pop { isSuccess -> + if (isSuccess) { + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } } override fun openQrCodeScanner(network: String) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.SEND, - QrScanningRouter.NETWORK_KEY to network, - ), + router.push( + AppRoute.QrScanning( + source = SourceType.SEND, + networkName = network, ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 1c7d7f9cee..0a3b32c382 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect @@ -16,7 +18,6 @@ import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint -import java.lang.ref.WeakReference import javax.inject.Inject /** @@ -31,6 +32,9 @@ internal class SendFragment : ComposeFragment() { @Inject lateinit var router: SendRouter + @Inject + lateinit var appRouter: AppRouter + @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @@ -44,11 +48,11 @@ internal class SendFragment : ComposeFragment() { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) - val isEditingDisabled = arguments?.getString(SendRouter.TRANSACTION_ID_KEY) != null + val isEditingDisabled = arguments?.getString(AppRoute.Send.TRANSACTION_ID_KEY) != null viewModel.setRouter( innerSendRouter, StateRouter( - fragmentManager = WeakReference(parentFragmentManager), + appRouter = appRouter, isEditingDisabled = isEditingDisabled, analyticsEventsHandler = analyticsEventsHandler, ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e09467fa9c..6335c3b154 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -1,15 +1,14 @@ package com.tangem.features.send.impl.presentation.state -import androidx.fragment.app.FragmentManager +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import java.lang.ref.WeakReference internal class StateRouter( - private val fragmentManager: WeakReference, + private val appRouter: AppRouter, private val analyticsEventsHandler: AnalyticsEventHandler, private val isEditingDisabled: Boolean, ) { @@ -26,7 +25,7 @@ internal class StateRouter( } fun popBackStack() { - fragmentManager.get()?.popBackStack() + appRouter.pop() } fun onBackClick(isSuccess: Boolean = false) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 3d37fc0db7..7115f7b525 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel +import android.os.Bundle import android.os.SystemClock import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -10,6 +11,8 @@ import arrow.core.getOrElse import arrow.core.left import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -38,7 +41,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents @@ -101,17 +103,18 @@ internal class SendViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { - private val userWalletId: UserWalletId = savedStateHandle.get(SendRouter.USER_WALLET_ID_KEY) - ?.let { stringValue -> UserWalletId(stringValue) } + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Send.USER_WALLET_ID_KEY) + ?.let { it.unbundle(UserWalletId.serializer()) } ?: error("This screen can't open without `UserWalletId`") - private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + private val cryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Send.CRYPTO_CURRENCY_KEY) + ?.let { it.unbundle(CryptoCurrency.serializer()) } ?: error("This screen can't open without `CryptoCurrency`") - private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY] - private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY] - private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY] - private val memo: String? = savedStateHandle[SendRouter.TAG_KEY] + private val transactionId: String? = savedStateHandle[AppRoute.Send.TRANSACTION_ID_KEY] + private val amount: String? = savedStateHandle[AppRoute.Send.AMOUNT_KEY] + private val destinationAddress: String? = savedStateHandle[AppRoute.Send.DESTINATION_ADDRESS_KEY] + private val memo: String? = savedStateHandle[AppRoute.Send.TAG_KEY] private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() From 9d3759986ad492fae1a5d89ed55b6544ece2c3b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 12:58:41 +0400 Subject: [PATCH 044/223] Updated on 2026-08-14 --- features/swap/presentation/build.gradle.kts | 1 + .../feature/swap/presentation/SwapFragment.kt | 12 +++------ .../tangem/feature/swap/router/SwapRouter.kt | 25 ++++++------------- .../feature/swap/viewmodels/SwapViewModel.kt | 10 +++++--- 4 files changed, 20 insertions(+), 28 deletions(-) diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 6335a9d729..aa70954160 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.utils) implementation(projects.core.ui) + implementation(projects.common.routing) /** Domain modules **/ implementation(projects.domain.appCurrency) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 6409755135..51ac5b23a2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -5,7 +5,8 @@ import androidx.compose.animation.Crossfade import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter + import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -28,7 +29,7 @@ class SwapFragment : ComposeFragment() { override lateinit var uiDependencies: UiDependencies @Inject - lateinit var reduxNavController: ReduxNavController + lateinit var appRouter: AppRouter private val viewModel by viewModels() @@ -37,9 +38,8 @@ class SwapFragment : ComposeFragment() { lifecycle.addObserver(viewModel) viewModel.setRouter( SwapRouter( - fragmentManager = WeakReference(parentFragmentManager), customTabsManager = CustomTabsManager(WeakReference(context)), - reduxNavController = reduxNavController, + router = appRouter, ), ) } @@ -84,8 +84,4 @@ class SwapFragment : ComposeFragment() { lifecycle.removeObserver(viewModel) super.onDestroy() } - - companion object { - const val CURRENCY_BUNDLE_KEY = "swap_currency" - } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt index 081730868d..bc5009da3c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt @@ -3,20 +3,14 @@ package com.tangem.feature.swap.router import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.os.bundleOf -import androidx.fragment.app.FragmentManager -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import java.lang.ref.WeakReference internal class SwapRouter( - private val fragmentManager: WeakReference, private val customTabsManager: CustomTabsManager, - private val reduxNavController: ReduxNavController, + private val router: AppRouter, ) { var currentScreen by mutableStateOf(SwapNavScreen.Main) @@ -30,7 +24,7 @@ internal class SwapRouter( if (currentScreen == SwapNavScreen.SelectToken) { currentScreen = SwapNavScreen.Main } else { - fragmentManager.get()?.popBackStack() + router.pop() } } @@ -39,13 +33,10 @@ internal class SwapRouter( } fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, - ), + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, ), ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 2adc1f3505..8f357bae4a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -1,11 +1,14 @@ package com.tangem.feature.swap.viewmodels +import android.os.Bundle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -32,7 +35,6 @@ import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* -import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -70,8 +72,10 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - private val initialCryptoCurrency: CryptoCurrency = - savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`") + private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Swap.CURRENCY_BUNDLE_KEY) + ?.let { it.unbundle(CryptoCurrency.serializer()) } + ?: error("no expected parameter CryptoCurrency found`") + private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus private var isBalanceHidden = true From 40e0a8d0e55baf89e6f46341b48c40e55864f7fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 13:00:23 +0400 Subject: [PATCH 045/223] Updated on 2026-08-14 --- features/referral/presentation/build.gradle.kts | 1 + .../java/com/tangem/feature/referral/ReferralFragment.kt | 7 +++++-- .../com/tangem/feature/referral/router/ReferralRouter.kt | 9 +++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 900ab7de1b..8a99c77aa6 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.ui) implementation(projects.libs.crypto) + implementation(projects.common.routing) /** AndroidX */ implementation(deps.androidx.appCompat) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index 2ccd4b74fc..3d157aa005 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -13,7 +14,6 @@ import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen import com.tangem.feature.referral.viewmodels.ReferralViewModel import dagger.hilt.android.AndroidEntryPoint -import java.lang.ref.WeakReference import javax.inject.Inject @AndroidEntryPoint @@ -22,12 +22,15 @@ class ReferralFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies + @Inject + internal lateinit var appRouter: AppRouter + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.onScreenOpened() - viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager))) + viewModel.setRouter(ReferralRouter(appRouter)) } @Composable diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt index ecd73795bf..a8ac11e54b 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt @@ -1,11 +1,12 @@ package com.tangem.feature.referral.router -import androidx.fragment.app.FragmentManager -import java.lang.ref.WeakReference +import com.tangem.common.routing.AppRouter -internal class ReferralRouter(private val fragmentManager: WeakReference) { +internal class ReferralRouter( + private val appRouter: AppRouter, +) { fun back() { - fragmentManager.get()?.popBackStack() + appRouter.pop() } } \ No newline at end of file From ad88ca4b0872ab9aae7db5e3b0fc0e4c333156e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 13:01:45 +0400 Subject: [PATCH 046/223] Updated on 2026-08-14 --- .../java/com/tangem/feature/qrscanning/QrScanningRouter.kt | 5 ----- features/qr-scanning/impl/build.gradle.kts | 1 + .../tangem/feature/qrscanning/di/QrScanningRouterModule.kt | 6 +++--- .../qrscanning/navigation/DefaultQrScanningRouter.kt | 7 ++++--- .../feature/qrscanning/viewmodel/QrScanningViewModel.kt | 7 +++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt index 7f5f0352b7..049e039209 100644 --- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt +++ b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt @@ -5,9 +5,4 @@ import androidx.fragment.app.Fragment interface QrScanningRouter { fun getEntryFragment(): Fragment - - companion object { - const val SOURCE_KEY = "source" - const val NETWORK_KEY = "network" - } } \ No newline at end of file diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 5ad7d8346d..f2dfb02aa4 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.navigation) + implementation(projects.common.routing) implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt index 5bf6beee3f..b22a6c713f 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.feature.qrscanning.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.qrscanning.navigation.DefaultQrScanningRouter import dagger.Module @@ -15,7 +15,7 @@ internal object QrScanningRouterModule { @Provides @ActivityScoped - fun provideQrScanRouter(reduxNavController: ReduxNavController): QrScanningRouter { - return DefaultQrScanningRouter(reduxNavController) + fun provideQrScanRouter(appRouter: AppRouter): QrScanningRouter { + return DefaultQrScanningRouter(appRouter) } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt index 17de053b79..3813274b68 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt @@ -1,15 +1,16 @@ package com.tangem.feature.qrscanning.navigation import androidx.fragment.app.Fragment -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter + import com.tangem.feature.qrscanning.QrScanningFragment class DefaultQrScanningRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, ) : QrScanningInnerRouter { override fun getEntryFragment(): Fragment = QrScanningFragment.create() override fun popBackStack() { - reduxNavController.popBackStack() + router.pop() } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index cca4825f93..cfcd24a19b 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -3,8 +3,7 @@ package com.tangem.feature.qrscanning.viewmodel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY -import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY +import com.tangem.common.routing.AppRoute import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningState @@ -24,8 +23,8 @@ internal class QrScanningViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory") - private val network: String? = savedStateHandle[NETWORK_KEY] + private val source: SourceType = savedStateHandle[AppRoute.QrScanning.SOURCE_KEY] ?: error("Source is mandatory") + private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY] val uiState: StateFlow = stateHolder.uiState val launchGalleryEvent: SharedFlow = clickIntents.launchGallery From d8c5f217bb5e6db04238e5484061ba1ed8aa5f8a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 13:05:19 +0400 Subject: [PATCH 047/223] Updated on 2026-08-14 --- .../feature/onboarding/navigation/OnboardingRouter.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt index 5704da9b7f..b6bf1b0f60 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt @@ -4,9 +4,4 @@ package com.tangem.feature.onboarding.navigation * Onboarding router */ // TODO: Move to onboarding api module [REDACTED_JIRA] -interface OnboardingRouter { - - companion object { - const val CAN_SKIP_BACKUP = "onboarding_wallet_can_skip_backup" - } -} \ No newline at end of file +interface OnboardingRouter \ No newline at end of file From 0225de915a7432f7dc29f48539656aa64389c348 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 13:10:29 +0400 Subject: [PATCH 048/223] Updated on 2026-08-14 --- .../tap/features/details/ui/details/DetailsViewModel.kt | 4 +++- .../main/java/com/tangem/tap/routing/utils/ChildFactory.kt | 4 +++- .../kotlin/com/tangem/features/details/DetailsRouter.kt | 2 +- .../java/com/tangem/features/tester/api/TesterRouter.kt | 4 +++- .../tester/presentation/navigation/DefaultTesterRouter.kt | 6 ++---- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 0be2c4cda4..e0122f240b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -127,7 +127,9 @@ internal class DetailsViewModel( } private fun navigateToTesterMenu() { - store.state.daggerGraphState.testerRouter?.startTesterScreen() + store.dispatchNavigationAction { + push(AppRoute.TesterMenu) + } } private fun navigateToToS() { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 6e3ac96e92..5e0319cd2c 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -8,6 +8,7 @@ import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.features.details.component.DetailsComponent import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment @@ -43,6 +44,7 @@ internal class ChildFactory @Inject constructor( private val walletRouter: WalletRouter, private val qrScanningRouter: QrScanningRouter, private val stakingRouter: StakingRouter, + private val testerRouter: TesterRouter, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -133,7 +135,7 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { WelcomeFragment() }) } is AppRoute.TesterMenu -> { - TODO("Will be implemented later") + Child.LegacyIntent(testerRouter.getEntryIntent()) } is AppRoute.Staking -> { route.asFragmentChild(Provider { stakingRouter.getEntryFragment() }) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt index 93e60c90ef..b723a9a48b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt @@ -30,7 +30,7 @@ internal class DetailsRouter @Inject constructor( feedbackManager.sendEmail(FeedbackType.Feedback) } is DetailsRoute.TesterMenu -> { - testerRouter.startTesterScreen() + // testerRouter.startTesterScreen() } is DetailsRoute.Url -> { urlOpener.openUrl(route.url) diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt index 913c04a076..7e647c8323 100644 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt @@ -1,5 +1,7 @@ package com.tangem.features.tester.api +import android.content.Intent + /** * Outer tester feature router * @@ -8,5 +10,5 @@ package com.tangem.features.tester.api interface TesterRouter { /** Open tester menu */ - fun startTesterScreen() + fun getEntryIntent(): Intent } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt index 97126f3540..0d3c250e91 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt @@ -22,10 +22,8 @@ internal class DefaultTesterRouter @Inject constructor( private var navController: NavController? = null - override fun startTesterScreen() { - context.startActivity( - Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) + override fun getEntryIntent(): Intent { + return Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } override fun setNavController(navController: NavController) { From 1526da80ebd4806934e276021f63ed6426e4b9f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 15:22:15 +0400 Subject: [PATCH 049/223] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 - .../java/com/tangem/tap/TangemApplication.kt | 5 -- .../tap/common/extensions/Navigation.kt | 8 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 - .../tangem/tap/routing/utils/ChildFactory.kt | 17 +++-- features/details/api/build.gradle.kts | 1 + .../features/details/DetailsEntryPoint.kt | 13 ---- .../details/component/DetailsComponent.kt | 3 - .../component/UserWalletListComponent.kt | 0 features/details/impl/build.gradle.kts | 1 + .../features/details/DetailsFragment.kt | 74 ------------------- .../tangem/features/details/DetailsRouter.kt | 61 --------------- .../component/impl/DefaultDetailsComponent.kt | 4 - .../preview/PreviewDetailsComponent.kt | 10 +-- .../features/details/di/FeatureModule.kt | 8 -- .../features/details/routing/DetailsRoute.kt | 20 ----- .../features/details/ui/DetailsScreen.kt | 4 +- .../features/details/utils/ItemsBuilder.kt | 20 +++-- .../features/details/utils/SocialsBuilder.kt | 7 +- .../features/details/utils/UserWalletSaver.kt | 6 +- 20 files changed, 41 insertions(+), 226 deletions(-) delete mode 100644 features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt rename features/details/{impl => api}/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt (85%) rename features/details/{impl => api}/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt (100%) delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt delete mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 6f45b6b45b..24ce4e1822 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -29,7 +29,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -112,8 +111,6 @@ interface ApplicationEntryPoint { fun getDetailsFeatureToggles(): DetailsFeatureToggles - fun getDetailsEntryPoint(): DetailsEntryPoint - fun getUrlOpener(): UrlOpener fun getShareManager(): ShareManager diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 5ecb0622ca..e4c530273b 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -44,7 +44,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -186,9 +185,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val detailsFeatureToggles: DetailsFeatureToggles get() = entryPoint.getDetailsFeatureToggles() - private val detailsEntryPoint: DetailsEntryPoint - get() = entryPoint.getDetailsEntryPoint() - private val urlOpener get() = entryPoint.getUrlOpener() @@ -285,7 +281,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { blockchainSDKFactory = blockchainSDKFactory, saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, detailsFeatureToggles = detailsFeatureToggles, - detailsEntryPoint = detailsEntryPoint, urlOpener = urlOpener, shareManager = shareManager, appRouter = appRouter, diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 8282d27be6..d7f3aad533 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -182,13 +182,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { } } AppScreen.Details -> { - val featureToggles = store.inject(getDependency = DaggerGraphState::detailsFeatureToggles) - - if (featureToggles.isRedesignEnabled) { - store.inject(DaggerGraphState::detailsEntryPoint).entryFragment() - } else { - DetailsFragment() - } + DetailsFragment() } AppScreen.DetailsSecurity -> SecurityModeFragment() AppScreen.CardSettings -> CardSettingsFragment() 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 08744d3939..8a555f904d 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 @@ -27,7 +27,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensUi @@ -82,7 +81,6 @@ data class DaggerGraphState( val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, val assetLoader: AssetLoader? = null, val detailsFeatureToggles: DetailsFeatureToggles? = null, - val detailsEntryPoint: DetailsEntryPoint? = null, val stakingRouter: StakingRouter? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 5e0319cd2c..3199a16968 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -16,6 +17,7 @@ import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragmen import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment +import com.tangem.tap.features.details.ui.details.DetailsFragment import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment @@ -45,6 +47,7 @@ internal class ChildFactory @Inject constructor( private val qrScanningRouter: QrScanningRouter, private val stakingRouter: StakingRouter, private val testerRouter: TesterRouter, + private val detailsFeatureToggles: DetailsFeatureToggles, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -80,11 +83,15 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { CardSettingsFragment() }) } is AppRoute.Details -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = DetailsComponent.Params(route.userWalletId), - componentFactory = detailsComponentFactory, - ) + if (detailsFeatureToggles.isRedesignEnabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DetailsComponent.Params(route.userWalletId), + componentFactory = detailsComponentFactory, + ) + } else { + route.asFragmentChild(Provider { DetailsFragment() }) + } } is AppRoute.DetailsSecurity -> { route.asFragmentChild(Provider { SecurityModeFragment() }) diff --git a/features/details/api/build.gradle.kts b/features/details/api/build.gradle.kts index 5010f64d85..103bd2c268 100644 --- a/features/details/api/build.gradle.kts +++ b/features/details/api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) + implementation(projects.core.ui) /* AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt deleted file mode 100644 index d3ac9fe6f7..0000000000 --- a/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.details - -import androidx.fragment.app.Fragment - -interface DetailsEntryPoint { - - fun entryFragment(): Fragment - - companion object { - - const val USER_WALLET_ID_KEY = "user_wallet_id" - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt similarity index 85% rename from features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt rename to features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index da63581436..f53bc84d85 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -1,6 +1,5 @@ package com.tangem.features.details.component -import androidx.compose.material3.SnackbarHostState import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.ComposableContentComponent @@ -8,8 +7,6 @@ import com.tangem.domain.wallets.models.UserWalletId interface DetailsComponent : ComposableContentComponent { - val snackbarHostState: SnackbarHostState - interface Factory : ComponentFactory { override fun create(context: AppComponentContext, params: Params): DetailsComponent diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt similarity index 100% rename from features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt rename to features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 8a26e80f40..8b6951e1e9 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.navigation) implementation(projects.core.analytics.models) + implementation(projects.common.routing) /* Project - Domain */ implementation(projects.domain.models) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt deleted file mode 100644 index 4bd1e416ee..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.features.details - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.fragment.app.Fragment -import com.arkivanov.decompose.defaultComponentContext -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.message.EventMessageEffect -import com.tangem.core.ui.message.EventMessageHandler -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.details.component.DetailsComponent -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -// TODO: Remove after [REDACTED_JIRA] -@AndroidEntryPoint -internal class DetailsFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - @Inject - internal lateinit var componentFactory: DetailsComponent.Factory - - @Inject - internal lateinit var detailsRouter: DetailsRouter - - @Inject - @RootAppComponentContext - internal lateinit var rootContext: AppComponentContext - - private val component: DetailsComponent by lazy { initComponent() } - - private val messageHandler = EventMessageHandler() - - @Composable - override fun ScreenContent(modifier: Modifier) { - component.Content(modifier = modifier) - - EventMessageEffect( - messageHandler = messageHandler, - snackbarHostState = component.snackbarHostState, - ) - } - - private fun initComponent(): DetailsComponent { - val selectedUserWalletId = arguments?.getString(DetailsEntryPoint.USER_WALLET_ID_KEY) - ?.let(::UserWalletId) - - requireNotNull(selectedUserWalletId) { "UserWalletId must be provided" } - - val context = rootContext.childByContext( - componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = messageHandler, - router = detailsRouter, - ) - - return componentFactory.create( - context = context, - params = DetailsComponent.Params( - selectedUserWalletId = selectedUserWalletId, - ), - ) - } - - companion object : DetailsEntryPoint { - - override fun entryFragment(): Fragment = DetailsFragment() - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt deleted file mode 100644 index b723a9a48b..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.features.details - -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController -import com.tangem.core.navigation.feedback.FeedbackManager -import com.tangem.core.navigation.feedback.FeedbackType -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.details.routing.DetailsRoute -import com.tangem.features.tester.api.TesterRouter -import javax.inject.Inject -import kotlin.reflect.KClass - -// TODO: Remove after [REDACTED_JIRA] -internal class DetailsRouter @Inject constructor( - private val reduxNavController: ReduxNavController, - private val feedbackManager: FeedbackManager, - private val testerRouter: TesterRouter, - private val urlOpener: UrlOpener, -) : Router { - - override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - if (route is DetailsRoute) { - when (route) { - is DetailsRoute.Screen -> { - reduxNavController.navigate(NavigationAction.NavigateTo(route.screen, bundle = route.params)) - } - is DetailsRoute.Feedback -> { - feedbackManager.sendEmail(FeedbackType.Feedback) - } - is DetailsRoute.TesterMenu -> { - // testerRouter.startTesterScreen() - } - is DetailsRoute.Url -> { - urlOpener.openUrl(route.url) - } - } - onComplete(true) - } else { - onComplete(false) - } - } - - override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - reduxNavController.popBackStack() - onComplete(true) - } - - override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { - TODO("This class will be removed in future") - } - - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - TODO("This class will be removed in future") - } - - override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { - TODO("This class will be removed in future") - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt index bc9a891d84..08d302a183 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt @@ -1,6 +1,5 @@ package com.tangem.features.details.component.impl -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -28,8 +27,6 @@ internal class DefaultDetailsComponent @AssistedInject constructor( context = child(key = "user_wallet_list"), ) - override val snackbarHostState: SnackbarHostState = SnackbarHostState() - init { model.provideUserWalletId(params.selectedUserWalletId) } @@ -41,7 +38,6 @@ internal class DefaultDetailsComponent @AssistedInject constructor( DetailsScreen( modifier = modifier, state = state, - snackbarHostState = snackbarHostState, userWalletListBlockContent = userWalletListComponent, ) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index d91ffd5ce1..ccbc7f2409 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -1,9 +1,10 @@ package com.tangem.features.details.component.preview -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.core.navigation.feedback.DummyFeedbackManager +import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -14,16 +15,16 @@ import kotlinx.coroutines.runBlocking internal class PreviewDetailsComponent : DetailsComponent { - override val snackbarHostState: SnackbarHostState = SnackbarHostState() - private val previewBlocks = runBlocking { ItemsBuilder( router = DummyRouter(), + urlOpener = DummyUrlOpener(), + feedbackManager = DummyFeedbackManager(), ).buldAll(isWalletConnectAvailable = true) } private val previewFooter = DetailsFooterUM( - socials = SocialsBuilder(DummyRouter()).buildAll(), + socials = SocialsBuilder(DummyUrlOpener()).buildAll(), appVersion = "1.0.0-preview", ) @@ -39,7 +40,6 @@ internal class PreviewDetailsComponent : DetailsComponent { DetailsScreen( modifier = modifier, state = previewState, - snackbarHostState = snackbarHostState, userWalletListBlockContent = PreviewUserWalletListComponent(), ) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt index f9a437ee25..44a655a33f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt @@ -2,9 +2,7 @@ package com.tangem.features.details.di import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.features.details.DefaultDetailsFeatureToggles -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.details.DetailsFragment import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,10 +18,4 @@ internal object FeatureModule { fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles { return DefaultDetailsFeatureToggles(featureTogglesManager) } - - @Provides - @Singleton - fun provideEntryPoint(): DetailsEntryPoint { - return DetailsFragment.Companion - } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt deleted file mode 100644 index 878987aca5..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.details.routing - -import android.os.Bundle -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.navigation.AppScreen - -// TODO: Remove after [REDACTED_JIRA] -internal sealed class DetailsRoute : Route { - - data class Screen( - val screen: AppScreen, - val params: Bundle? = null, - ) : DetailsRoute() - - data class Url(val url: String) : DetailsRoute() - - data object Feedback : DetailsRoute() - - data object TesterMenu : DetailsRoute() -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 95413f3fc5..af683ca7a8 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.ComposableContentComponent import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.preview.PreviewDetailsComponent @@ -35,7 +36,6 @@ private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f @Composable internal fun DetailsScreen( state: DetailsUM, - snackbarHostState: SnackbarHostState, userWalletListBlockContent: ComposableContentComponent, modifier: Modifier = Modifier, ) { @@ -54,7 +54,7 @@ internal fun DetailsScreen( snackbarHost = { TangemSnackbarHost( modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - hostState = snackbarHostState, + hostState = LocalSnackbarHostState.current, ) }, topBar = { TopBar(state, scrollBehavior) }, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index fb7e8768e4..9e87499c97 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -1,14 +1,16 @@ package com.tangem.features.details.utils +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.feedback.FeedbackManager +import com.tangem.core.navigation.feedback.FeedbackType +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R -import com.tangem.features.details.routing.DetailsRoute import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -17,6 +19,8 @@ import javax.inject.Inject @ComponentScoped internal class ItemsBuilder @Inject constructor( private val router: Router, + private val urlOpener: UrlOpener, + private val feedbackManager: FeedbackManager, ) { suspend fun buldAll(isWalletConnectAvailable: Boolean): ImmutableList = buildList { @@ -30,7 +34,7 @@ internal class ItemsBuilder @Inject constructor( private suspend fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( - onClick = { router.push(DetailsRoute.Screen(AppScreen.WalletConnectSessions)) }, + onClick = { router.push(AppRoute.WalletConnectSessions) }, ) } else { null @@ -46,7 +50,7 @@ internal class ItemsBuilder @Inject constructor( id = "buy_tangem_wallet", title = resourceReference(R.string.details_buy_wallet), iconRes = R.drawable.ic_tangem_24, - onClick = { router.push(DetailsRoute.Url(BUY_TANGEM_URL)) }, + onClick = { urlOpener.openUrl(BUY_TANGEM_URL) }, ), ), ) @@ -58,7 +62,7 @@ internal class ItemsBuilder @Inject constructor( id = "app_settings", title = resourceReference(R.string.app_settings_title), iconRes = R.drawable.ic_settings_24, - onClick = { router.push(DetailsRoute.Screen(AppScreen.AppSettings)) }, + onClick = { router.push(AppRoute.AppSettings) }, ).let(::add) if (BuildConfig.TESTER_MENU_ENABLED) { @@ -66,7 +70,7 @@ internal class ItemsBuilder @Inject constructor( id = "tester_menu", title = stringReference(value = "Tester menu"), iconRes = R.drawable.ic_alert_24, - onClick = { router.push(DetailsRoute.TesterMenu) }, + onClick = { router.push(AppRoute.TesterMenu) }, ).let(::add) } }.toImmutableList(), @@ -79,13 +83,13 @@ internal class ItemsBuilder @Inject constructor( id = "send_feedback", title = resourceReference(R.string.details_send_feedback), iconRes = R.drawable.ic_comment_24, - onClick = { router.push(DetailsRoute.Feedback) }, + onClick = { feedbackManager.sendEmail(FeedbackType.Feedback) }, ), DetailsItemUM.Basic.Item( id = "disclaimer", title = resourceReference(R.string.disclaimer_title), iconRes = R.drawable.ic_text_24, - onClick = { router.push(DetailsRoute.Screen(AppScreen.Disclaimer)) }, + onClick = { router.push(AppRoute.Disclaimer) }, ), ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt index 9d54a022c2..8282bbd41c 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt @@ -2,17 +2,16 @@ package com.tangem.features.details.utils import androidx.compose.ui.text.intl.Locale import com.tangem.core.decompose.di.ComponentScoped -import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.impl.R -import com.tangem.features.details.routing.DetailsRoute import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @ComponentScoped internal class SocialsBuilder @Inject constructor( - private val router: Router, + private val urlOpener: UrlOpener, ) { fun buildAll(): ImmutableList = Social.all.map { social -> @@ -32,7 +31,7 @@ internal class SocialsBuilder @Inject constructor( social.url } - router.push(DetailsRoute.Url(url)) + urlOpener.openUrl(url) } private enum class Social( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index cd539ea937..f1bb1fd398 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -2,9 +2,11 @@ package com.tangem.features.details.utils import arrow.core.raise.* import arrow.core.recover +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.navigation.popTo import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty @@ -42,7 +44,7 @@ internal class UserWalletSaver @Inject constructor( saveWallet(userWallet) - // router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + router.popTo() }, recover = { error -> val message = error.message @@ -75,7 +77,7 @@ internal class UserWalletSaver @Inject constructor( selectWalletUseCase(userWallet.walletId).bind() } - // router.popTo(DetailsRoute.Screen(AppScreen.Wallet)) + router.popTo() } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { From 7bd4bd3d52cf54a2a7c81991e87ba9f2e065013d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 15:28:09 +0400 Subject: [PATCH 050/223] Updated on 2026-08-14 --- .../com/tangem/tap/LockUserWalletsTimer.kt | 11 +- .../main/java/com/tangem/tap/MainActivity.kt | 5 - .../tap/common/extensions/Navigation.kt | 165 +----------------- .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../redux/navigation/NavigationMiddleware.kt | 60 ------- .../redux/navigation/NavigationReducer.kt | 39 ----- .../com/tangem/tap/di/AppStateHolderModule.kt | 5 - .../com/tangem/tap/proxy/AppStateHolder.kt | 16 +- .../core/navigation/NavigationAction.kt | 25 --- .../tangem/core/navigation/NavigationState.kt | 39 ----- .../core/navigation/ReduxNavController.kt | 16 -- ...mentShareTransition.kt => ShareElement.kt} | 10 -- 13 files changed, 9 insertions(+), 388 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt delete mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt delete mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt delete mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt rename core/navigation/src/main/java/com/tangem/core/navigation/{FragmentShareTransition.kt => ShareElement.kt} (74%) diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index bb6e616898..8a80aff9f7 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -3,13 +3,12 @@ package com.tangem.tap import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import kotlinx.coroutines.* import timber.log.Timber import kotlin.time.Duration @@ -50,7 +49,7 @@ internal class LockUserWalletsTimer( start() if (shouldOpenWelcomeScreenOnResume) { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { popTo() } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) } } @@ -128,7 +127,7 @@ internal class LockUserWalletsTimer( if (wasApplicationStopped) { settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) } else { - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { popTo() } } } } diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index ed2a8d35af..32ce55bfb5 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -36,7 +36,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference @@ -86,7 +85,6 @@ import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -261,8 +259,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun installActivityDependencies() { - store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) - cardSdkLifecycleObserver.onCreate(context = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager @@ -376,7 +372,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } override fun onDestroy() { - store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) intentProcessor.removeAll() cardSdkLifecycleObserver.onDestroy(this) super.onDestroy() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index d7f3aad533..251edacdf4 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -1,33 +1,8 @@ package com.tangem.tap.common.extensions -import android.os.Bundle -import androidx.fragment.app.* -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.FragmentShareTransition -import com.tangem.feature.referral.ReferralFragment -import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment -import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment -import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment -import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment -import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment -import com.tangem.tap.features.details.ui.details.DetailsFragment -import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment -import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment -import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment -import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment -import com.tangem.tap.features.home.HomeFragment -import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment -import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment -import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment -import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment -import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment -import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment -import com.tangem.tap.features.send.ui.SendFragment -import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment -import com.tangem.tap.features.welcome.ui.WelcomeFragment -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import com.tangem.utils.Provider import com.tangem.wallet.R import timber.log.Timber @@ -73,138 +48,4 @@ private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String transaction.addToBackStack(name) transaction.commitAllowingStateLoss() -} - -fun FragmentActivity.openFragment( - screen: AppScreen, - addToBackstack: Boolean, - bundle: Bundle? = null, - fgShareTransition: FragmentShareTransition? = null, -) { - val transaction = supportFragmentManager.beginTransaction().apply { - setReorderingAllowed(true) - } - - val fragment = fragmentFactory(screen).apply { - arguments = bundle - - if (fgShareTransition != null) { - sharedElementEnterTransition = fgShareTransition.enterTransitionSet - sharedElementReturnTransition = fgShareTransition.exitTransitionSet - fgShareTransition.shareElements.forEach { shareElement -> - shareElement.wView.get()?.let { view -> - transaction.addSharedElement(view, shareElement.elementName) - } - } - } - } - - if (screen.isDialogFragment && fragment is DialogFragment) { - fragment.showAllowingStateLoss( - fragmentManager = supportFragmentManager, - baseTransaction = transaction, - tag = screen.name, - addToBackstack = addToBackstack, - ) - } else { - transaction.replace(R.id.fragment_container, fragment, screen.name) - if (addToBackstack) { - transaction.addToBackStack(screen.name) - } - transaction.commitAllowingStateLoss() - } -} - -private fun DialogFragment.showAllowingStateLoss( - fragmentManager: FragmentManager, - baseTransaction: FragmentTransaction, - tag: String, - addToBackstack: Boolean, -) { - runCatching { - if (addToBackstack) baseTransaction.addToBackStack(tag) - show(baseTransaction, tag) - } - .onFailure { throwable -> - if (throwable is IllegalStateException) { - val transaction = fragmentManager.beginTransaction() - transaction.add(this, tag) - if (addToBackstack) transaction.addToBackStack(tag) - - transaction.commitAllowingStateLoss() - } else { - Timber.e(throwable) - } - } -} - -fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) { - val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0 - try { - this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag) - } catch (e: IllegalStateException) { - Timber.e(e) - } -} - -fun FragmentActivity.getPreviousScreen(): AppScreen? { - val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) { - this.supportFragmentManager.backStackEntryCount - 1 - } else { - 0 - } - val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount) { - this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name - } else { - null - } - return tag?.let { AppScreen.valueOf(tag) } -} - -@Suppress("ComplexMethod", "LongMethod") -private fun fragmentFactory(screen: AppScreen): Fragment { - return when (screen) { - AppScreen.Home -> HomeFragment() - AppScreen.OnboardingNote -> OnboardingNoteFragment() - AppScreen.OnboardingWallet -> OnboardingWalletFragment() - AppScreen.OnboardingTwins -> OnboardingTwinsFragment() - AppScreen.OnboardingOther -> OnboardingOtherCardsFragment() - AppScreen.Wallet -> { - store.inject(getDependency = DaggerGraphState::walletRouter).getEntryFragment() - } - AppScreen.Send -> { - val featureToggles = store.inject(getDependency = DaggerGraphState::sendFeatureToggles) - - if (featureToggles.isRedesignedSendEnabled) { - store.inject(getDependency = DaggerGraphState::sendRouter).getEntryFragment() - } else { - SendFragment() - } - } - AppScreen.Details -> { - DetailsFragment() - } - AppScreen.DetailsSecurity -> SecurityModeFragment() - AppScreen.CardSettings -> CardSettingsFragment() - AppScreen.AppSettings -> AppSettingsFragment() - AppScreen.ResetToFactory -> ResetCardFragment() - AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() - AppScreen.Disclaimer -> DisclaimerFragment() - AppScreen.ManageTokens -> TokensListFragment() - AppScreen.AddCustomToken -> AddCustomTokenFragment() - AppScreen.WalletDetails -> { - store.inject(getDependency = DaggerGraphState::tokenDetailsRouter).getEntryFragment() - } - AppScreen.WalletConnectSessions -> WalletConnectFragment() - AppScreen.QrScanning -> { - store.inject(getDependency = DaggerGraphState::qrScanningRouter).getEntryFragment() - } - AppScreen.ReferralProgram -> ReferralFragment() - AppScreen.Swap -> SwapFragment() - AppScreen.Welcome -> WelcomeFragment() - AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() - AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment() - AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment() - AppScreen.Staking -> store.inject(getDependency = DaggerGraphState::stakingRouter).getEntryFragment() - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 7adedbed8f..d047620b5e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -1,7 +1,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer -import com.tangem.tap.common.redux.navigation.NavigationReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer @@ -23,7 +22,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) if (action is AppAction.RestoreState) return action.state return AppState( - navigationState = NavigationReducer.reduce(action, state), globalState = globalReducer(action, state, appStateHolder), homeState = HomeReducer.reduce(action, state), onboardingNoteState = OnboardingNoteReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 59b899ee8b..2a83cbf4df 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -1,13 +1,11 @@ package com.tangem.tap.common.redux -import com.tangem.core.navigation.NavigationState import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.NetworkServices import com.tangem.tap.common.redux.global.GlobalMiddleware import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.legacy.LegacyMiddleware -import com.tangem.tap.common.redux.navigation.navigationMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware @@ -40,7 +38,6 @@ import org.rekotlin.Middleware import org.rekotlin.StateType data class AppState( - val navigationState: NavigationState = NavigationState(), val globalState: GlobalState = GlobalState(), val homeState: HomeState = HomeState(), val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(), @@ -73,7 +70,6 @@ data class AppState( fun getMiddleware(): List> { return listOf( logMiddleware, - navigationMiddleware, notificationsMiddleware, GlobalMiddleware.handler, HomeMiddleware.handler, diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt deleted file mode 100644 index 00f61f18db..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.common.redux.navigation - -import android.content.Intent -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.openFragment -import com.tangem.tap.common.extensions.popBackTo -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.store -import org.rekotlin.Middleware - -val navigationMiddleware: Middleware = { _, state -> - { next -> - { action -> - if (action is NavigationAction) { - val navState = state()?.navigationState - when (action) { - is NavigationAction.NavigateTo -> { - navState?.activity?.get()?.openFragment( - screen = action.screen, - addToBackstack = action.addToBackstack, - fgShareTransition = action.fragmentShareTransition, - bundle = action.bundle, - ) - } - is NavigationAction.PopBackTo -> { - if (navState?.backStack?.lastOrNull() != action.screen) { - when (val screen = action.screen) { - AppScreen.Home, - AppScreen.Welcome, - -> { - if (navState?.backStack?.contains(screen) == false) { - // Pop back to activity - navState.activity?.get()?.popBackTo(screen = null, inclusive = true) - store.dispatchOnMain(NavigationAction.NavigateTo(screen)) - } else { - navState?.activity?.get()?.popBackTo(screen, action.inclusive) - } - } - else -> { - navState?.activity?.get()?.popBackTo(screen, action.inclusive) - } - } - } - } - is NavigationAction.OpenDocument -> { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = action.url - navState?.activity?.get()?.startActivity(intent) - } - is NavigationAction.ActivityCreated, - is NavigationAction.ActivityDestroyed, - -> Unit - } - } - next(action) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt deleted file mode 100644 index ff53145651..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.common.redux.navigation - -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.NavigationState -import com.tangem.tap.common.extensions.getPreviousScreen -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -object NavigationReducer { - fun reduce(action: Action, state: AppState): NavigationState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, state: AppState): NavigationState { - val navigationAction = action as? NavigationAction ?: return state.navigationState - val navState = state.navigationState - - return when (navigationAction) { - is NavigationAction.NavigateTo -> { - navState.copy(backStack = navState.backStack + navigationAction.screen) - } - is NavigationAction.PopBackTo -> { - if (navState.backStack.lastOrNull() == navigationAction.screen) return navState - - val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen() - val index = navState.backStack.lastIndexOf(screen) + 1 - state.navigationState.copy(backStack = navState.backStack.subList(0, index)) - } - is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity) - is NavigationAction.ActivityDestroyed -> { - when { - // Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a - // new scan from the background [REDACTED_TASK_KEY] - navState.activity?.get() == navigationAction.activity.get() -> navState.copy(activity = null) - else -> navState - } - } - else -> navState - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt index c6e0267c6b..267fa733f1 100644 --- a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.di -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder import com.tangem.tap.proxy.AppStateHolder import dagger.Binds @@ -13,10 +12,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface AppStateHolderModule { - @Binds - @Singleton - fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController - @Binds @Singleton fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder 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 0e1a98b9c7..a2c9dd483f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,15 +1,11 @@ package com.tangem.tap.proxy -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.extensions.dispatchDialogShow -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 @@ -23,7 +19,7 @@ import javax.inject.Inject * Holds objects from old modules, that missing in DI graph. * Object sets manually to use in new modules and [AppStateHolder] proxies its to DI. */ -class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolder { +class AppStateHolder @Inject constructor() : ReduxStateHolder { @Deprecated("Use scan response from selected user wallet") var scanResponse: ScanResponse? = null @@ -35,16 +31,6 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde return scanResponse?.card } - override fun navigate(action: NavigationAction) { - mainStore?.dispatchOnMain(action) - } - - override fun getBackStack(): List = mainStore?.state?.navigationState?.backStack.orEmpty() - - override fun popBackStack(screen: AppScreen?) { - mainStore?.dispatchOnMain(NavigationAction.PopBackTo(screen)) - } - override fun dispatch(action: Action) { mainStore?.dispatch(action) } diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt deleted file mode 100644 index ec50c35bc0..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.core.navigation - -import android.net.Uri -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import org.rekotlin.Action -import java.lang.ref.WeakReference - -sealed class NavigationAction : Action { - - data class NavigateTo( - val screen: AppScreen, - val fragmentShareTransition: FragmentShareTransition? = null, - val addToBackstack: Boolean = true, - val bundle: Bundle? = null, - ) : NavigationAction() - - data class PopBackTo(val screen: AppScreen? = null, val inclusive: Boolean = false) : NavigationAction() - - data class OpenDocument(val url: Uri) : NavigationAction() - - data class ActivityCreated(val activity: WeakReference) : NavigationAction() - - data class ActivityDestroyed(val activity: WeakReference) : NavigationAction() -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt deleted file mode 100644 index 40d6612fed..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.core.navigation - -import androidx.appcompat.app.AppCompatActivity -import org.rekotlin.StateType -import java.lang.ref.WeakReference - -data class NavigationState( - val backStack: List = emptyList(), - val activity: WeakReference? = null, -) : StateType - -enum class AppScreen(val isDialogFragment: Boolean = false) { - Home, - Disclaimer, - OnboardingNote, - OnboardingWallet, - OnboardingTwins, - OnboardingOther, - Wallet, - WalletDetails, - Send(isDialogFragment = true), - Details, - DetailsSecurity, - CardSettings, - AppSettings, - ResetToFactory, - AccessCodeRecovery, - ManageTokens, - AddCustomToken, - WalletConnectSessions, - QrScanning, - ReferralProgram, - Swap, - Welcome, - SaveWallet(isDialogFragment = true), - AppCurrencySelector, - ModalNotification(isDialogFragment = true), - Staking, -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt b/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt deleted file mode 100644 index 20e37f3958..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.core.navigation - -/** - * Navigation controller that based on redux actions - * -[REDACTED_AUTHOR] - */ -interface ReduxNavController { - - /** Navigate by [action] */ - fun navigate(action: NavigationAction) - - fun popBackStack(screen: AppScreen? = null) - - fun getBackStack(): List -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/FragmentShareTransition.kt b/core/navigation/src/main/java/com/tangem/core/navigation/ShareElement.kt similarity index 74% rename from core/navigation/src/main/java/com/tangem/core/navigation/FragmentShareTransition.kt rename to core/navigation/src/main/java/com/tangem/core/navigation/ShareElement.kt index 2cb2132eec..b3fd049583 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/FragmentShareTransition.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/ShareElement.kt @@ -1,18 +1,8 @@ package com.tangem.core.navigation import android.view.View -import androidx.transition.TransitionSet import java.lang.ref.WeakReference -/** -[REDACTED_AUTHOR] - */ -data class FragmentShareTransition( - val shareElements: List, - val enterTransitionSet: TransitionSet, - val exitTransitionSet: TransitionSet, -) - /** * For ease of use, the name is used as transitionName\name into the FragmentTransaction.addSharedElement */ From 10171a3b3e121f3b3ad7fa80b1b11826e148818a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 13:18:05 +0400 Subject: [PATCH 051/223] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/MainActivity.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 32ce55bfb5..e55b98f204 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -224,7 +224,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac appRouterConfig.componentRouter = routingComponent.router routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack -> - appRouterConfig.baclStack = childStack.backStack.map { it.configuration } + appRouterConfig.stack = childStack.backStack + .plus(childStack.active) + .map { it.configuration } when (val child = childStack.active.instance) { is RoutingComponent.Child.Initial -> Unit @@ -514,10 +516,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { - push(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) + replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) } } else { - store.dispatchNavigationAction { push(AppRoute.Home) } + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } lifecycleScope.launch { intentProcessor.handleIntent(intentWhichStartedActivity, false) } From ecf8e5fc96b7d77d67df2a3d2b9048596beff379 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 13:24:05 +0400 Subject: [PATCH 052/223] Updated on 2026-08-14 --- .../details/redux/DetailsMiddleware.kt | 4 +- .../features/details/redux/DetailsReducer.kt | 2 +- .../saveWallet/redux/SaveWalletMiddleware.kt | 6 +-- .../com/tangem/common/routing/AppRoute.kt | 14 +++++- domain/core/build.gradle.kts | 3 ++ .../serialization/BigDecimalSerializer.kt | 21 +++++++++ .../serialization/SerializedBigDecimal.kt | 6 +++ domain/staking/build.gradle.kts | 7 +-- domain/staking/models/.gitignore | 1 + domain/staking/models/build.gradle.kts | 11 +++++ .../com/tangem/domain/staking/model/Yield.kt | 45 ++++++++++++------- .../domain/tokens/model/CryptoCurrency.kt | 8 ++-- .../staking/api/navigation/StakingRouter.kt | 6 --- features/staking/impl/build.gradle.kts | 1 + .../viewmodel/StakingViewModel.kt | 15 +++++-- .../router/DefaultTokenDetailsRouter.kt | 11 +++++ .../router/InnerTokenDetailsRouter.kt | 3 ++ .../viewmodels/TokenDetailsViewModel.kt | 15 +------ settings.gradle.kts | 1 + 19 files changed, 125 insertions(+), 55 deletions(-) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt create mode 100644 domain/staking/models/.gitignore create mode 100644 domain/staking/models/build.gradle.kts rename domain/staking/{src/main/java => models/src/main/kotlin}/com/tangem/domain/staking/model/Yield.kt (88%) 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 d0ce3c2f74..23a552773c 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 @@ -140,9 +140,9 @@ class DetailsMiddleware { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } } else { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index d89b862c1b..658dfcd6ba 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -74,7 +74,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS return DetailsState( scanResponse = action.scanResponse, // If the current screen is ResetToFactory, we must save the ResetToFactory's state - cardSettingsState = if (router.backStack.lastOrNull() is AppRoute.ResetToFactory) { + cardSettingsState = if (router.stack.lastOrNull() is AppRoute.ResetToFactory) { state.detailsState.cardSettingsState } else { null 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 541ceefdcc..82799c76e6 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 @@ -157,11 +157,7 @@ internal class SaveWalletMiddleware { private fun navigateToWallet() { store.dispatchNavigationAction { - popTo { isSuccess -> - if (!isSuccess) { - clear { push(AppRoute.Wallet) } - } - } + replaceAll(AppRoute.Wallet) } } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 2a337dbeaa..f912745220 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -6,6 +6,7 @@ import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @@ -160,5 +161,16 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { data object ModalNotification : AppRoute(path = "/modal_notification") @Serializable - data object Staking : AppRoute(path = "/staking") + data class Staking( + val userWalletId: UserWalletId, + val cryptoCurrencyId: CryptoCurrency.ID, + val yield: Yield, + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/${yield.id}") { + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + const val CRYPTO_CURRENCY_ID_KEY = "cryptoCurrencyId" + const val YIELD_KEY = "yield" + } + } } \ No newline at end of file diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 63325166f7..46d2670d68 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -7,4 +8,6 @@ dependencies { api(deps.kotlin.coroutines) api(deps.arrow.core) api(deps.arrow.fx) + + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt new file mode 100644 index 0000000000..ea7727681f --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.core.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.math.BigDecimal + +internal object BigDecimalSerializer : KSerializer { + + override val descriptor = PrimitiveSerialDescriptor(serialName = "BigDecimal", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: BigDecimal) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): BigDecimal { + return BigDecimal(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt new file mode 100644 index 0000000000..3243892b18 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.serialization + +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +typealias SerializedBigDecimal = @Serializable(with = BigDecimalSerializer::class) BigDecimal \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 252bfb49d9..fc6ced152c 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -10,10 +11,10 @@ android { dependencies { - implementation(deps.kotlin.coroutines) - implementation(deps.arrow.core) - implementation(deps.kotlin.serialization) + api(projects.domain.staking.models) + api(projects.domain.core) + implementation(deps.kotlin.serialization) implementation(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/staking/models/.gitignore b/domain/staking/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/staking/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts new file mode 100644 index 0000000000..2a478f5d98 --- /dev/null +++ b/domain/staking/models/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + + implementation(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt similarity index 88% rename from domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt index 853c3d4247..aa55291bb5 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt @@ -1,54 +1,64 @@ package com.tangem.domain.staking.model -import java.io.Serializable -import java.math.BigDecimal +import com.tangem.domain.core.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable +@Serializable data class Yield( val id: String, val token: Token, val tokens: List, val args: Args, val status: Status, - val apy: BigDecimal, + val apy: SerializedBigDecimal, val rewardRate: Double, val rewardType: RewardType, val metadata: Metadata, val validators: List, val isAvailable: Boolean, -) : Serializable { +) { + + @Serializable data class Status( val enter: Boolean, val exit: Boolean?, - ) : Serializable + ) + @Serializable data class Args( val enter: Enter, val exit: Enter?, - ) : Serializable { + ) { + + @Serializable data class Enter( val addresses: Addresses, val args: Map, - ) : Serializable { + ) { + + @Serializable data class Addresses( val address: AddressArgument, val additionalAddresses: Map? = null, - ) : Serializable + ) } } + @Serializable data class Validator( val address: String, val status: String, val name: String, val image: String?, val website: String?, - val apr: BigDecimal?, + val apr: SerializedBigDecimal?, val commission: Double?, val stakedBalance: String?, val votingPower: Double?, val preferred: Boolean, - ) : Serializable + ) + @Serializable data class Metadata( val name: String, val logoUri: String, @@ -67,14 +77,17 @@ data class Yield( val supportsMultipleValidators: Boolean, val revshare: Enabled, val fee: Enabled, - ) : Serializable { + ) { + + @Serializable data class Period( val days: Int, - ) : Serializable + ) + @Serializable data class Enabled( val enabled: Boolean, - ) : Serializable + ) } enum class RewardType { @@ -84,6 +97,7 @@ data class Yield( } } +@Serializable data class Token( val name: String, val network: NetworkType, @@ -93,7 +107,7 @@ data class Token( val coinGeckoId: String?, val logoURI: String?, val isPoints: Boolean?, -) : Serializable { +) { enum class NetworkType { AVALANCHE_C, AVALANCHE_ATOMIC, @@ -165,9 +179,10 @@ data class Token( } } +@Serializable data class AddressArgument( val required: Boolean, val network: String? = null, val minimum: Double? = null, val maximum: Double? = null, -) : Serializable \ No newline at end of file +) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index e650d2b8ea..a0468f3100 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -188,11 +188,11 @@ sealed class CryptoCurrency { return "ID(value='$value')" } - private companion object { + companion object { // should use delimiters that could be used in URL not like path or query delimiters - const val PREFIX_DELIMITER = '_' - const val SUFFIX_DELIMITER = ';' - const val DERIVATION_PATH_DELIMITER = 'd' + private const val PREFIX_DELIMITER = '_' + private const val SUFFIX_DELIMITER = ';' + private const val DERIVATION_PATH_DELIMITER = 'd' } } diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt index 05277770a8..d2b216513e 100644 --- a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/navigation/StakingRouter.kt @@ -5,10 +5,4 @@ import androidx.fragment.app.Fragment interface StakingRouter { fun getEntryFragment(): Fragment - - companion object { - const val CRYPTO_CURRENCY_ID_KEY = "CRYPTO_CURRENCY_ID_KEY" - const val USER_WALLET_ID_KEY = "USER_WALLET_ID_KEY" - const val YIELD_KEY = "YIELD_KEY" - } } \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 072e1bcd7b..1547a55bd2 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { /** Common */ implementation(projects.common.ui) + implementation(projects.common.routing) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 7144e4d5ed..68a13a920a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.viewmodel +import android.os.Bundle import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -7,6 +8,8 @@ import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase @@ -52,14 +55,18 @@ internal class StakingViewModel @Inject constructor( var stakingStateRouter: StakingStateRouter by Delegates.notNull() private set - private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle[StakingRouter.CRYPTO_CURRENCY_ID_KEY] + private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle.get( + AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY, + ) + ?.let { it.unbundle(CryptoCurrency.ID.serializer()) } ?: error("This screen can't be opened without `CryptoCurrency.ID`") - private val userWalletId: UserWalletId = savedStateHandle.get(StakingRouter.USER_WALLET_ID_KEY) - ?.let { stringValue -> UserWalletId(stringValue) } + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Staking.USER_WALLET_ID_KEY) + ?.let { it.unbundle(UserWalletId.serializer()) } ?: error("This screen can't be opened without `UserWalletId`") - private val yield: Yield = savedStateHandle[StakingRouter.YIELD_KEY] + private val yield: Yield = savedStateHandle.get(AppRoute.Staking.YIELD_KEY) + ?.let { it.unbundle(Yield.serializer()) } ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() 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 880d92fba4..c2ddee4828 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 @@ -5,6 +5,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment @@ -37,4 +38,14 @@ internal class DefaultTokenDetailsRouter( ), ) } + + override fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield) { + router.push( + AppRoute.Staking( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrency.id, + yield = yield, + ), + ) + } } \ 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 index 56eead22a5..7060190ec5 100644 --- 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.router +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -16,4 +17,6 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter { fun share(text: String) fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) + + fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield) } \ 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 8542738099..09a908e65f 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,7 +4,6 @@ import android.os.Bundle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.os.bundleOf import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse @@ -14,8 +13,6 @@ import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels @@ -70,7 +67,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles -import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin @@ -448,16 +444,7 @@ internal class TokenDetailsViewModel @Inject constructor( val yield = getYieldUseCase.invoke(cryptoCurrency.id, cryptoCurrency.symbol).getOrNull() yield ?: error("Staking is unavailable") - reduxStateHolder.dispatch( - action = NavigationAction.NavigateTo( - screen = AppScreen.Staking, - bundle = bundleOf( - StakingRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - StakingRouter.CRYPTO_CURRENCY_ID_KEY to cryptoCurrency.id, - StakingRouter.YIELD_KEY to yield, - ), - ), - ) + router.openStaking(userWalletId, cryptoCurrency, yield) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 40841c6327..ab66fbb133 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -212,6 +212,7 @@ include(":domain:feedback") include(":domain:qr-scanning") include(":domain:qr-scanning:models") include(":domain:staking") +include(":domain:staking:models") include(":domain:wallet-connect") // endregion Domain modules From 7846772be2ccdd0e4e25c4e015d0abf0f1420ef9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 14 Jun 2024 15:07:26 +0400 Subject: [PATCH 053/223] Updated on 2026-08-14 --- .../disclaimer/redux/DisclaimerMiddleware.kt | 4 +++- .../kotlin/com/tangem/common/routing/AppRoute.kt | 9 ++++++++- .../tangem/features/details/utils/ItemsBuilder.kt | 2 +- features/disclaimer/impl/build.gradle.kts | 1 + .../features/disclaimer/impl/DisclaimerFragment.kt | 8 ++++---- .../disclaimer/impl/di/DisclaimerRouterModule.kt | 6 +++--- .../impl/navigation/DefaultDisclaimerRouter.kt | 12 +++++------- 7 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index 20c8df7fa9..60ffa51781 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -26,7 +26,9 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { when (action) { is DisclaimerAction.Show -> { - store.dispatchNavigationAction { push(AppRoute.Disclaimer) } + store.dispatchNavigationAction { + push(AppRoute.Disclaimer(isTosAccepted = action.from == DisclaimerSource.Details)) + } } is DisclaimerAction.AcceptDisclaimer -> { mainScope.launch { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f912745220..ebb991332a 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -33,7 +33,14 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { } @Serializable - data object Disclaimer : AppRoute(path = "/disclaimer") + data class Disclaimer( + val isTosAccepted: Boolean, + ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}") { + + companion object { + const val IS_TOS_ACCEPTED_KEY = "isTosAccepted" + } + } @Serializable data object OnboardingNote : AppRoute(path = "/onboarding/note") diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 9e87499c97..dbc63d89f5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -89,7 +89,7 @@ internal class ItemsBuilder @Inject constructor( id = "disclaimer", title = resourceReference(R.string.disclaimer_title), iconRes = R.drawable.ic_text_24, - onClick = { router.push(AppRoute.Disclaimer) }, + onClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) }, ), ), ) diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index bc658e93f3..a2053b346f 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.common.routing) /** Domain modules */ implementation(projects.domain.models) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt index e0a30ce395..fb87182dd5 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt @@ -5,7 +5,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme @@ -23,7 +23,7 @@ internal class DisclaimerFragment : ComposeFragment() { override lateinit var uiDependencies: UiDependencies @Inject - lateinit var reduxNavController: ReduxNavController + lateinit var appRouter: AppRouter private val viewModel by viewModels() @@ -40,7 +40,7 @@ internal class DisclaimerFragment : ComposeFragment() { val systemBarsColor = TangemTheme.colors.background.primary BackHandler { if (isTosAccepted) { - reduxNavController.popBackStack() + appRouter.pop() } else { requireActivity().finish() } @@ -48,7 +48,7 @@ internal class DisclaimerFragment : ComposeFragment() { SystemBarsEffect { setSystemBarsColor(systemBarsColor) } - DisclaimerScreen(viewModel.state, reduxNavController::popBackStack) + DisclaimerScreen(viewModel.state, appRouter::pop) } companion object { diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt index 8b3d34e029..cdac031d49 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.features.disclaimer.impl.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter import dagger.Module @@ -18,7 +18,7 @@ object DisclaimerRouterModule { @Provides @ActivityScoped - fun provideDisclaimerRouter(reduxNavController: ReduxNavController): DisclaimerRouter { - return DefaultDisclaimerRouter(reduxNavController) + fun provideDisclaimerRouter(appRouter: AppRouter): DisclaimerRouter { + return DefaultDisclaimerRouter(appRouter) } } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt index 4860d3265d..a49d4a94c4 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt @@ -1,14 +1,12 @@ package com.tangem.features.disclaimer.impl.navigation import androidx.fragment.app.Fragment -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.features.disclaimer.impl.DisclaimerFragment -import javax.inject.Inject -internal class DefaultDisclaimerRouter @Inject constructor( - private val reduxNavController: ReduxNavController, +internal class DefaultDisclaimerRouter( + private val appRouter: AppRouter, ) : InnerDisclaimerRouter { override fun entryFragment(): Fragment = DisclaimerFragment.create() @@ -18,6 +16,6 @@ internal class DefaultDisclaimerRouter @Inject constructor( } override fun openHome() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + appRouter.push(AppRoute.Home) } } \ No newline at end of file From 1ea0843733613dd394a7ef50eb4cd765c9b2e156 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 15:16:43 +0400 Subject: [PATCH 054/223] Updated on 2026-08-14 --- .../staking/impl/presentation/viewmodel/StakingViewModel.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 68a13a920a..107205fa5b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -6,10 +6,10 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase @@ -18,7 +18,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter From 2f5ed092242128d5e6bfc89df8c32384e2ada773 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 16:42:57 +0300 Subject: [PATCH 055/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 14 ++- .../presentation/AddCustomTokenFragment.kt | 10 +- .../AppCurrencySelectorFragment.kt | 9 +- .../appcurrency/AppCurrencySelectorScreen.kt | 12 +- .../ui/appsettings/AppSettingsScreen.kt | 11 +- .../ui/common/DetailsComposeElements.kt | 29 +++-- .../tangem/tap/features/home/HomeFragment.kt | 9 +- .../impl/presentation/TokensListFragment.kt | 8 -- .../impl/presentation/ui/TokensListScreen.kt | 51 +++++--- .../impl/presentation/ui/TokensListToolbar.kt | 13 ++- .../features/welcome/ui/WelcomeFragment.kt | 10 +- .../tangem/core/ui/components/BottomFade.kt | 33 ++++++ .../core/ui/components/SystemBarsEffect.kt | 12 -- .../core/ui/components/SystemBarsUtils.kt | 61 ++++++++++ .../bottomsheets/TangemBottomSheet.kt | 29 +++-- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../com/tangem/core/ui/res/TangemTheme.kt | 13 ++- .../tangem/core/ui/utils/WindowInsetsZero.kt | 12 ++ .../features/details/ui/DetailsScreen.kt | 5 - .../disclaimer/impl/DisclaimerFragment.kt | 6 - .../impl/presentation/ui/DisclaimerScreen.kt | 109 +++++++++--------- .../ui/AddCustomTokenBottomSheet.kt | 15 +-- .../managetokens/ui/ManageTokensScreen.kt | 1 - .../feature/qrscanning/QrScanningFragment.kt | 35 +----- .../feature/referral/ReferralFragment.kt | 11 +- .../ui/AgreementBottomSheetContent.kt | 24 ++-- .../referral/ui/ReferralBottomSheet.kt | 11 ++ .../feature/referral/ui/ReferralScreen.kt | 4 +- .../send/impl/presentation/SendFragment.kt | 6 - .../send/impl/presentation/ui/SendScreen.kt | 4 +- .../impl/presentation/StakingFragment.kt | 6 - .../impl/presentation/ui/StakingScreen.kt | 4 +- .../feature/swap/presentation/SwapFragment.kt | 12 +- .../com/tangem/feature/swap/ui/SwapScreen.kt | 3 +- .../tester/presentation/TesterActivity.kt | 7 +- .../presentation/TokenDetailsFragment.kt | 11 +- .../tokendetails/ui/TokenDetailsScreen.kt | 13 ++- .../wallet/presentation/WalletFragment.kt | 7 -- .../organizetokens/OrganizeTokensScreen.kt | 39 +++---- .../router/DefaultWalletRouter.kt | 3 - .../presentation/wallet/ui/WalletScreen.kt | 61 ++++++---- 41 files changed, 414 insertions(+), 320 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index e55b98f204..cf81714141 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -12,15 +12,18 @@ import android.nfc.NfcAdapter import android.os.Build import android.os.Bundle import android.view.View +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope @@ -195,6 +198,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation + enableEdgeToEdge( + navigationBarStyle = SystemBarStyle.auto( + Color.Transparent.toArgb(), + Color.Transparent.toArgb(), + ), + ) + super.onCreate(savedInstanceState) splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } @@ -307,8 +317,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @SuppressLint("SourceLockedOrientationActivity") private fun initContent() { - WindowCompat.setDecorFitsSystemWindows(window, false) - supportFragmentManager.registerFragmentLifecycleCallbacks( NavBarInsetsFragmentLifecycleCallback(), true, diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index da295c57c5..3bae12b5a1 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -1,12 +1,12 @@ package com.tangem.tap.features.customtoken.impl.presentation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen @@ -30,12 +30,10 @@ internal class AddCustomTokenFragment : ComposeFragment() { val viewModel = hiltViewModel().apply { LocalLifecycleOwner.current.lifecycle.addObserver(this) } - val statusBarColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(color = statusBarColor) - } AddCustomTokenScreen( - modifier = Modifier.systemBarsPadding(), + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .systemBarsPadding(), stateHolder = viewModel.uiState, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt index d48050ec85..e4170e8003 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -6,8 +6,7 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.screen.ComposeFragment import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -22,12 +21,8 @@ internal class AppCurrencySelectorFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + NavigationBar3ButtonsScrim() AppCurrencySelectorScreen( modifier = modifier, state = uiState, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt index b29b503a39..168fccb7e7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -40,7 +41,9 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier val listState = rememberLazyListState() Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = modifier + .imePadding() + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = TangemTheme.colors.background.secondary, topBar = { TopBar( @@ -49,6 +52,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier state = state, ) }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), content = { paddingValues -> val contentModifier = Modifier .padding(paddingValues) @@ -62,7 +66,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier modifier = contentModifier, listState = listState, currencies = state.items, - selectedId = state.selectedId.orEmpty(), + selectedId = state.selectedId, onCurrencyClick = state.onCurrencyClick, ) } @@ -175,6 +179,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi @Composable private fun LoadingList(modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } Column(modifier = modifier) { repeat(times = 10) { Row( @@ -199,6 +204,7 @@ private fun LoadingList(modifier: Modifier = Modifier) { ) } } + Spacer(Modifier.height(bottomBarHeight)) } } @@ -210,9 +216,11 @@ private fun CurrenciesList( onCurrencyClick: (Currency) -> Unit, modifier: Modifier = Modifier, ) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( modifier = modifier, state = listState, + contentPadding = PaddingValues(bottom = bottomBarHeight), ) { items( items = currencies, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index c60f34ecce..f3d0b721c9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,13 +1,17 @@ package com.tangem.tap.features.details.ui.appsettings import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -32,6 +36,7 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> }, titleRes = R.string.app_settings_title, onBackClick = onBackClick, + addBottomInsets = false, ) } @@ -44,7 +49,11 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { null -> Unit } - LazyColumn { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + LazyColumn( + contentPadding = PaddingValues(bottom = bottomBarHeight), + ) { items( items = state.items, key = Item::id, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index 35b36c0f00..8cdc6777a9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -13,8 +13,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.wallet.R @Composable @@ -24,31 +24,42 @@ internal fun SettingsScreensScaffold( modifier: Modifier = Modifier, @StringRes titleRes: Int? = null, snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + addBottomInsets: Boolean = true, fab: @Composable () -> Unit = {}, ) { val state = rememberScaffoldState(snackbarHostState = snackbarHostState) val backgroundColor = TangemTheme.colors.background.secondary BackHandler(onBack = onBackClick) - SystemBarsEffect { - setSystemBarsColor(backgroundColor) - } Scaffold( scaffoldState = state, topBar = { EmptyTopBarWithNavigation( + modifier = Modifier.statusBarsPadding(), onBackClick = onBackClick, backgroundColor = backgroundColor, ) }, - modifier = modifier.systemBarsPadding(), + modifier = modifier, + contentWindowInsets = WindowInsetsZero, backgroundColor = backgroundColor, - floatingActionButton = fab, - content = { paddings -> + floatingActionButton = { + Box(modifier = Modifier.navigationBarsPadding()) { + fab() + } + }, + content = { paddingValues -> Column( modifier = Modifier - .padding(paddings) + .run { + if (addBottomInsets) { + navigationBarsPadding() + } else { + this + } + } + .padding(paddingValues) .fillMaxSize(), ) { if (titleRes != null) { @@ -81,9 +92,11 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { @Composable internal fun EmptyTopBarWithNavigation( onBackClick: () -> Unit, + modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.primary, ) { TopAppBar( + modifier = modifier, title = { }, navigationIcon = { diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index e27f58f4a3..f3fae68135 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -6,14 +6,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess @@ -44,15 +42,12 @@ class HomeFragment : ComposeFragment(), StoreSubscriber { @Composable override fun ScreenContent(modifier: Modifier) { BackHandler(onBack = requireActivity()::finish) - SystemBarsEffect { - setSystemBarsColor(color = Color.Transparent, darkIcons = false) - } + SystemBarsIconsDisposable(darkIcons = false) ScreenContent() } override fun onStart() { super.onStart() - activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) } store.subscribe(subscriber = this) { state -> state 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 272c773c95..ed260691fe 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 @@ -1,13 +1,10 @@ package com.tangem.tap.features.tokens.impl.presentation -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel @@ -30,12 +27,7 @@ internal class TokensListFragment : ComposeFragment() { val viewModel = hiltViewModel().apply { LocalLifecycleOwner.current.lifecycle.addObserver(this) } - val statusBarColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(color = statusBarColor) - } TokensListScreen( - modifier = Modifier.systemBarsPadding(), stateHolder = viewModel.uiState, ) } 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 56366dfb51..d045437bb8 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 @@ -15,7 +15,6 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity @@ -34,6 +33,8 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @@ -61,32 +62,45 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi Scaffold( modifier = modifier, - topBar = { TokensListToolbar(state = stateHolder.toolbarState) }, + topBar = { + TokensListToolbar(state = stateHolder.toolbarState) + }, floatingActionButton = { if (stateHolder is TokensListStateHolder.ManageContent) { val density = LocalDensity.current val verticalPadding = TangemTheme.dimens.spacing32 SaveChangesButton( - modifier = Modifier.onSizeChanged { - with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding } - }, + modifier = Modifier + .navigationBarsPadding() + .onSizeChanged { + with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding } + }, showProgress = stateHolder.isSavingInProgress, onClick = stateHolder.onSaveButtonClick, ) } }, floatingActionButtonPosition = FabPosition.Center, - backgroundColor = TangemTheme.colors.background.primary, - ) { scaffoldPadding -> + backgroundColor = TangemTheme.colors.background.secondary, + ) { _ -> val tokens = stateHolder.tokens.collectAsLazyPagingItems() - TokensListContent( - isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible, - tokens = tokens, - scaffoldPadding = scaffoldPadding, - bottomMarginDp = floatingButtonHeight, - ) + if (stateHolder !is TokensListStateHolder.ManageContent) { + NavigationBar3ButtonsScrim() + } + + Box { + TokensListContent( + isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible, + tokens = tokens, + bottomMarginDp = floatingButtonHeight, + ) + + if (stateHolder is TokensListStateHolder.ManageContent) { + BottomFade(Modifier.align(Alignment.BottomCenter)) + } + } Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") { if (it) { @@ -112,23 +126,24 @@ private fun LoadingContent() { } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun TokensListContent( isDifferentAddressesBlockVisible: Boolean, tokens: LazyPagingItems, - scaffoldPadding: PaddingValues, bottomMarginDp: Dp, ) { val state = rememberLazyListState() + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( modifier = Modifier + .background(color = TangemTheme.colors.background.primary) .imePadding() - .fillMaxSize() - .padding(scaffoldPadding), + .fillMaxSize(), state = state, - contentPadding = PaddingValues(bottom = bottomMarginDp), + contentPadding = PaddingValues( + bottom = bottomMarginDp + bottomBarHeight, + ), ) { item( key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible", diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt index 5e0aafc999..0a4dd9c309 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt @@ -1,10 +1,7 @@ package com.tangem.tap.features.tokens.impl.presentation.ui import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -16,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -42,8 +40,15 @@ internal fun TokensListToolbar(state: TokensListToolbarState) { } else { AppBarDefaults.TopAppBarElevation } + val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } + TopAppBar( backgroundColor = TangemTheme.colors.background.secondary, + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing4, + end = TangemTheme.dimens.spacing4, + top = statusBarHeight, + ), elevation = toolbarElevation, ) { IconButton(onClick = state.onBackButtonClick) { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 41ef679b6a..c34497d9f0 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -16,7 +16,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.common.analytics.events.SignIn @@ -49,19 +48,14 @@ internal class WelcomeFragment : ComposeFragment() { val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val warning by rememberUpdatedState(newValue = state.warning) - val backgroundColor = TangemTheme.colors.background.primary - SystemBarsEffect { - setSystemBarsColor(backgroundColor) - } - BackHandler { requireActivity().finish() } Box( modifier = modifier - .systemBarsPadding() - .background(backgroundColor), + .background(TangemTheme.colors.background.primary) + .systemBarsPadding(), ) { WelcomeScreenContent( showUnlockProgress = state.showUnlockWithBiometricsProgress, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt new file mode 100644 index 0000000000..fbb4ae896b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.res.TangemTheme + +/** + * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. + */ +@Composable +fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt deleted file mode 100644 index ea843b7a7a..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import com.google.accompanist.systemuicontroller.SystemUiController -import com.google.accompanist.systemuicontroller.rememberSystemUiController - -@Composable -fun SystemBarsEffect(block: SystemUiController.() -> Unit) { - val systemUiController = rememberSystemUiController() - SideEffect { block(systemUiController) } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt new file mode 100644 index 0000000000..0e40554581 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt @@ -0,0 +1,61 @@ +package com.tangem.core.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.core.ui.res.LocalIsInDarkTheme + +/** + * Provides the ability to set a scrim for 3-button navigation + * + * Automatically makes navigation bar transparent when the composable is disposed. + * + * Usage example: [com.tangem.feature.tokendetails.presentation.TokenDetailsFragment] + */ +@Composable +fun NavigationBar3ButtonsScrim() { + val systemUiController = rememberSystemUiController() + val isDarkTheme = LocalIsInDarkTheme.current + SideEffect { + systemUiController.isNavigationBarContrastEnforced = true + } + LaunchedEffect(systemUiController.isNavigationBarContrastEnforced) { + if (systemUiController.isNavigationBarContrastEnforced.not()) { + systemUiController.isNavigationBarContrastEnforced = true + } + } + DisposableEffect(isDarkTheme) { + onDispose { + systemUiController.isNavigationBarContrastEnforced = false + } + } +} + +/** + * Provides the ability to set dark/light icons in cases where the darkness of the screen differs from that + * provided in [TangemTheme]. + * + * Automatically returns the icon colors to their original state when the composable is disposed. + * + * Usage example: [com.tangem.feature.qrscanning.QrScanningFragment] + */ +@Composable +fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) { + val systemUiController = rememberSystemUiController() + + SideEffect { + systemUiController.systemBarsDarkContentEnabled = darkIcons + systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced + } + + val isDarkTheme = LocalIsInDarkTheme.current + + DisposableEffect(isDarkTheme) { + onDispose { + systemUiController.systemBarsDarkContentEnabled = !isDarkTheme + systemUiController.isNavigationBarContrastEnforced = false + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 3f934a0f9b..ec10ddfa0a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -1,13 +1,14 @@ package com.tangem.core.ui.components.bottomsheets -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch * Tangem bottom sheet with custom draggable header and config * * @param config data model containing logic and ui models + * @param * @param content custom bottom sheet content */ @OptIn(ExperimentalMaterial3Api::class) @@ -22,20 +24,33 @@ import kotlinx.coroutines.launch inline fun TangemBottomSheet( config: TangemBottomSheetConfig, containerColor: Color = TangemTheme.colors.background.primary, + addBottomInsets: Boolean = true, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { var isVisible by remember { mutableStateOf(value = config.isShow) } - + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) if (isVisible && config.content is T) { ModalBottomSheet( + // FIXME temporary solution to fix height of the bottom sheet + modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), onDismissRequest = config.onDismissRequest, sheetState = sheetState, containerColor = containerColor, shape = TangemTheme.shapes.bottomSheetLarge, + windowInsets = WindowInsetsZero, dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, ) { - content(config.content) + if (addBottomInsets) { + Column( + // FIXME temporary solution to fix height of the bottom sheet + modifier = Modifier.navigationBarsPadding(), + ) { + content(config.content) + } + } else { + content(config.content) + } } } 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 863f3e59cc..a2649901ad 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 @@ -81,6 +81,7 @@ data class TangemDimens internal constructor( val size90: Dp = 90.dp, val size93: Dp = 93.dp, val size96: Dp = 96.dp, + val size100: Dp = 100.dp, val size102: Dp = 102.dp, val size108: Dp = 108.dp, val size110: Dp = 110.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 968b635c5d..58552b1353 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -5,9 +5,10 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.MockHapticManager - import com.tangem.core.ui.windowsize.WindowSize @Composable @@ -25,6 +26,16 @@ fun TangemTheme( .also { it.update(themeColors) } val shapes = remember { TangemShapes(dimens) } + val systemUiController = rememberSystemUiController() + + SideEffect { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = !isDark, + isNavigationBarContrastEnforced = false, + ) + } + MaterialTheme( colors = materialThemeColors(colors = themeColors, isDark = isDark), ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt new file mode 100644 index 0000000000..a17ac5f1ef --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.utils + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +object WindowInsetsZero : WindowInsets { + override fun getBottom(density: Density): Int = 0 + override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 + override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 + override fun getTop(density: Density): Int = 0 +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index af683ca7a8..02c43a0396 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -19,7 +19,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.ComposableContentComponent -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme @@ -42,10 +41,6 @@ internal fun DetailsScreen( val backgroundColor = TangemTheme.colors.background.secondary val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - SystemBarsEffect { - setSystemBarsColor(backgroundColor) - } - BackHandler(onBack = state.popBack) Scaffold( diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt index fb87182dd5..e17e88299f 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt @@ -7,8 +7,6 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen @@ -37,7 +35,6 @@ internal class DisclaimerFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.primary BackHandler { if (isTosAccepted) { appRouter.pop() @@ -45,9 +42,6 @@ internal class DisclaimerFragment : ComposeFragment() { requireActivity().finish() } } - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } DisclaimerScreen(viewModel.state, appRouter::pop) } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt index f99bd7bc4e..e9a40a7a03 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt @@ -15,17 +15,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons import com.tangem.core.ui.components.appbar.models.AdditionalButton @@ -39,17 +38,14 @@ import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer @Composable internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { - val bottomPadding = if (state.isTosAccepted) TangemTheme.dimens.spacing16 else TangemTheme.dimens.size64 + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + Box( modifier = Modifier - .statusBarsPadding() - .background(TangemColorPalette.Dark6), + .background(TangemColorPalette.Dark6) + .statusBarsPadding(), ) { - Column( - modifier = Modifier - .navigationBarsPadding() - .padding(bottom = bottomPadding), - ) { + Column { AppBarWithAdditionalButtons( text = resourceReference(R.string.disclaimer_title), startButton = AdditionalButton( @@ -58,23 +54,34 @@ internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { ).takeIf { state.isTosAccepted }, ) DisclaimerContent(state.url) + Spacer( + modifier = Modifier + .background(TangemColorPalette.Dark6) + .height( + height = if (!state.isTosAccepted) { + bottomBarHeight + } else { + bottomBarHeight + TangemTheme.dimens.size64 + }, + ), + ) } + if (!state.isTosAccepted) { + BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = TangemColorPalette.Dark6) DisclaimerButton(state.onAccept) + } else { + NavigationBar3ButtonsScrim() } } } @Composable private fun ColumnScope.DisclaimerContent(url: String) { - val transparent = Color.Transparent - val progressState = remember { mutableStateOf(ProgressState.Loading) } val webClient = remember { DisclaimerWebViewClient(progressState) } Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f), + modifier = Modifier, ) { AndroidView( factory = { @@ -83,9 +90,10 @@ private fun ColumnScope.DisclaimerContent(url: String) { ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, ) - setBackgroundColor(transparent.toArgb()) + setBackgroundColor(TangemColorPalette.Dark6.toArgb()) settings.allowFileAccess = false settings.javaScriptEnabled = false + overScrollMode = View.OVER_SCROLL_NEVER webViewClient = webClient loadUrl(url) @@ -94,28 +102,34 @@ private fun ColumnScope.DisclaimerContent(url: String) { ) when (progressState.value) { - ProgressState.Loading -> CircularProgressIndicator( - color = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.Center) - .padding(TangemTheme.dimens.spacing8), - ) - ProgressState.Error -> Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing16) - .align(Alignment.Center), - ) { - Text( - text = stringResource(id = R.string.disclaimer_error_loading), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.constantWhite, - ) - PrimaryButton( - text = stringResource(R.string.common_retry), - onClick = { webClient.reset() }, - ) + ProgressState.Loading -> { + Box(modifier = Modifier.fillMaxSize()) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) + } + } + ProgressState.Error -> Box(modifier = Modifier.fillMaxSize()) { + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing16) + .align(Alignment.Center), + ) { + Text( + text = stringResource(id = R.string.disclaimer_error_loading), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.constantWhite, + ) + PrimaryButton( + text = stringResource(R.string.common_retry), + onClick = { webClient.reset() }, + ) + } } else -> Unit } @@ -130,23 +144,6 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { } else { true } - val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(TangemTheme.dimens.size110 + bottomInsetsPx.dp) - .background( - Brush.verticalGradient( - colors = listOf( - TangemTheme.colors.background.primary.copy(alpha = 0f), - TangemTheme.colors.background.primary.copy(alpha = 0.75f), - TangemTheme.colors.background.primary.copy(alpha = 0.95f), - TangemTheme.colors.background.primary, - ), - ), - ), - ) PrimaryButton( text = stringResource(id = R.string.common_accept), onClick = { onAccept(shouldAskPushPermission) }, diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt index 69fe567327..95dfd58871 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt @@ -4,12 +4,7 @@ import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalOnBackPressedDispatcherOwner import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.* import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -36,6 +31,7 @@ import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalDensity import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -43,7 +39,9 @@ import androidx.navigation.compose.rememberNavController import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader import com.tangem.core.ui.components.bottomsheets.collapse +import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRoute import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel @@ -57,6 +55,7 @@ fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) { var isVisible by remember { mutableStateOf(value = config.isShow) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } if (isVisible) { // ViewModel cannot be scoped to ModalBottomSheet's lifecycle, @@ -67,11 +66,13 @@ fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) { } ModalBottomSheetWithBackHandling( + modifier = Modifier + .sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), onDismissRequest = config.onDismissRequest, sheetState = sheetState, containerColor = TangemTheme.colors.background.tertiary, shape = TangemTheme.shapes.bottomSheetLarge, - windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top), + windowInsets = WindowInsetsZero, dragHandle = { TangemBottomSheetDraggableHeader(color = TangemTheme.colors.background.tertiary) }, properties = ModalBottomSheetDefaults.properties(shouldDismissOnBackPress = false), ) { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt index 15197a8e4a..6b91d83e18 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt @@ -65,7 +65,6 @@ private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) Box( modifier = Modifier .fillMaxSize() - .navigationBarsPadding() .imePadding() .background(color = TangemTheme.colors.background.primary), ) { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 72d725c460..5118630639 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -7,21 +7,16 @@ import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.mlkit.vision.common.InputImage import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter @@ -105,7 +100,8 @@ internal class QrScanningFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - StatusBarTransparencyDisposable() + SystemBarsIconsDisposable(darkIcons = false) + QrScanningContent( executor = { cameraExecutor }, analyzer = { cameraAnalyzer }, @@ -145,29 +141,4 @@ internal class QrScanningFragment : ComposeFragment() { fun create() = QrScanningFragment() } -} - -@Composable -private fun StatusBarTransparencyDisposable() { - val systemUiController = rememberSystemUiController() - val systemBarsColor = TangemTheme.colors.background.secondary - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(Unit) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = false, - ) - } - if (event == Lifecycle.Event.ON_STOP) { - systemUiController.setSystemBarsColor(systemBarsColor) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index 3d157aa005..cec296cc80 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -1,14 +1,11 @@ package com.tangem.feature.referral import android.os.Bundle -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen @@ -35,12 +32,6 @@ class ReferralFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val backgroundColor = TangemTheme.colors.background.secondary - SystemBarsEffect { setSystemBarsColor(backgroundColor) } - - ReferralScreen( - modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, - ) + ReferralScreen(stateHolder = viewModel.uiState) } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index e44e192d0d..8d8b5aadfa 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -2,23 +2,20 @@ package com.tangem.feature.referral.ui import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R /** @@ -28,15 +25,16 @@ import com.tangem.feature.referral.presentation.R */ @Composable internal fun AgreementBottomSheetContent(url: String) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + Column( modifier = Modifier - .fillMaxWidth() - .height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16), + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) - AgreementHtmlView(url = url) - } + AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) + AgreementHtmlView(url = url) + Spacer(modifier = Modifier.height(bottomBarHeight)) } } @@ -46,7 +44,7 @@ private fun AgreementHtmlView(url: String) { val isInPreviewMode = LocalInspectionMode.current WebView( state = state, - modifier = Modifier.background(TangemTheme.colors.background.secondary), + modifier = Modifier.background(TangemTheme.colors.background.primary), captureBackPresses = false, onCreated = { if (!isInPreviewMode) { diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt index 2d70513c8a..d91a122117 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt @@ -1,11 +1,18 @@ package com.tangem.feature.referral.ui +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.referral.models.ReferralStateHolder @OptIn(ExperimentalMaterial3Api::class) @@ -16,14 +23,18 @@ internal fun ReferralBottomSheet( onDismissRequest: () -> Unit, config: ReferralStateHolder.ReferralInfoState, ) { + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + if (isVisible) { ModalBottomSheet( + modifier = Modifier.height(LocalWindowSize.current.height - statusBarHeight), onDismissRequest = onDismissRequest, sheetState = sheetState, shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, topEnd = TangemTheme.dimens.radius16, ), + windowInsets = WindowInsetsZero, containerColor = TangemTheme.colors.background.primary, ) { AgreementBottomSheetContent( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 39b216fb16..7d6614a7aa 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -46,16 +46,16 @@ import kotlinx.coroutines.launch */ @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier = Modifier) { +internal fun ReferralScreen(stateHolder: ReferralStateHolder) { var isBottomSheetVisible by remember { mutableStateOf(value = false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val snackbarHostState = remember(::SnackbarHostState) Scaffold( - modifier = modifier, topBar = { AppBarWithBackButton( + modifier = Modifier.statusBarsPadding(), text = stringResource(R.string.details_referral_title), onBackClick = stateHolder.headerState.onBackClicked, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 0a3b32c382..231c029e8f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -9,8 +9,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter @@ -61,10 +59,6 @@ internal class SendFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.tertiary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle() SendScreen(viewModel.uiState, currentState.value) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index cdef0b59c6..b05b60afe0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -46,10 +46,10 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) BackHandler(onBack = onBackClick) Column( modifier = Modifier + .background(color = TangemTheme.colors.background.tertiary) .fillMaxSize() .imePadding() - .systemBarsPadding() - .background(color = TangemTheme.colors.background.tertiary), + .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { SendAppBar( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index 9648e0164d..64b167ead5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -7,8 +7,6 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.staking.impl.navigation.InnerStakingRouter @@ -59,10 +57,6 @@ internal class StakingFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.tertiary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } val currentState = viewModel.uiState.collectAsStateWithLifecycle() StakingScreen(currentState.value) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index ec18cf17e0..51be837ffe 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -28,10 +28,10 @@ internal fun StakingScreen(uiState: StakingUiState) { BackHandler(onBack = uiState.clickIntents::onBackClick) Column( modifier = Modifier + .background(color = TangemTheme.colors.background.tertiary) .fillMaxSize() .imePadding() - .systemBarsPadding() - .background(color = TangemTheme.colors.background.tertiary), + .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { SendAppBar( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 51ac5b23a2..60df4a856b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -2,13 +2,13 @@ package com.tangem.feature.swap.presentation import android.os.Bundle import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.swap.router.CustomTabsManager @@ -47,17 +47,17 @@ class SwapFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { viewModel.onScreenOpened() - - val backgroundColor = TangemTheme.colors.background.secondary - SystemBarsEffect { setSystemBarsColor(backgroundColor) } - ScreenContent(viewModel = viewModel) } @Suppress("TopLevelComposableFunctions") @Composable private fun ScreenContent(viewModel: SwapViewModel) { - Crossfade(targetState = viewModel.currentScreen, label = "") { screen -> + Crossfade( + modifier = Modifier.background(TangemTheme.colors.background.secondary), + targetState = viewModel.currentScreen, + label = "", + ) { screen -> when (screen) { SwapNavScreen.Main -> SwapScreen(stateHolder = viewModel.uiState) SwapNavScreen.Success -> { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 04a53a581c..ff34b02557 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig @@ -28,7 +29,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { iconRes = R.drawable.ic_close_24, ) }, - contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0), + contentWindowInsets = WindowInsetsZero, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index c94208933f..62206ea5e5 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -6,9 +6,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity import com.tangem.feature.tester.presentation.actions.TesterActionsScreen @@ -45,9 +45,8 @@ internal class TesterActivity : ComposeActivity() { @Composable override fun ScreenContent(modifier: Modifier) { val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } + val systemUiController = rememberSystemUiController() + systemUiController.setSystemBarsColor(systemBarsColor) TesterNavHost() } 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 index 94200fbd59..bbbe520ddf 100644 --- 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 @@ -5,8 +5,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen @@ -33,14 +32,8 @@ internal class TokenDetailsFragment : ComposeFragment() { override fun ScreenContent(modifier: Modifier) { val viewModel = hiltViewModel() viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) - - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - + NavigationBar3ButtonsScrim() TokenDetailsScreen(state = viewModel.uiState) } } \ 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 ddcb006eae..e4574897ac 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 @@ -3,10 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi @@ -14,6 +11,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -21,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -60,11 +59,13 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { BackHandler(onBack = state.topAppBarConfig.onBackClick) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> val pullRefreshState = rememberPullRefreshState( @@ -90,7 +91,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = TangemTheme.dimens.spacing16), + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, + ), ) { item { TokenInfoBlock( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 5a2c923015..6535806903 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -3,8 +3,6 @@ package com.tangem.feature.wallet.presentation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.features.managetokens.navigation.ManageTokensUi @@ -37,11 +35,6 @@ internal class WalletFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - _walletRouter.Initialize( onFinish = requireActivity()::finish, manageTokensUi = manageTokensUi, 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 4efcf827cb..6c3aea984f 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 @@ -16,11 +16,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +27,7 @@ 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.dp +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig @@ -36,6 +36,7 @@ import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem @@ -57,8 +58,12 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier Scaffold( modifier = modifier, topBar = { - TopBar(state.header, tokensListState) + TopBar( + config = state.header, + tokensListState = tokensListState, + ) }, + contentWindowInsets = WindowInsetsZero, content = { paddingValues -> TokenList( modifier = Modifier @@ -72,7 +77,9 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { - Actions(state.actions) + Box(modifier = Modifier.navigationBarsPadding()) { + Actions(state.actions) + } }, containerColor = TangemTheme.colors.background.secondary, ) @@ -104,9 +111,11 @@ private fun TokenList( onDragEnd = onDragEnd, ) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val listContentPadding = PaddingValues( top = TangemTheme.dimens.spacing4, - bottom = TangemTheme.dimens.spacing92, + bottom = TangemTheme.dimens.spacing92 + bottomBarHeight, start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, ) @@ -140,7 +149,7 @@ private fun TokenList( } } - BottomGradient(modifier = Modifier.align(Alignment.BottomCenter)) + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) } } @@ -191,23 +200,6 @@ private fun LazyItemScope.DraggableItem( } } -@Composable -private fun BottomGradient(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size116) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - TangemTheme.colors.background.secondary, - ), - ), - ), - ) -} - @Composable private fun TopBar( config: OrganizeTokensState.HeaderConfig, @@ -228,6 +220,7 @@ private fun TopBar( modifier = modifier .shadow(elevation) .background(TangemTheme.colors.background.secondary) + .statusBarsPadding() .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), ) { 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 69906449ee..292cea1bde 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,9 +1,7 @@ package com.tangem.feature.wallet.presentation.router import android.annotation.SuppressLint -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.* -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment @@ -86,7 +84,6 @@ internal class DefaultWalletRouter( val uiState by viewModel.uiState.collectAsStateWithLifecycle() OrganizeTokensScreen( - modifier = Modifier.statusBarsPadding(), state = uiState, ) } 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 eccfc5ea55..3dde33fa2b 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 @@ -20,6 +20,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController @@ -33,16 +34,13 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -50,6 +48,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* @@ -112,7 +111,7 @@ internal fun WalletScreen( ) } -@Suppress("LongMethod", "LongParameterList") +@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") @Composable private fun WalletContent( state: WalletScreenState, @@ -144,12 +143,14 @@ private fun WalletContent( .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + LazyColumn( modifier = Modifier .fillMaxSize() .testTag(TestTags.WALLET_SCREEN), contentPadding = PaddingValues( - bottom = TangemTheme.dimens.spacing92, + bottom = TangemTheme.dimens.spacing92 + bottomBarHeight, ), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -330,7 +331,7 @@ private fun BaseScaffoldManageTokenRedesign( coroutineScope.launch { bottomSheetState.partialExpand() } } }, - content = { paddingValues -> + content = { _ -> val pullRefreshState = rememberPullRefreshState( refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, onRefresh = { @@ -338,9 +339,7 @@ private fun BaseScaffoldManageTokenRedesign( }, ) - Column( - modifier = Modifier.padding(paddingValues), - ) { + Column { WalletTopBar(config = state.topBarConfig) Box( modifier = Modifier.pullRefresh(pullRefreshState), @@ -392,7 +391,7 @@ private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: ( } @OptIn(ExperimentalMaterial3Api::class) -@Suppress("CyclomaticComplexMethod") +@Suppress("CyclomaticComplexMethod", "MagicNumber") @Composable private fun BottomSheetStateEffects( bottomSheetState: SheetState, @@ -425,19 +424,29 @@ private fun BottomSheetStateEffects( val systemUiController = rememberSystemUiController() val navigationBarColor = TangemTheme.colors.background.primary - val navigationBarColorWithout = TangemTheme.colors.background.secondary - SystemBarsEffect { - if (showManageTokensBottomSheet) { - setNavigationBarColor(navigationBarColor) + LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) { + when (bottomSheetState.targetValue) { + SheetValue.Hidden, + SheetValue.Expanded, + -> systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = navigationBarColor.luminance() > 0.5f, + navigationBarContrastEnforced = true, + ) + SheetValue.PartiallyExpanded, + -> systemUiController.setNavigationBarColor(navigationBarColor) } } - DisposableEffect( - showManageTokensBottomSheet, - ) { + + DisposableEffect(showManageTokensBottomSheet) { onDispose { if (showManageTokensBottomSheet) { - systemUiController.setNavigationBarColor(navigationBarColorWithout) + systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = navigationBarColor.luminance() > 0.5f, + navigationBarContrastEnforced = false, + ) } } } @@ -506,6 +515,7 @@ private fun BaseScaffold( ) { Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, + contentWindowInsets = WindowInsetsZero, snackbarHost = { WalletSnackbarHost( snackbarHostState = snackbarHostState, @@ -520,7 +530,12 @@ private fun BaseScaffold( ) } - manageTokensButtonConfig?.let { ManageTokensButton(onClick = it.onClick) } + manageTokensButtonConfig?.let { + ManageTokensButton( + modifier = Modifier.navigationBarsPadding(), + onClick = it.onClick, + ) + } }, floatingActionButtonPosition = FabPosition.Center, containerColor = TangemTheme.colors.background.secondary, @@ -544,6 +559,8 @@ private fun BaseScaffold( state = pullRefreshState, modifier = Modifier.align(Alignment.TopCenter), ) + + BottomFade(Modifier.align(Alignment.BottomCenter)) } }, ) @@ -565,11 +582,11 @@ private fun WalletSnackbarHost( } @Composable -private fun ManageTokensButton(onClick: () -> Unit) { +private fun ManageTokensButton(onClick: () -> Unit, modifier: Modifier = Modifier) { PrimaryButton( text = stringResource(id = R.string.main_manage_tokens), onClick = onClick, - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), ) From 884813d9fa8fb3053738fbc8c682b2bf06ed5b26 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Jun 2024 13:29:46 +0400 Subject: [PATCH 056/223] Updated on 2026-08-14 --- .../com/tangem/common/routing/AppRoute.kt | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index ebb991332a..0720f58f9d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -12,9 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @Serializable -sealed class AppRoute(val path: String) : Route, RouteBundleParams { - - override fun getBundle(): Bundle = bundle(serializer()) +sealed class AppRoute(val path: String) : Route { @Serializable data object Initial : AppRoute(path = "/initial") @@ -25,7 +23,9 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { @Serializable data class Welcome( val intent: SerializableIntent? = null, - ) : AppRoute(path = "/welcome") { + ) : AppRoute(path = "/welcome"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val INITIAL_INTENT_KEY = "intent" @@ -35,7 +35,9 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { @Serializable data class Disclaimer( val isTosAccepted: Boolean, - ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}") { + ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val IS_TOS_ACCEPTED_KEY = "isTosAccepted" @@ -48,7 +50,9 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { @Serializable data class OnboardingWallet( val canSkipBackup: Boolean = true, - ) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}") { + ) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val CAN_SKIP_BACKUP_KEY = "canSkipBackup" @@ -68,7 +72,9 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { data class CurrencyDetails( val userWalletId: UserWalletId, val currency: CryptoCurrency, - ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") { + ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val USER_WALLET_ID_KEY = "userWalletId" @@ -90,7 +96,11 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { "&$amount" + "&$tag" + "&$destinationAddress", - ) { + ), + RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + companion object { const val USER_WALLET_ID_KEY = "userWalletId" const val CRYPTO_CURRENCY_KEY = "currency" @@ -134,7 +144,9 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { data class QrScanning( val source: SourceType, val networkName: String? = null, - ) : AppRoute(path = "/qr_scanning") { + ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val SOURCE_KEY = "source" @@ -148,7 +160,9 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { @Serializable data class Swap( val currency: CryptoCurrency, - ) : AppRoute(path = "/swap") { + ) : AppRoute(path = "/swap/${currency.id.value}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val CURRENCY_BUNDLE_KEY = "currency" @@ -172,7 +186,10 @@ sealed class AppRoute(val path: String) : Route, RouteBundleParams { val userWalletId: UserWalletId, val cryptoCurrencyId: CryptoCurrency.ID, val yield: Yield, - ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/${yield.id}") { + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/${yield.id}"), + RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) companion object { const val USER_WALLET_ID_KEY = "userWalletId" From 14c02eec467943069ba4d9c1b6c3c19d9763d6aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 10 Jun 2024 17:25:23 +0100 Subject: [PATCH 057/223] Updated on 2026-08-14 --- .../card/DefaultDerivationsRepository.kt | 25 ++++++++++++++++++- gradle/dependencies.toml | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index b7ad02d630..d7bb96e4c3 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -1,13 +1,17 @@ package com.tangem.tap.domain.card import com.tangem.common.CompletionResult +import com.tangem.common.card.EllipticCurve +import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet @@ -45,7 +49,10 @@ internal class DefaultDerivationsRepository( tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold( - onSuccess = { return }, + onSuccess = { + validateDerivations(userWallet.scanResponse, derivations) + return + }, onFailure = { throw it }, ) } @@ -76,6 +83,22 @@ internal class DefaultDerivationsRepository( } } + /** + * It throws an exception if any of the provided derivations are invalid + * Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths + * It needs to be called after success [derivePublicKeys] or in same flows + */ + private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) { + derivations.entries.forEach { derivationForKey -> + val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key } + if (wallet == null) return@forEach + val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } } + if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) { + throw TangemSdkError.NonHardenedDerivationNotSupported() + } + } + } + private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result { return runCatching(dispatchers.io) { userWalletsStore.update( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a9ef5f15c0..f67ad9b355 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -90,7 +90,7 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "develop-654" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-360" +tangemCardSdk = "develop-366" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 60258b30969b3cb2e33277b34d84f24b97738024 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 15:41:23 +0400 Subject: [PATCH 058/223] Updated on 2026-08-14 --- .../features/details/redux/DetailsAction.kt | 2 + .../details/redux/DetailsMiddleware.kt | 1 + .../features/details/redux/DetailsReducer.kt | 3 ++ .../ui/appsettings/AppSettingsViewModel.kt | 37 ++++++++++++++++--- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index cc3efe720c..302a437624 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -97,6 +97,8 @@ sealed class DetailsAction : Action { data class ChangeAppCurrency( val currency: AppCurrency, ) : AppSettings() + + data class Prepare(val state: AppSettingsState) : AppSettings() } data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction() 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 23a552773c..981f7fe8a6 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 @@ -245,6 +245,7 @@ class DetailsMiddleware { is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.BiometricsStatusChanged, + is DetailsAction.AppSettings.Prepare, -> Unit } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 658dfcd6ba..f2d048b261 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -283,6 +283,9 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail isHidingEnabled = action.hideBalance, ), ) + is DetailsAction.AppSettings.Prepare -> state.copy( + appSettingsState = action.state, + ) is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.CheckBiometricsStatus, -> state diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 60f7de63fd..34a693dbb4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -1,14 +1,17 @@ package com.tangem.tap.features.details.ui.appsettings -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.* import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler - +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.details.DetailsFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -27,14 +30,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class AppSettingsViewModel @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, + private val walletsRepository: WalletsRepository, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val balanceHidingRepository: BalanceHidingRepository, private val analyticsEventHandler: AnalyticsEventHandler, + private val appThemeModeRepository: AppThemeModeRepository, + private val settingsRepository: SettingsRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, + private val detailsFeatureToggles: DetailsFeatureToggles, ) : ViewModel(), StoreSubscriber, DefaultLifecycleObserver { @@ -51,6 +62,9 @@ internal class AppSettingsViewModel @Inject constructor( init { bootstrapAppCurrencyUpdates() + if (detailsFeatureToggles.isRedesignEnabled) { + bootstrapBiometricsUpdates() + } subscribeToStoreChanges() sendItemsAnalytics() @@ -215,6 +229,19 @@ internal class AppSettingsViewModel @Inject constructor( .saveIn(appCurrencyUpdatesJobHolder) } + private fun bootstrapBiometricsUpdates() = viewModelScope.launch { + val state = AppSettingsState( + saveWallets = walletsRepository.shouldSaveUserWalletsSync(), + saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), + isBiometricsAvailable = canUseBiometryUseCase(), + isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, + selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, + selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, + ) + + store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state)) + } + private fun subscribeToStoreChanges() { store.subscribe(subscriber = this) { state -> state.skipRepeats { oldState, newState -> From fa948bd91afd45b8fe64de160681cc2bd7480706 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Jun 2024 21:26:16 +0300 Subject: [PATCH 059/223] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 3 + .../common/ui/amountScreen/ui/AmountBlock.kt | 101 ++++++++++++ .../ui/amountScreen}/utils/FormatterUtils.kt | 8 +- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 11 +- .../state/confirm/SendNotificationFactory.kt | 2 +- .../fee/custom/BitcoinCustomFeeConverter.kt | 2 +- .../fee/custom/EthereumCustomFeeConverter.kt | 2 +- .../presentation/ui/SendNavigationButtons.kt | 5 +- .../ui/fee/SendSpeedSelectorItem.kt | 4 +- .../impl/presentation/ui/send/FeeBlock.kt | 4 +- features/staking/impl/build.gradle.kts | 1 + .../impl/presentation/state/InnerFeeState.kt | 16 ++ .../presentation/state/InnerValidatorState.kt | 16 ++ .../presentation/state/StakingNotification.kt | 54 +++++++ .../state/StakingStateController.kt | 1 - .../presentation/state/StakingStateRouter.kt | 17 +- .../impl/presentation/state/StakingUiState.kt | 50 +++--- .../ConfirmStakingStatePreviewData.kt | 92 +++++++++++ .../SetConfirmStateDataStateTransformer.kt | 35 +++++ .../SetInitialDataStateTransformer.kt | 2 +- .../presentation/ui/StakingConfirmContent.kt | 72 +++++++++ .../ui/StakingNavigationButtons.kt | 8 +- .../impl/presentation/ui/StakingScreen.kt | 11 +- .../ui/StakingValidatorAndFeeContent.kt | 11 -- .../ui/block/NotificationsBlock.kt | 13 ++ .../presentation/ui/block/StakingFeeBlock.kt | 148 ++++++++++++++++++ .../viewmodel/StakingViewModel.kt | 44 ++++-- 28 files changed, 651 insertions(+), 83 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt rename {features/send/impl/src/main/java/com/tangem/features/send/impl/presentation => common/ui/src/main/java/com/tangem/common/ui/amountScreen}/utils/FormatterUtils.kt (79%) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index fc0e428770..e57cd07bd1 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -30,4 +30,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt new file mode 100644 index 0000000000..942186b435 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -0,0 +1,101 @@ +package com.tangem.common.ui.amountScreen.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.core.ui.components.ResizableText +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter + +@Composable +fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) { + if (amountState !is AmountState.Data) return + val amount = amountState.amountTextField + + val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val fiatAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount.fiatAmount.value, + fiatCurrencySymbol = amount.fiatAmount.currencySymbol, + fiatCurrencyCode = amountState.appCurrencyCode, + ) + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + val (firstAmount, secondAmount) = if (amount.isFiatValue) { + fiatAmount to cryptoAmount + } else { + cryptoAmount to fiatAmount + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) + .padding(TangemTheme.dimens.spacing16), + ) { + TokenIcon(state = amountState.tokenIconState) + ResizableText( + text = firstAmount, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing24), + ) + Text( + text = secondAmount, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing8), + ) + } +} + +// region Preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: AmountState) { + TangemThemePreview { + AmountBlock( + amountState = value, + isClickDisabled = false, + isEditingDisabled = false, + onClick = {}, + ) + } +} + +private class AmountBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + AmountStatePreviewData.amountState, + ) +} +// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt similarity index 79% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index 7c8a2becb6..67c6ed8cc9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.utils +package com.tangem.common.ui.amountScreen.utils import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference @@ -11,7 +11,7 @@ import java.math.BigDecimal private const val CRYPTO_FEE_DECIMALS = 6 -internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { +fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { if (amount == null) return null return combinedReference( if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, @@ -25,13 +25,13 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex ) } -internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { +fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { if (value == null || rate == null) return null val formattedFiat = getFiatString(value = value, rate = rate, appCurrency = appCurrency) return stringReference(formattedFiat) } -internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { +fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) return BigDecimalFormatter.formatFiatAmount( diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9e421c5384..7ad7dd2099 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -543,6 +543,7 @@ Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Неверный адрес Транзакция отправлена + Подготовьтесь к сканированию карты, которую вы хотите настроить. Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 49aad9383e..752c42de5a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -115,7 +115,7 @@ Primary Card Passphrase Paste - %1$s-%2$s + %1$s-%2$s Read more Receive Reject @@ -310,7 +310,12 @@ Upvote Choose wallet The wallet doesn\'t support more than one network + To start buying, exchanging or receiving this asset, add this token to at least 1 network + This asset is not available + Add to portfolio + My portfolio Market + Select wallet Sort By You have to set up a single access code to protect all your cards Protect @@ -541,6 +546,7 @@ Invalid address %1$s (%2$s) Transaction sent + Prepare to scan card you want to setup. Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name @@ -559,7 +565,10 @@ Staking %s Unbonding Period Warmup period + Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. + Earn staking rewards Rewards + Validator Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 7d19a4e8f8..3982fbe052 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter @@ -25,7 +26,6 @@ import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.fee.* import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.utils.getFiatString import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index 62bf1460ad..2d5e5999e3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal @@ -14,7 +15,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.utils.getFiatReference import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index d64f1c3d47..0c9185cf91 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal @@ -14,7 +15,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.utils.getFiatReference import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index f5247e516a..157a21ad02 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -21,6 +21,8 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.R import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SecondaryButtonIconStart @@ -32,12 +34,9 @@ import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.tokens.model.Amount import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.utils.getFiatString -import com.tangem.features.send.impl.presentation.utils.getCryptoReference @Composable internal fun SendNavigationButtons( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index ed1ad443a4..b1cf3f315d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem @@ -20,8 +22,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType -import com.tangem.features.send.impl.presentation.utils.getCryptoReference -import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun SendSpeedSelectorItem( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index f2dcd33987..6873ed2b63 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -14,6 +14,8 @@ 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.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemThemePreview @@ -24,8 +26,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData -import com.tangem.features.send.impl.presentation.utils.getCryptoReference -import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) { diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 1547a55bd2..8325def273 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.transaction) /** Common */ implementation(projects.common.ui) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt new file mode 100644 index 0000000000..2511ffa889 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt @@ -0,0 +1,16 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.transaction.TransactionFee + +@Immutable +internal sealed class InnerFeeState { + + data class Content( + val fees: TransactionFee, + ) : InnerFeeState() + + data object Loading : InnerFeeState() + + data object Error : InnerFeeState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt new file mode 100644 index 0000000000..8a3c91fa45 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt @@ -0,0 +1,16 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.domain.staking.model.Yield + +@Immutable +internal sealed class InnerValidatorState { + + data class Content( + val chosenValidator: Yield.Validator, + ) : InnerValidatorState() + + data object Loading : InnerValidatorState() + + data object Error : InnerValidatorState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt new file mode 100644 index 0000000000..967f681edb --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -0,0 +1,54 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.staking.impl.R + +internal sealed class StakingNotification(val config: NotificationConfig) { + + sealed class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : StakingNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonState, + onCloseClick = onCloseClick, + ), + ) { + // TODO staking + } + + sealed class Warning( + title: TextReference, + subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : StakingNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) { + data class EarnRewards( + val currencyName: String, + val days: Int, + ) : Warning( + title = resourceReference(R.string.staking_notification_earn_rewards_title), + subtitle = resourceReference( + R.string.staking_notification_earn_rewards_text, + wrappedList(currencyName, days), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 74f5a5bff2..8a61d12bad 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -39,7 +39,6 @@ internal class StakingStateController @Inject constructor() { currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), - validatorState = StakingStates.ValidatorState.Empty(), confirmStakingState = StakingStates.ConfirmStakingState.Empty(), isBalanceHidden = false, event = consumedEvent(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index bc00ead8ca..7d70ebd667 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,7 +1,6 @@ package com.tangem.features.staking.impl.presentation.state import androidx.fragment.app.FragmentManager -import kotlinx.coroutines.flow.update import java.lang.ref.WeakReference internal class StakingStateRouter( @@ -22,7 +21,7 @@ internal class StakingStateRouter( isSuccess -> popBackStack() else -> when (type) { StakingStep.Amount -> showInitial() - StakingStep.ValidatorAndFee -> showAmount() + StakingStep.Confirm -> showAmount() else -> popBackStack() } } @@ -31,9 +30,9 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.uiState.value.currentStep) { StakingStep.InitialInfo -> showAmount() - StakingStep.Amount -> showValidator() - StakingStep.ValidatorAndFee -> showConfirm() - StakingStep.Confirm -> onBackClick() + StakingStep.Amount -> showConfirm() + StakingStep.Confirm -> showSuccess() + StakingStep.Success -> onBackClick() } } @@ -52,13 +51,13 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Amount) } } - fun showValidator() { - stateController.update { it.copy(currentStep = StakingStep.ValidatorAndFee) } - } - fun showConfirm() { stateController.update { it.copy(currentStep = StakingStep.Confirm) } } + fun showSuccess() { + stateController.update { it.copy(currentStep = StakingStep.Success) } + } + private fun getInitialState() = StakingStep.InitialInfo } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index ebea79fcbc..bd01183407 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -1,10 +1,15 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable +import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.Yield import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal /** * Ui states of the staking screen @@ -16,7 +21,6 @@ internal data class StakingUiState( val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, - val validatorState: StakingStates.ValidatorState, val confirmStakingState: StakingStates.ConfirmStakingState, val isBalanceHidden: Boolean, val event: StateEvent, @@ -25,12 +29,10 @@ internal data class StakingUiState( fun copyWrapped( initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, amountState: AmountState = this.amountState, - validatorState: StakingStates.ValidatorState = this.validatorState, confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState, ): StakingUiState = copy( initialInfoState = initialInfoState, amountState = amountState, - validatorState = validatorState, confirmStakingState = confirmStakingState, ) } @@ -58,32 +60,14 @@ internal sealed class StakingStates { ) : InitialInfoState() } - /** Validator state */ - sealed class ValidatorState : StakingStates() { - data class Data( - override val isPrimaryButtonEnabled: Boolean, - ) : ValidatorState() - - data class Empty( - override val isPrimaryButtonEnabled: Boolean = false, - ) : ValidatorState() - } - - /** Fee state */ - sealed class FeeState : StakingStates() { - data class Data( - override val isPrimaryButtonEnabled: Boolean, - ) : FeeState() - - data class Empty( - override val isPrimaryButtonEnabled: Boolean = false, - ) : FeeState() - } - /** Confirm state */ sealed class ConfirmStakingState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val feeState: FeeState, + val validatorState: ValidatorState, + val notifications: ImmutableList, + val footerText: String, val isSuccess: Boolean, val isStaking: Boolean, ) : ConfirmStakingState() @@ -92,11 +76,25 @@ internal sealed class StakingStates { override val isPrimaryButtonEnabled: Boolean = false, ) : ConfirmStakingState() } + + data class FeeState( + val innerFeeState: InnerFeeState, + val fee: Fee?, + val rate: BigDecimal?, + val isFeeConvertibleToFiat: Boolean, + val appCurrency: AppCurrency, + val isFeeApproximate: Boolean, + ) + + data class ValidatorState( + val validatorState: InnerValidatorState, + val availableValidators: List, + ) } enum class StakingStep { InitialInfo, Amount, - ValidatorAndFee, Confirm, + Success, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt new file mode 100644 index 0000000000..13d3cc4a1c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt @@ -0,0 +1,92 @@ +package com.tangem.features.staking.impl.presentation.state.previewdata + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType.Coin +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.InnerFeeState +import com.tangem.features.staking.impl.presentation.state.InnerValidatorState +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +internal object ConfirmStakingStatePreviewData { + + private val validatorList = listOf( + Yield.Validator( + address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", + status = "active", + name = "Luganodes", + image = "https://assets.stakek.it/validators/luganodes.png", + apr = BigDecimal("0.054823398040640445"), + commission = 0.1, + stakedBalance = "355544384.45009977", + website = "https://luganodes.com/", + votingPower = 0.09778360195377911, + preferred = true, + ), + Yield.Validator( + address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", + status = "active", + name = "InfStones", + image = "https://assets.stakek.it/validators/infstones.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "12495684.05643019", + website = "https://infstones.com/", + votingPower = 0.0034366257754399774, + preferred = true, + ), + Yield.Validator( + address = "0xd14a87025109013b0a2354a775cb335f926af65a", + status = "active", + name = "Kiln", + image = "https://assets.stakek.it/validators/kiln.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "85400369.96393165", + website = "https://infstones.com/", + votingPower = 0.023487238579718264, + preferred = true, + ), + ) + + private val fee = Fee.Common( + amount = Amount( + currencySymbol = "MATIC", + value = BigDecimal(0.159806), + decimals = 18, + type = Coin, + ), + ) + + val confirmStakingState = StakingStates.ConfirmStakingState.Data( + isPrimaryButtonEnabled = true, + feeState = StakingStates.FeeState( + innerFeeState = InnerFeeState.Content(TransactionFee.Single(fee)), + fee = fee, + rate = BigDecimal.ONE, + appCurrency = AppCurrency.Default, + isFeeApproximate = false, + isFeeConvertibleToFiat = true, + ), + validatorState = StakingStates.ValidatorState( + validatorState = InnerValidatorState.Content( + chosenValidator = validatorList[0], + ), + availableValidators = validatorList, + ), + footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", + notifications = persistentListOf( + StakingNotification.Warning.EarnRewards( + currencyName = "Solana", + days = 2, + ), + ), + isStaking = false, + isSuccess = false, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt new file mode 100644 index 0000000000..1097794d4b --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt @@ -0,0 +1,35 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer + +@Suppress("UnusedPrivateMember") +internal class SetConfirmStateDataStateTransformer( + private val yield: Yield, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + // TODO staking fill with real data + return prevState.copy( + confirmStakingState = ConfirmStakingStatePreviewData.confirmStakingState, + ) + } + + private fun isFeeApproximate(fee: Fee): Boolean { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + return isFeeApproximateUseCase( + networkId = cryptoCurrencyStatus.currency.network.id, + amountType = fee.amount.type, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index dcbe461b4f..c06765c6ed 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -93,7 +93,7 @@ internal class SetInitialDataStateTransformer( if (maxApr - minApr < EQUALITY_THRESHOLD) { return stringReference("$formattedMinApr%") } - return resourceReference(R.string.common_percent_range, wrappedList(formattedMinApr, formattedMaxApr)) + return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr)) } companion object { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt new file mode 100644 index 0000000000..9c78086d8f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt @@ -0,0 +1,72 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock +import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock + +@Composable +internal fun StakingConfirmContent(amountState: AmountState, state: StakingStates.ConfirmStakingState) { + if (state !is StakingStates.ConfirmStakingState.Data) return + + Column( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + AmountBlock( + amountState = amountState, + isClickDisabled = true, + isEditingDisabled = true, + onClick = {}, + ) + StakingFeeBlock(feeState = state.feeState) + NotificationsBlock(notifications = state.notifications) + SpacerHMax() + FooterText(text = state.footerText) + } +} + +@Composable +private fun FooterText(text: String) { + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_StakingConfirmContent() { + TangemThemePreview { + Column(Modifier.background(TangemTheme.colors.background.primary)) { + StakingConfirmContent( + amountState = AmountStatePreviewData.amountState, + state = ConfirmStakingStatePreviewData.confirmStakingState, + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index aaceadf9c6..21105e3419 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -51,8 +51,8 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier val isSuccess = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isSuccess ?: false val isStaking = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isStaking ?: false - val isButtonsVisible = uiState.currentStep != StakingStep.Confirm - val isStakingState = uiState.currentStep == StakingStep.Confirm && !isSuccess && !isStaking + val isButtonsVisible = uiState.currentStep != StakingStep.Success + val isStakingState = uiState.currentStep == StakingStep.Success && !isSuccess && !isStaking val (buttonTextId, buttonClick) = getButtonData( currentState = uiState, @@ -103,8 +103,8 @@ private fun getButtonData(currentState: StakingUiState): Pair Unit> { return when (currentState.currentStep) { StakingStep.InitialInfo, StakingStep.Amount, - StakingStep.ValidatorAndFee, StakingStep.Confirm, + StakingStep.Success, -> R.string.common_next to { currentState.clickIntents.onNextClick() } } } @@ -113,7 +113,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean { return when (uiState.currentStep) { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled - StakingStep.ValidatorAndFee -> uiState.validatorState.isPrimaryButtonEnabled StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + StakingStep.Success -> true } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 51be837ffe..a8f36b6e5e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -52,13 +52,13 @@ private fun SendAppBar(uiState: StakingUiState) { val titleRes = when (uiState.currentStep) { StakingStep.InitialInfo -> stringResource(id = R.string.common_stake) StakingStep.Amount -> stringResource(id = R.string.send_amount_label) - StakingStep.ValidatorAndFee -> stringResource(id = R.string.common_stake) - StakingStep.Confirm -> "" + StakingStep.Confirm -> stringResource(id = R.string.common_stake) + StakingStep.Success -> "" } val backIcon = when (uiState.currentStep) { StakingStep.Amount, - StakingStep.ValidatorAndFee, StakingStep.Confirm, + StakingStep.Success, -> { R.drawable.ic_close_24 } @@ -127,8 +127,9 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) - StakingStep.ValidatorAndFee -> StakingValidatorAndFeeContent( - state = uiState.validatorState, + StakingStep.Confirm -> StakingConfirmContent( + amountState = uiState.amountState, + state = uiState.confirmStakingState, ) else -> TODO() } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt deleted file mode 100644 index a73d0000d4..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorAndFeeContent.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.staking.impl.presentation.ui - -import androidx.compose.runtime.Composable -import com.tangem.features.staking.impl.presentation.state.StakingStates - -@Composable -internal fun StakingValidatorAndFeeContent(state: StakingStates.ValidatorState) { - if (state !is StakingStates.ValidatorState.Data) return - - // TODO staking -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt new file mode 100644 index 0000000000..68d638aaaa --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -0,0 +1,13 @@ +package com.tangem.features.staking.impl.presentation.ui.block + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.StakingNotification + +@Composable +internal fun NotificationsBlock(notifications: List) { + notifications.forEach { + Notification(config = it.config, iconTint = TangemTheme.colors.icon.accent) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt new file mode 100644 index 0000000000..3aa945bd20 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -0,0 +1,148 @@ +package com.tangem.features.staking.impl.presentation.ui.block + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +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.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.staking.impl.presentation.state.InnerFeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import java.math.BigDecimal + +@Composable +internal fun StakingFeeBlock(feeState: StakingStates.FeeState) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResource(R.string.common_network_fee_title), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + + Box( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + val feeAmount = feeState.fee?.amount + val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + SelectorRowItem( + titleRes = title, + iconRes = icon, + preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), + postDot = if (feeState.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) + } else { + null + }, + ellipsizeOffset = feeAmount?.currencySymbol?.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + FeeLoading(feeState.innerFeeState) + FeeError(feeState.innerFeeState) + } + } +} + +@Composable +private fun BoxScope.FeeLoading(feeSelectorState: InnerFeeState) { + AnimatedContent( + targetState = feeSelectorState, + label = "Fee Loading State Change", + modifier = Modifier.align(Alignment.CenterEnd), + ) { + if (it == InnerFeeState.Loading) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier.size( + height = TangemTheme.dimens.size12, + width = TangemTheme.dimens.size90, + ), + ) + } + } +} + +@Composable +private fun BoxScope.FeeError(feeSelectorState: InnerFeeState) { + AnimatedContent( + targetState = feeSelectorState, + label = "Fee Error State Change", + modifier = Modifier.align(Alignment.CenterEnd), + ) { + if (it == InnerFeeState.Error) { + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + ) + } + } +} + +// region Preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: StakingStates.FeeState) { + TangemThemePreview { + StakingFeeBlock( + feeState = value, + ) + } +} + +private class FeeBlockPreviewProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + feeState, + ) + + private val fee = Fee.Common( + amount = Amount( + currencySymbol = "MATIC", + value = BigDecimal(0.159806), + decimals = 18, + type = AmountType.Coin, + ), + ) + + private val feeState = StakingStates.FeeState( + innerFeeState = InnerFeeState.Content(TransactionFee.Single(normal = fee)), + fee = fee, + rate = BigDecimal.ONE, + appCurrency = AppCurrency.Default, + isFeeApproximate = false, + isFeeConvertibleToFiat = true, + ) +} + +// endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 107205fa5b..15d7803cc7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -15,14 +15,17 @@ import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmStateDataStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer @@ -45,6 +48,7 @@ internal class StakingViewModel @Inject constructor( private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val isFeeApproximateUseCase: IsFeeApproximateUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -72,9 +76,10 @@ internal class StakingViewModel @Inject constructor( private var innerRouter: InnerStakingRouter by Delegates.notNull() private var userWallet: UserWallet by Delegates.notNull() - private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private var appCurrency: AppCurrency by Delegates.notNull() init { + subscribeOnSelectedAppCurrency() subscribeOnBalanceHiding() subscribeOnCurrencyStatusUpdates() } @@ -85,6 +90,27 @@ internal class StakingViewModel @Inject constructor( override fun onNextClick() { stakingStateRouter.onNextClick() + when (value.currentStep) { + StakingStep.Confirm -> { + stateController.update( + SetConfirmStateDataStateTransformer( + yield = yield, + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + isFeeApproximateUseCase = isFeeApproximateUseCase, + ), + ) + } + StakingStep.InitialInfo -> { + // TODO staking + } + StakingStep.Amount -> { + // TODO staking + } + StakingStep.Success -> { + // TODO staking + } + } } override fun onPrevClick() { @@ -131,7 +157,7 @@ internal class StakingViewModel @Inject constructor( yield = yield, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, + appCurrencyProvider = Provider { appCurrency }, ), ) }, @@ -153,18 +179,14 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() + private fun subscribeOnSelectedAppCurrency() { + getSelectedAppCurrencyUseCase() .conflate() .distinctUntilChanged() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } + .onEach { maybeAppCurrency -> + appCurrency = maybeAppCurrency.getOrElse { AppCurrency.Default } } .flowOn(dispatchers.main) - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) + .launchIn(viewModelScope) } } \ No newline at end of file From dd6d478c4f0db2b81c7ce601a3e78aaf48c6825f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:17:08 +0500 Subject: [PATCH 060/223] Updated on 2026-08-14 --- .../local/preferences/PreferencesKeys.kt | 10 +++++----- .../settings/DefaultPermissionRepository.kt | 19 ++++++++++--------- .../settings/DelayPermissionRequestUseCase.kt | 2 +- .../IsFirstTimeAskingPermissionUseCase.kt | 2 +- .../settings/NeverRequestPermissionUseCase.kt | 2 +- .../SetFirstTimeAskingPermissionUseCase.kt | 2 +- .../settings/ShouldAskPermissionUseCase.kt | 2 +- .../ShouldInitiallyAskPermissionUseCase.kt | 2 +- .../repositories/PermissionRepository.kt | 12 ++++++------ 9 files changed, 27 insertions(+), 26 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 4d357add91..42df77c07d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -96,17 +96,17 @@ object PreferencesKeys { fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") // region Permission - fun getShouldShowPermission(permission: Int) = booleanPreferencesKey("shouldShowPushPermission_$permission") + fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") - fun getShouldShowInitialPermissionScreen(permission: Int) = + fun getShouldShowInitialPermissionScreen(permission: String) = booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") - fun getIsFirstTimeAskingPermission(permission: Int) = + fun getIsFirstTimeAskingPermission(permission: String) = booleanPreferencesKey("shouldAskInitialPushPermission_$permission") - fun getPermissionLaunchCount(permission: Int) = intPreferencesKey("pushPermissionLaunchCount_$permission") + fun getPermissionLaunchCount(permission: String) = intPreferencesKey("pushPermissionLaunchCount_$permission") - fun getPermissionDaysCount(permission: Int) = longPreferencesKey("pushPermissionDaysCount_$permission") + fun getPermissionDaysCount(permission: String) = longPreferencesKey("pushPermissionDaysCount_$permission") // endregion } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt index c2f07bae1c..b73d5d069f 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt @@ -16,26 +16,27 @@ internal class DefaultPermissionRepository( private val appPreferencesStore: AppPreferencesStore, ) : PermissionRepository { - override suspend fun shouldInitiallyShowPermissionScreen(permission: Int): Boolean { + override suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean { val key = getShouldShowInitialPermissionScreen(permission) val initialPermissionScreen = appPreferencesStore.getSyncOrDefault(key = key, default = true) if (initialPermissionScreen) appPreferencesStore.store(key = key, value = false) return initialPermissionScreen } - override suspend fun isFirstTimeAskingPermission(permission: Int): Boolean = appPreferencesStore.getSyncOrDefault( - key = getIsFirstTimeAskingPermission(permission), - default = true, - ) + override suspend fun isFirstTimeAskingPermission(permission: String): Boolean = + appPreferencesStore.getSyncOrDefault( + key = getIsFirstTimeAskingPermission(permission), + default = true, + ) - override suspend fun setFirstTimeAskingPermission(permission: Int, value: Boolean) { + override suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) { appPreferencesStore.store( key = getIsFirstTimeAskingPermission(permission), value = value, ) } - override suspend fun shouldAskPermission(permission: Int): Boolean { + override suspend fun shouldAskPermission(permission: String): Boolean { val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0) val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0) @@ -47,11 +48,11 @@ internal class DefaultPermissionRepository( return shouldAskPermission && isDaysDelayed && isLaunchesDelayed } - override suspend fun neverAskPermission(permission: Int) { + override suspend fun neverAskPermission(permission: String) { appPreferencesStore.store(key = getShouldShowPermission(permission), value = false) } - override suspend fun delayPermissionAsking(permission: Int) { + override suspend fun delayPermissionAsking(permission: String) { appPreferencesStore.editData { val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) val nowMillis = SystemClock.elapsedRealtime() diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt index 7c4c74baba..bf300cb779 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt @@ -7,7 +7,7 @@ class DelayPermissionRequestUseCase( private val repository: PermissionRepository, ) { - suspend operator fun invoke(permission: Int): Either = Either.catch { + suspend operator fun invoke(permission: String): Either = Either.catch { repository.delayPermissionAsking(permission) } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt index f6b9f562d8..000bfd7ed1 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt @@ -5,7 +5,7 @@ import com.tangem.domain.settings.repositories.PermissionRepository class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { - suspend operator fun invoke(permission: Int): Either = Either.catch { + suspend operator fun invoke(permission: String): Either = Either.catch { repository.isFirstTimeAskingPermission(permission) } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt index 7b9f0219b5..84d654dc42 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt @@ -7,7 +7,7 @@ class NeverRequestPermissionUseCase( private val repository: PermissionRepository, ) { - suspend operator fun invoke(permission: Int): Either = Either.catch { + suspend operator fun invoke(permission: String): Either = Either.catch { repository.neverAskPermission(permission) } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt index 6a82495cf4..fa5d057e77 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt @@ -5,7 +5,7 @@ import com.tangem.domain.settings.repositories.PermissionRepository class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { - suspend operator fun invoke(permission: Int): Either = Either.catch { + suspend operator fun invoke(permission: String): Either = Either.catch { repository.setFirstTimeAskingPermission(permission, false) } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt index 2cf3378e84..44f063119a 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt @@ -6,5 +6,5 @@ class ShouldAskPermissionUseCase( private val repository: PermissionRepository, ) { - suspend operator fun invoke(permission: Int): Boolean = repository.shouldAskPermission(permission) + suspend operator fun invoke(permission: String): Boolean = repository.shouldAskPermission(permission) } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt index b020fc8c9e..b7d5a6f1ca 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt @@ -5,7 +5,7 @@ import com.tangem.domain.settings.repositories.PermissionRepository class ShouldInitiallyAskPermissionUseCase(private val repository: PermissionRepository) { - suspend operator fun invoke(permission: Int): Either = Either.catch { + suspend operator fun invoke(permission: String): Either = Either.catch { repository.shouldInitiallyShowPermissionScreen(permission) } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt index 390868c8d4..fd136cf651 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt @@ -6,34 +6,34 @@ interface PermissionRepository { * Return true if should display screen ONCE asking to allow [permission]. * False otherwise or screen already was displayed */ - suspend fun shouldInitiallyShowPermissionScreen(permission: Int): Boolean + suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean /** * Indicates which time [permission] was asked via platform dialog. * NOTE: Use this method to indicate either reroute to settings or display platform dialog. */ - suspend fun isFirstTimeAskingPermission(permission: Int): Boolean + suspend fun isFirstTimeAskingPermission(permission: String): Boolean /** * Sets value indicating that [permission] was asked via platform dialog. * NOTE: Use this method to indicate either reroute to settings or display platform dialog. */ - suspend fun setFirstTimeAskingPermission(permission: Int, value: Boolean) + suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) /** * Is clear to ask [permission]. * User could already granted or permanently denied permission * Or there is an active delay before next request */ - suspend fun shouldAskPermission(permission: Int): Boolean + suspend fun shouldAskPermission(permission: String): Boolean /** * Permanently deny [permission] and never request again */ - suspend fun neverAskPermission(permission: Int) + suspend fun neverAskPermission(permission: String) /** * Delay next [permission] request for some time or active sessions */ - suspend fun delayPermissionAsking(permission: Int) + suspend fun delayPermissionAsking(permission: String) } \ No newline at end of file From 7f131732ffcbeb32f64dc30c14694a5197ad8764 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:17:56 +0500 Subject: [PATCH 061/223] Updated on 2026-08-14 --- .../common/settings/IntentSettingsManager.kt | 20 +++++++++ .../java/com/tangem/tap/di/UtilsModule.kt | 6 +++ .../configs/feature_toggles_config.json | 4 ++ .../settings/DummySettingsManager.kt | 5 +++ .../navigation/settings/SettingsManager.kt | 5 +++ core/ui/build.gradle.kts | 1 + features/push-notifications/api/.gitignore | 1 + .../push-notifications/api/build.gradle.kts | 15 +++++++ .../PushNotificationsFeatureToggles.kt | 9 ++++ features/push-notifications/impl/.gitignore | 1 + .../push-notifications/impl/build.gradle.kts | 42 +++++++++++++++++++ .../PushNotificationsFeatureTogglesModule.kt | 24 +++++++++++ .../DefaultPushNotificationsFeatureToggles.kt | 11 +++++ settings.gradle.kts | 3 ++ 14 files changed, 147 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt create mode 100644 features/push-notifications/api/.gitignore create mode 100644 features/push-notifications/api/build.gradle.kts create mode 100644 features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt create mode 100644 features/push-notifications/impl/.gitignore create mode 100644 features/push-notifications/impl/build.gradle.kts create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt new file mode 100644 index 0000000000..7dbed67b24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.common.settings + +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal class IntentSettingsManager : SettingsManager { + override fun openSettings() { + foregroundActivityObserver.withForegroundActivity { activity -> + val openSettingsIntent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", activity.packageName, null), + ) + activity.startActivity(openSettingsIntent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index d5565743e9..e5c898b899 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -3,10 +3,12 @@ package com.tangem.tap.di import android.content.Context import com.tangem.core.navigation.feedback.FeedbackManager import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.feedback.ProxyFeedbackManager import com.tangem.tap.common.finisher.AndroidAppFinisher +import com.tangem.tap.common.settings.IntentSettingsManager import com.tangem.tap.common.share.IntentShareManager import com.tangem.tap.common.url.CustomTabsUrlOpener import dagger.Module @@ -35,4 +37,8 @@ internal object UtilsModule { @Provides @Singleton fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context) + + @Provides + @Singleton + fun provideSettingsManager(): SettingsManager = IntentSettingsManager() } \ 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 015ca0a117..1f8dd14df9 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -42,5 +42,9 @@ { "name": "DETAILS_REDESIGN_ENABLED", "version": "undefined" + }, + { + "name": "PUSH_NOTIFICATIONS_ENABLED", + "version": "undefined" } ] diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt new file mode 100644 index 0000000000..25c4706fb9 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt @@ -0,0 +1,5 @@ +package com.tangem.core.navigation.settings + +class DummySettingsManager : SettingsManager { + override fun openSettings() { /* no-op */ } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt new file mode 100644 index 0000000000..4be904c9cb --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt @@ -0,0 +1,5 @@ +package com.tangem.core.navigation.settings + +interface SettingsManager { + fun openSettings() +} \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 9933f9f8df..cb8f7f15ee 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) implementation(deps.material) implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) diff --git a/features/push-notifications/api/.gitignore b/features/push-notifications/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/push-notifications/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/push-notifications/api/build.gradle.kts b/features/push-notifications/api/build.gradle.kts new file mode 100644 index 0000000000..4335917f36 --- /dev/null +++ b/features/push-notifications/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.pushnotifications.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt new file mode 100644 index 0000000000..06346f3b01 --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt @@ -0,0 +1,9 @@ +package com.tangem.features.pushnotifications.api.featuretoggles + +/** + * Push notifications feature toggles + */ +interface PushNotificationsFeatureToggles { + /** Availability of push notifications */ + val isRedesignedSendEnabled: Boolean +} \ No newline at end of file diff --git a/features/push-notifications/impl/.gitignore b/features/push-notifications/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/push-notifications/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts new file mode 100644 index 0000000000..4524106ece --- /dev/null +++ b/features/push-notifications/impl/build.gradle.kts @@ -0,0 +1,42 @@ +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.feature.pushnotifications.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) + + /** Other dependencies */ + implementation(deps.timber) + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + + /** Domain module */ + implementation(projects.domain.settings) + + /** Feature modules */ + implementation(projects.features.pushNotifications.api) + + /** DI */ + implementation(deps.hilt.android) + implementation(project(":core:navigation")) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt new file mode 100644 index 0000000000..0df7b0a222 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.pushnotifications.impl.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.impl.featuretoggles.DefaultPushNotificationsFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * DI module provides implementation of [PushNotificationsFeatureToggles] + */ +@Module +@InstallIn(SingletonComponent::class) +internal object PushNotificationsFeatureTogglesModule { + + @Provides + @Singleton + fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): PushNotificationsFeatureToggles { + return DefaultPushNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt new file mode 100644 index 0000000000..05c8d6dfc4 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.pushnotifications.impl.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles + +internal class DefaultPushNotificationsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : PushNotificationsFeatureToggles { + override val isRedesignedSendEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED") +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index ab66fbb133..acbedb8b55 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -180,6 +180,9 @@ include(":features:details:impl") include(":features:disclaimer:api") include(":features:disclaimer:impl") + +include(":features:push-notifications:api") +include(":features:push-notifications:impl") // endregion Feature modules // region Domain modules From bd20838395aa8012f1c0fc18340e2f8d5b01d479 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 13 Jun 2024 19:45:04 +0500 Subject: [PATCH 062/223] Updated on 2026-08-14 --- .../utils/SendScreenAnalyticSender.kt | 3 +- .../presentation/state/SendStateFactory.kt | 34 +++-- .../impl/presentation/state/SendUiState.kt | 28 +--- .../state/amount/AmountStateFactory.kt | 3 +- .../presentation/state/amount/AmountUtils.kt | 59 -------- .../amount/SendAmountCurrencyConverter.kt | 35 ++--- ...SendAmountPastedTriggerDismissConverter.kt | 12 +- .../amount/SendAmountReduceByConverter.kt | 48 +------ .../amount/SendAmountReduceToConverter.kt | 36 +---- .../SendAmountSegmentedButtonsConfig.kt | 21 --- .../state/amount/SendAmountStateConverter.kt | 63 -------- .../state/confirm/SendNotificationFactory.kt | 7 +- .../fields/SendAmountFieldChangeConverter.kt | 68 +-------- .../state/fields/SendAmountFieldConverter.kt | 76 ---------- .../SendAmountFieldMaxAmountConverter.kt | 30 +--- .../state/fields/SendTextField.kt | 17 --- .../previewdata/AmountStatePreviewData.kt | 89 ------------ .../state/previewdata/SendClickIntentsStub.kt | 1 + .../previewdata/SendStatesPreviewData.kt | 1 + .../presentation/ui/SendNavigationButtons.kt | 3 +- .../send/impl/presentation/ui/SendScreen.kt | 11 +- .../presentation/ui/amount/AmountButtons.kt | 123 ---------------- .../presentation/ui/amount/AmountField.kt | 134 ------------------ .../ui/amount/AmountFieldContainer.kt | 69 --------- .../ui/amount/SendAmountContent.kt | 68 --------- .../impl/presentation/ui/send/AmountBlock.kt | 105 -------------- .../impl/presentation/ui/send/SendContent.kt | 3 +- .../viewmodel/SendClickIntents.kt | 13 +- .../presentation/viewmodel/SendViewModel.kt | 11 +- 29 files changed, 80 insertions(+), 1091 deletions(-) delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt delete mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt index ac6485fc8a..7b8bbc3be3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.analytics.utils import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -36,7 +37,7 @@ internal class SendScreenAnalyticSender( } } SendUiStateType.Amount -> { - val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return + val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return val isFiatSelected = amountState.amountTextField.isFiatValue val selectedCurrency = if (!isFiatSelected) { SelectedCurrencyType.Token diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 8e552379b5..3a344425d3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,18 +1,18 @@ package com.tangem.features.send.impl.presentation.state import com.tangem.blockchain.common.TransactionData +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage -import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -33,20 +33,12 @@ internal class SendStateFactory( ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { - SendAmountFieldConverter( - clickIntents = clickIntents, - stateRouterProvider = stateRouterProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - ) - } private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendAmountStateConverter( + AmountStateConverter( + clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, userWalletProvider = userWalletProvider, - sendAmountFieldConverter = amountFieldConverter, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } @@ -79,12 +71,19 @@ internal class SendStateFactory( isBalanceHidden = false, cryptoCurrencyName = "", isSubtracted = false, + amountState = AmountState.Empty(false), + editAmountState = AmountState.Empty(false), ) fun getReadyState(): SendUiState { val state = currentStateProvider() + val amountState = if (state.amountState is AmountState.Empty) { + amountStateConverter.convert("") + } else { + state.amountState + } return state.copy( - amountState = state.amountState ?: amountStateConverter.convert(""), + amountState = amountState, recipientState = state.recipientState ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), @@ -95,8 +94,13 @@ internal class SendStateFactory( fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState { val state = currentStateProvider() + val amountState = if (state.amountState is AmountState.Empty) { + amountStateConverter.convert(amount) + } else { + state.amountState + } return state.copy( - amountState = state.amountState ?: amountStateConverter.convert(amount), + amountState = amountState, recipientState = state.recipientState ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)), feeState = state.feeState ?: feeStateConverter.convert(Unit), @@ -117,7 +121,7 @@ internal class SendStateFactory( fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() val balance = cryptoCurrencyStatusProvider().value.amount ?: return state - val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return state + val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return state val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return state val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index c718256d95..e9badc64d0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -3,17 +3,14 @@ package com.tangem.features.send.impl.presentation.state import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.tangem.blockchain.common.transaction.Fee -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.PersistentList import java.math.BigDecimal /** @@ -24,11 +21,11 @@ internal data class SendUiState( val clickIntents: SendClickIntents, val isEditingDisabled: Boolean, val cryptoCurrencyName: String, - val amountState: SendStates.AmountState? = null, + val amountState: AmountState, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, val sendState: SendStates.SendState? = null, - val editAmountState: SendStates.AmountState? = null, + val editAmountState: AmountState, val editRecipientState: SendStates.RecipientState? = null, val editFeeState: SendStates.FeeState? = null, val isBalanceHidden: Boolean, @@ -36,7 +33,7 @@ internal data class SendUiState( val event: StateEvent, ) { - fun getAmountState(isEditState: Boolean): SendStates.AmountState? { + fun getAmountState(isEditState: Boolean): AmountState { return if (isEditState) { editAmountState } else { @@ -62,7 +59,7 @@ internal data class SendUiState( fun copyWrapped( isEditState: Boolean, - amountState: SendStates.AmountState? = this.amountState, + amountState: AmountState = this.amountState, feeState: SendStates.FeeState? = this.feeState, recipientState: SendStates.RecipientState? = this.recipientState, sendState: SendStates.SendState? = this.sendState, @@ -90,21 +87,6 @@ internal sealed class SendStates { abstract val isPrimaryButtonEnabled: Boolean - /** Amount state */ - @Stable - data class AmountState( - override val type: SendUiStateType = SendUiStateType.Amount, - override val isPrimaryButtonEnabled: Boolean, - val walletName: String, - val walletBalance: TextReference, - val tokenIconState: TokenIconState, - val segmentedButtonConfig: PersistentList, - val selectedButton: Int, - val isSegmentedButtonsEnabled: Boolean, - val amountTextField: SendTextField.AmountField, - val appCurrencyCode: String, - ) : SendStates() - /** Recipient state */ @Stable data class RecipientState( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index d933d3276f..204918b99b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.amount +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -64,7 +65,7 @@ internal class AmountStateFactory( fun getOnAmountReduceByState(reduceAmountBy: BigDecimal, reduceAmountByDiff: BigDecimal) = amountReduceByConverter.convert( - SendAmountReduceByConverter.ReduceByData( + AmountReduceByTransformer.ReduceByData( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt deleted file mode 100644 index 3921429828..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.amount - -import androidx.compose.ui.text.input.ImeAction -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import java.math.BigDecimal -import java.math.RoundingMode - -internal fun String.getCryptoValue(fiatRate: BigDecimal?, isFiatValue: Boolean, decimals: Int): String { - return if (isFiatValue && fiatRate != null) { - parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN) - .parseBigDecimal(decimals) - } else { - this - } -} - -internal fun String.getFiatValue( - fiatRate: BigDecimal?, - isFiatValue: Boolean, - decimals: Int, -): Pair { - return if (fiatRate != null) { - val fiatValue = if (!isFiatValue) { - parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals) - } else { - this - } - val decimalFiatValue = fiatValue.parseToBigDecimal(decimals) - fiatValue to decimalFiatValue - } else { - "" to null - } -} - -internal fun String.checkExceedBalance( - cryptoCurrencyStatus: CryptoCurrencyStatus, - amountTextField: SendTextField.AmountField, -): Boolean { - val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO - val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) - val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) - return if (amountTextField.isFiatValue) { - fiatDecimal > currencyFiatAmount - } else { - cryptoDecimal > currencyCryptoAmount - } -} - -internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = - if (!isExceedBalance && !decimalCryptoValue.isZero()) { - ImeAction.Done - } else { - ImeAction.None - } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt index fa31b63ea0..c59bf9780c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt @@ -1,46 +1,27 @@ package com.tangem.features.send.impl.presentation.state.amount -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero internal class SendAmountCurrencyConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { + override fun convert(value: Boolean): SendUiState { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState - val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state - val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() - val isDoneActionEnabled = amountState.isPrimaryButtonEnabled - return if (amountTextField.isFiatValue == value && !isValidFiatRate) { - state - } else { - return state.copyWrapped( - isEditState = isEditState, - amountState = amountState.copy( - amountTextField = amountTextField.copy( - isFiatValue = value, - isValuePasted = true, - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ), - selectedButton = amountState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, - ), - ) - } + return state.copyWrapped( + isEditState = isEditState, + amountState = AmountCurrencyTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt index 093b1b6a8e..3766d7c9c9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount +import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider @@ -9,17 +11,15 @@ internal class SendAmountPastedTriggerDismissConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, ) : Converter { + override fun convert(value: Boolean): SendUiState { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState - val amountState = state.getAmountState(isEditState) ?: return state + val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state + return state.copyWrapped( isEditState = isEditState, - amountState = amountState.copy( - amountTextField = amountState.amountTextField.copy( - isValuePasted = false, - ), - ), + amountState = AmountPastedTriggerDismissTransformer().transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt index e937600d51..0468cd8c82 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt @@ -1,67 +1,29 @@ package com.tangem.features.send.impl.presentation.state.amount -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal internal class SendAmountReduceByConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, -) : Converter { - override fun convert(value: ReduceByData): SendUiState { +) : Converter { + + override fun convert(value: AmountReduceByTransformer.ReduceByData): SendUiState { val state = currentStateProvider() - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state - val decimalCryptoValue = amountValue.minus(value.reduceAmountByDiff) - val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) - val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = false, - decimals = fiatDecimals, - ) - - val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy( reduceAmountBy = value.reduceAmountBy, ), - amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, - amountTextField = amountTextField.copy( - value = cryptoValue, - fiatValue = fiatValue, - isError = isExceedBalance, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), - keyboardType = KeyboardType.Number, - ), - ), - ), + amountState = AmountReduceByTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), ) } - - internal data class ReduceByData( - val reduceAmountBy: BigDecimal, - val reduceAmountByDiff: BigDecimal, - ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt index 76fd7e2654..1bb5518063 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt @@ -1,15 +1,11 @@ package com.tangem.features.send.impl.presentation.state.amount -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero import java.math.BigDecimal internal class SendAmountReduceToConverter( @@ -17,41 +13,15 @@ internal class SendAmountReduceToConverter( private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { + override fun convert(value: BigDecimal): SendUiState { val state = currentStateProvider() - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - val cryptoValue = value.parseBigDecimal(cryptoDecimals) - val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = false, - decimals = fiatDecimals, - ) - - val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isZero() return state.copyWrapped( isEditState = isEditState, - amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, - amountTextField = amountTextField.copy( - value = cryptoValue, - fiatValue = fiatValue, - isError = isExceedBalance, - cryptoAmount = amountTextField.cryptoAmount.copy(value = value), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, value), - keyboardType = KeyboardType.Number, - ), - ), - ), + amountState = AmountReduceToTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt deleted file mode 100644 index eca219a09d..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.amount - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.core.ui.extensions.TextReference - -/** - * Segmented buttons config - * - * @param title button title - * @param iconState currency icon state - * @param iconUrl currency icon url - * @param isFiat is fiat currency - */ -@Immutable -internal data class SendAmountSegmentedButtonsConfig( - val title: TextReference, - val iconState: TokenIconState? = null, - val iconUrl: String? = null, - val isFiat: Boolean, -) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt deleted file mode 100644 index 6afdc1ada8..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.amount - -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount -import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import kotlinx.collections.immutable.persistentListOf - -internal class SendAmountStateConverter( - private val appCurrencyProvider: Provider, - private val userWalletProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, - private val iconStateConverter: CryptoCurrencyToIconStateConverter, - private val sendAmountFieldConverter: SendAmountFieldConverter, -) : Converter { - - override fun convert(value: String): SendStates.AmountState { - val userWallet = userWalletProvider() - val appCurrency = appCurrencyProvider() - val status = cryptoCurrencyStatusProvider() - val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol) - val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) - val noFeeRate = status.value.fiatRate.isNullOrZero() - - return SendStates.AmountState( - walletName = userWallet.name, - walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), - tokenIconState = iconStateConverter.convert(status), - amountTextField = sendAmountFieldConverter.convert(value), - isPrimaryButtonEnabled = false, - appCurrencyCode = appCurrency.code, - segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( - title = stringReference(status.currency.symbol), - iconState = iconStateConverter.convertCustom( - value = status, - forceGrayscale = noFeeRate, - showCustomTokenBadge = false, - ), - isFiat = false, - ), - SendAmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), - isSegmentedButtonsEnabled = !noFeeRate, - selectedButton = 0, - ) - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 3982fbe052..98001756d9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -6,6 +6,8 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId @@ -25,7 +27,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.fee.* -import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos @@ -62,7 +63,7 @@ internal class SendNotificationFactory( val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val sendState = state.sendState ?: return@map persistentListOf() val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf() - val amountState = state.getAmountState(isEditState) ?: return@map persistentListOf() + val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf() val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO @@ -239,7 +240,7 @@ internal class SendNotificationFactory( private fun MutableList.addFeeCoverageNotification( isFeeCoverage: Boolean, - amountField: SendTextField.AmountField, + amountField: AmountFieldModel, sendingValue: BigDecimal, ) { if (isFeeCoverage) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 112b0f1cb7..4b5dda431d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,89 +1,27 @@ package com.tangem.features.send.impl.presentation.state.fields -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter -import com.tangem.features.send.impl.presentation.state.amount.checkExceedBalance -import com.tangem.features.send.impl.presentation.state.amount.getCryptoValue -import com.tangem.features.send.impl.presentation.state.amount.getFiatValue -import com.tangem.features.send.impl.presentation.state.amount.getKeyboardAction import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal internal class SendAmountFieldChangeConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { + override fun convert(value: String): SendUiState { val state = currentStateProvider() - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - if (value.isEmpty()) return state.emptyState() - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - - val trimmedValue = value.trim() - val cryptoValue = trimmedValue.getCryptoValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = amountTextField.isFiatValue, - decimals = cryptoDecimals, - ) - val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) - val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = amountTextField.isFiatValue, - decimals = fiatDecimals, - ) - - val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, - amountTextField = amountTextField.copy( - value = cryptoValue, - fiatValue = fiatValue, - isError = isExceedBalance, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), - keyboardType = KeyboardType.Number, - ), - ), - ), - ) - } - - private fun SendUiState.emptyState(): SendUiState { - val isEditState = stateRouterProvider().isEditState - val amountState = getAmountState(isEditState) ?: return this - val amountTextField = amountState.amountTextField - return copyWrapped( - isEditState = isEditState, - amountState = amountState.copy( - isPrimaryButtonEnabled = false, - amountTextField = amountTextField.copy( - value = "", - fiatValue = "", - cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), - fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), - isError = false, - ), - ), + amountState = AmountFieldChangeTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt deleted file mode 100644 index 8bf30c0b57..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.fields - -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.blockchain.extensions.toBigDecimalOrDefault -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.Amount -import com.tangem.domain.tokens.model.AmountType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.StateRouter -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal - -private const val FIAT_DECIMALS = 2 - -internal class SendAmountFieldConverter( - private val clickIntents: SendClickIntents, - private val stateRouterProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, - private val appCurrencyProvider: Provider, -) : Converter { - - override fun convert(value: String): SendTextField.AmountField { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val cryptoDecimal = value.toBigDecimalOrDefault() - val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) - val fiatRate = cryptoCurrencyStatus.value.fiatRate - val (fiatValue, fiatDecimal) = when { - fiatRate.isNullOrZero() -> "" to null - value.isEmpty() -> "" to BigDecimal.ZERO - else -> { - val fiatDecimal = fiatRate?.multiply(cryptoDecimal) - val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() - fiatValue to fiatDecimal - } - } - val isDoneActionEnabled = !cryptoDecimal.isZero() - return SendTextField.AmountField( - value = value, - fiatValue = fiatValue, - onValueChange = clickIntents::onAmountValueChange, - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - keyboardActions = KeyboardActions( - onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) }, - ), - isFiatValue = false, - cryptoAmount = cryptoAmount, - fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), - isError = false, - error = TextReference.Res(R.string.send_validation_amount_exceeds_balance), - isFiatUnavailable = fiatRate == null, - isValuePasted = false, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - ) - } - - private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( - currencySymbol = appCurrency.symbol, - value = fiatValue, - decimals = FIAT_DECIMALS, - type = AmountType.FiatType(appCurrency.code), - ) -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 11cfa98faa..5577388040 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -1,16 +1,12 @@ package com.tangem.features.send.impl.presentation.state.fields -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero -import java.math.RoundingMode internal class SendAmountFieldMaxAmountConverter( private val stateRouterProvider: Provider, @@ -23,36 +19,14 @@ internal class SendAmountFieldMaxAmountConverter( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals val decimalCryptoValue = cryptoCurrencyStatus.value.amount - val decimalFiatValue = cryptoCurrencyStatus.value.fiatAmount - if (decimalCryptoValue.isNullOrZero()) return state - val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() - val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() - val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = amountState.copy( - isPrimaryButtonEnabled = true, - amountTextField = amountTextField.copy( - isValuePasted = true, - value = cryptoValue, - fiatValue = fiatValue, - isError = false, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ), - ), + amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatusProvider()).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 4f5ce27038..e289c7968a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.Amount @Immutable internal sealed class SendTextField { @@ -18,22 +17,6 @@ internal sealed class SendTextField { /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions - data class AmountField( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - val keyboardActions: KeyboardActions, - val cryptoAmount: Amount, - val fiatAmount: Amount, - val isFiatValue: Boolean, - val fiatValue: String, - val isFiatUnavailable: Boolean, - val isValuePasted: Boolean, - val onValuePastedTriggerDismiss: () -> Unit, - val isError: Boolean, - val error: TextReference, - ) : SendTextField() - data class RecipientAddress( override val value: String, override val onValueChange: (String) -> Unit, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt deleted file mode 100644 index 7b2017d6e7..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.features.send.impl.presentation.state.previewdata - -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.tokens.model.Amount -import com.tangem.domain.tokens.model.AmountType -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig -import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal - -internal object AmountStatePreviewData { - - val amountState = SendStates.AmountState( - type = SendUiStateType.Amount, - isPrimaryButtonEnabled = false, - walletName = "Family Wallet", - walletBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), - tokenIconState = TokenIconState.Loading, - segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( - title = stringReference("USDT"), - iconState = TokenIconState.Locked, - isFiat = false, - ), - SendAmountSegmentedButtonsConfig( - title = stringReference("USD"), - isFiat = true, - ), - ), - appCurrencyCode = "usd", - amountTextField = SendTextField.AmountField( - value = "", - onValueChange = {}, - keyboardOptions = KeyboardOptions.Default, - keyboardActions = KeyboardActions.Default, - cryptoAmount = Amount( - currencySymbol = "USDT", - value = BigDecimal.ZERO, - decimals = 18, - type = AmountType.CoinType, - ), - fiatAmount = Amount( - currencySymbol = "$", - value = BigDecimal.ZERO, - decimals = 2, - type = AmountType.CoinType, - ), - isFiatValue = false, - fiatValue = "123.123", - isFiatUnavailable = false, - isError = false, - error = TextReference.EMPTY, - isValuePasted = false, - onValuePastedTriggerDismiss = {}, - ), - isSegmentedButtonsEnabled = true, - selectedButton = 0, - ) - - val amountWithValueState = amountState.copy( - amountTextField = amountState.amountTextField.copy( - value = "100.00", - cryptoAmount = amountState.amountTextField.cryptoAmount.copy( - value = BigDecimal("100.00"), - ), - fiatValue = "99.98", - fiatAmount = amountState.amountTextField.fiatAmount.copy( - value = BigDecimal("99.98"), - ), - ), - ) - - val amountWithValueFiatState = amountWithValueState.copy( - amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false), - ) - - val amountErrorState = amountWithValueState.copy( - amountTextField = amountWithValueState.amountTextField.copy( - isError = true, - error = stringReference("Insufficient funds for transfer"), - ), - ) -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index 2eeda94fd5..8eab2cbac3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -29,6 +29,7 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onAmountValueChange(value: String) {} override fun onCurrencyChangeClick(isFiat: Boolean) {} + override fun onAmountNext() {} override fun onMaxValueClick() {} diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt index 2366b09686..dd64a24aed 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.previewdata +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.event.consumedEvent import com.tangem.features.send.impl.presentation.state.SendUiState diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 157a21ad02..e47d869fb6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.getCryptoReference import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.R @@ -165,7 +166,7 @@ private fun SendingText( exit = fadeOut(tween(durationMillis = 300)), label = "Animate show sending state text", ) { - val amountState = uiState.getAmountState(isEditState) + val amountState = uiState.getAmountState(isEditState) as? AmountState.Data val feeState = uiState.getFeeState(isEditState) val fiatRate = feeState?.rate val fiatAmount = amountState?.amountTextField?.fiatAmount diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index b05b60afe0..992da60b9f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -16,6 +16,8 @@ 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.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference @@ -28,7 +30,6 @@ import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData import com.tangem.features.send.impl.presentation.state.previewdata.SendStatesPreviewData -import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent import com.tangem.features.send.impl.presentation.ui.send.SendContent @@ -87,7 +88,7 @@ private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) -> resourceReference(R.string.common_fee_selector_title) to null SendUiStateType.Send -> if (uiState.sendState?.isSuccess == false) { resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) to - uiState.amountState?.walletName + (uiState.amountState as? AmountState.Data)?.walletName } else { null to null } @@ -160,13 +161,13 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS isTransitionAnimationRunning = transition.targetState != transition.currentState when (state.type) { - SendUiStateType.Amount -> SendAmountContent( + SendUiStateType.Amount -> AmountScreenContent( amountState = uiState.amountState, isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) - SendUiStateType.EditAmount -> SendAmountContent( - amountState = uiState.editAmountState, + SendUiStateType.EditAmount -> AmountScreenContent( + amountState = uiState.editAmountState!!, isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt deleted file mode 100644 index 19325703a5..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt +++ /dev/null @@ -1,123 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.amount - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -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.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.fiaticon.FiatIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import kotlinx.collections.immutable.PersistentList - -private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" - -internal fun LazyListScope.buttons( - segmentedButtonConfig: PersistentList, - clickIntents: SendClickIntents, - isSegmentedButtonsEnabled: Boolean, - selectedButton: Int, -) { - item( - key = AMOUNT_BUTTONS_KEY, - ) { - val hapticFeedback = LocalHapticFeedback.current - Row( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), - ) { - if (segmentedButtonConfig.isNotEmpty()) { - SegmentedButtons( - modifier = Modifier - .weight(1f) - .height(TangemTheme.dimens.size40), - config = segmentedButtonConfig, - showIndication = false, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onCurrencyChangeClick(it.isFiat) - }, - initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), - isEnabled = isSegmentedButtonsEnabled, - ) { - SendAmountCurrencyButton( - button = it, - isSegmentedButtonsEnabled = isSegmentedButtonsEnabled, - ) - } - } else { - SpacerWMax() - } - Text( - text = stringResource(R.string.send_max_amount), - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .height(TangemTheme.dimens.size40) - .clip(shape = RoundedCornerShape(TangemTheme.dimens.radius26)) - .background(TangemTheme.colors.button.secondary) - .clickable { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onMaxValueClick() - } - .padding( - vertical = TangemTheme.dimens.spacing10, - horizontal = TangemTheme.dimens.spacing34, - ), - ) - } - } -} - -@Composable -private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { - Row( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing10, - ), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - val iconModifier = Modifier.size(TangemTheme.dimens.size18) - .padding(horizontal = TangemTheme.dimens.spacing1) - if (button.isFiat) { - FiatIcon( - url = button.iconUrl, - size = TangemTheme.dimens.size18, - isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier, - ) - } else if (button.iconState != null) { - TokenIcon( - state = button.iconState, - shouldDisplayNetwork = false, - modifier = iconModifier, - ) - } - Text( - text = button.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - ), - ) - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt deleted file mode 100644 index 8f7a69c8c1..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.amount - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeightIn -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection -import com.tangem.core.ui.components.fields.AmountTextField -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.rememberDecimalFormat -import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import kotlinx.coroutines.delay - -@Composable -internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) { - val decimalFormat = rememberDecimalFormat() - val isFiatValue = sendField.isFiatValue - val currencyCode = if (isFiatValue) appCurrencyCode else null - val (primaryAmount, primaryValue) = if (isFiatValue) { - sendField.fiatAmount to sendField.fiatValue - } else { - sendField.cryptoAmount to sendField.value - } - val requester = remember { FocusRequester() } - - AmountTextField( - value = primaryValue, - decimals = primaryAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = primaryAmount.decimals, - symbol = primaryAmount.currencySymbol, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - ), - onValueChange = sendField.onValueChange, - keyboardOptions = sendField.keyboardOptions, - keyboardActions = sendField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isAutoResize = true, - isValuePasted = sendField.isValuePasted, - onValuePastedTriggerDismiss = sendField.onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing24, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32), - ) - - LaunchedEffect(key1 = Unit) { - delay(timeMillis = 200) - requester.requestFocus() - } - - AmountSecondary(sendField, appCurrencyCode) -} - -@Composable -private fun AmountSecondary(sendField: SendTextField.AmountField, appCurrencyCode: String) { - val secondaryAmount = if (sendField.isFiatValue) sendField.cryptoAmount else sendField.fiatAmount - Box( - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - ) { - val text = if (sendField.isFiatValue) { - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = secondaryAmount.value, - cryptoCurrency = secondaryAmount.currencySymbol, - decimals = secondaryAmount.decimals, - ) - } else { - BigDecimalFormatter.formatFiatAmount( - fiatAmount = secondaryAmount.value, - fiatCurrencySymbol = secondaryAmount.currencySymbol, - fiatCurrencyCode = appCurrencyCode, - ) - } - Text( - text = text, - style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .align(BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing32), - ) - AmountFieldError( - isError = sendField.isError, - error = sendField.error, - modifier = Modifier - .align(BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } -} - -@Composable -private fun AmountFieldError(isError: Boolean, error: TextReference, modifier: Modifier = Modifier) { - AnimatedVisibility( - visible = isError, - enter = fadeIn(), - exit = fadeOut(), - modifier = modifier, - ) { - Text( - text = error.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.warning, - textAlign = TextAlign.Center, - ) - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt deleted file mode 100644 index fe4e6cc285..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.amount - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.utils.Strings.STARS - -private const val AMOUNT_FIELD_KEY = "amountFieldKey" - -internal fun LazyListScope.amountField( - amountState: SendStates.AmountState, - isBalanceHiding: Boolean, - modifier: Modifier = Modifier, -) { - item(key = AMOUNT_FIELD_KEY) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action), - ) { - Text( - text = amountState.walletName, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14), - ) - - val balance = if (isBalanceHiding) STARS else amountState.walletBalance.resolveReference() - AnimatedContent( - targetState = balance, - label = "Hide Balance Animation", - ) { - Text( - text = it, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2), - ) - } - TokenIcon( - state = amountState.tokenIconState, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32), - ) - AmountField( - sendField = amountState.amountTextField, - appCurrencyCode = amountState.appCurrencyCode, - ) - } - } -} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt deleted file mode 100644 index 6181fc2174..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.amount - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.runtime.Composable -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.PreviewParameterProvider -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData -import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents - -@Composable -internal fun SendAmountContent( - amountState: SendStates.AmountState?, - isBalanceHiding: Boolean, - clickIntents: SendClickIntents, -) { - if (amountState == null) return - // Do not put fillMaxSize() in here - LazyColumn( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - amountField(amountState = amountState, isBalanceHiding = isBalanceHiding) - buttons( - segmentedButtonConfig = amountState.segmentedButtonConfig, - clickIntents = clickIntents, - isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, - selectedButton = amountState.selectedButton, - ) - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SendAmountContentPreview( - @PreviewParameter(SendAmountContentPreviewProvider::class) amountState: SendStates.AmountState, -) { - TangemThemePreview { - SendAmountContent( - amountState = amountState, - isBalanceHiding = false, - clickIntents = SendClickIntentsStub, - ) - } -} - -private class SendAmountContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - AmountStatePreviewData.amountState, - ) -} -// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt deleted file mode 100644 index a4912c792e..0000000000 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.features.send.impl.presentation.ui.send - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -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.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.components.ResizableText -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData - -@Composable -internal fun AmountBlock( - amountState: SendStates.AmountState, - isClickDisabled: Boolean, - isEditingDisabled: Boolean, - onClick: () -> Unit, -) { - val amount = amountState.amountTextField - - val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount.fiatAmount.value, - fiatCurrencySymbol = amount.fiatAmount.currencySymbol, - fiatCurrencyCode = amountState.appCurrencyCode, - ) - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - - val (firstAmount, secondAmount) = if (amount.isFiatValue) { - fiatAmount to cryptoAmount - } else { - cryptoAmount to fiatAmount - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) - .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) - .padding(TangemTheme.dimens.spacing16), - ) { - TokenIcon(state = amountState.tokenIconState) - ResizableText( - text = firstAmount, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing24), - ) - Text( - text = secondAmount, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing8), - ) - } -} - -// region Preview -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState) { - TangemThemePreview { - AmountBlock( - amountState = value, - isClickDisabled = false, - isEditingDisabled = false, - onClick = {}, - ) - } -} - -private class AmountBlockPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - AmountStatePreviewData.amountState, - ) -} -// endregion \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 0f6304ae81..2576d7b37f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -24,6 +24,7 @@ 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.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -53,7 +54,7 @@ internal fun SendContent(uiState: SendUiState) { } private fun LazyListScope.blocks(uiState: SendUiState) { - val amountState = uiState.amountState ?: return + val amountState = uiState.amountState val recipientState = uiState.recipientState ?: return val feeState = uiState.feeState ?: return val sendState = uiState.sendState ?: return diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 457de01af1..b7bc64d493 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -8,7 +9,7 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType import java.math.BigDecimal @Suppress("TooManyFunctions") -internal interface SendClickIntents { +internal interface SendClickIntents : AmountScreenClickIntents { fun popBackStack() @@ -26,16 +27,6 @@ internal interface SendClickIntents { fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) - // region Amount - fun onAmountValueChange(value: String) - - fun onCurrencyChangeClick(isFiat: Boolean) - - fun onMaxValueClick() - - fun onAmountPasteTriggerDismiss() - // endregion - // region Recipient fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 7115f7b525..5a8f4ff142 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -104,11 +105,11 @@ internal class SendViewModel @Inject constructor( ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Send.USER_WALLET_ID_KEY) - ?.let { it.unbundle(UserWalletId.serializer()) } + ?.unbundle(UserWalletId.serializer()) ?: error("This screen can't open without `UserWalletId`") private val cryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Send.CRYPTO_CURRENCY_KEY) - ?.let { it.unbundle(CryptoCurrency.serializer()) } + ?.unbundle(CryptoCurrency.serializer()) ?: error("This screen can't open without `CryptoCurrency`") private val transactionId: String? = savedStateHandle[AppRoute.Send.TRANSACTION_ID_KEY] @@ -493,6 +494,8 @@ internal class SendViewModel @Inject constructor( stateRouter.onNextClick() } + override fun onAmountNext() = onNextClick(stateRouter.isEditState) + override fun onPrevClick() { cancelFeeRequest() stateRouter.onPrevClick() @@ -709,7 +712,7 @@ internal class SendViewModel @Inject constructor( private suspend fun callFeeUseCase(): Either? { val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation - val amountState = uiState.getAmountState(isFromConfirmation) ?: return null + val amountState = uiState.getAmountState(isFromConfirmation) as? AmountState.Data ?: return null val recipientState = uiState.getRecipientState(isFromConfirmation) ?: return null val amount = amountState.amountTextField.cryptoAmount.value ?: return null @@ -800,7 +803,7 @@ internal class SendViewModel @Inject constructor( val feeState = uiState.feeState ?: return val fee = feeState.fee ?: return val memo = uiState.recipientState?.memoTextField?.value - val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return + val amountValue = (uiState.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: return val feeValue = fee.amount.value ?: return val receivingAmount = checkAndCalculateSubtractedAmount( From 7619e9706b229b96d64dc7161969fed7f01a8541 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:18:35 +0500 Subject: [PATCH 063/223] Updated on 2026-08-14 --- .../common/settings/IntentSettingsManager.kt | 17 +++--- .../java/com/tangem/tap/di/UtilsModule.kt | 2 +- .../core/ui/utils/RequestPushPermission.kt | 55 +++++++++++++++++ .../api/navigation/PushNotificationsRouter.kt | 8 +++ .../api/utils/PermissionUtils.kt | 10 ++++ .../push-notifications/impl/build.gradle.kts | 3 + .../impl/PushNotificationsFragment.kt | 39 ++++++++++++ .../impl/di/PushNotificationsModule.kt | 24 ++++++++ .../DefaultPushNotificationsRouter.kt | 18 ++++++ .../InnerPushNotificationsRouter.kt | 8 +++ .../ui/PushNotificationsScreen.kt | 59 +++++++++++++++++++ .../viewmodel/PushNotificationViewModel.kt | 46 +++++++++++++++ .../PushNotificationsClickIntents.kt | 11 ++++ 13 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt create mode 100644 features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt create mode 100644 features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt create mode 100644 features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt diff --git a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt index 7dbed67b24..09305fd59e 100644 --- a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt @@ -1,20 +1,17 @@ package com.tangem.tap.common.settings +import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings import com.tangem.core.navigation.settings.SettingsManager -import com.tangem.tap.foregroundActivityObserver -import com.tangem.tap.withForegroundActivity -internal class IntentSettingsManager : SettingsManager { +internal class IntentSettingsManager(val context: Context) : SettingsManager { override fun openSettings() { - foregroundActivityObserver.withForegroundActivity { activity -> - val openSettingsIntent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", activity.packageName, null), - ) - activity.startActivity(openSettingsIntent) - } + val openSettingsIntent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ) + context.startActivity(openSettingsIntent) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index e5c898b899..2db04791cf 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -40,5 +40,5 @@ internal object UtilsModule { @Provides @Singleton - fun provideSettingsManager(): SettingsManager = IntentSettingsManager() + fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt new file mode 100644 index 0000000000..134039ed07 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt @@ -0,0 +1,55 @@ +package com.tangem.core.ui.utils + +import android.Manifest +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale + +/** + * Returns push permission requester. + * Handles granting permission from app settings. + */ +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun requestPushPermission( + isFirstTimeAsking: Boolean, + isClicked: MutableState, + onAllow: () -> Unit, + onDeny: () -> Unit, + onOpenSettings: () -> Unit, +): () -> Unit { + val permissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val permission = Manifest.permission.POST_NOTIFICATIONS + val tempPermissionState = rememberPermissionState(permission = permission) + rememberPermissionState(permission = permission) { + when { + it -> onAllow() + !tempPermissionState.status.shouldShowRationale && !isFirstTimeAsking -> onOpenSettings() + else -> onDeny() + } + } + } else { + null + } + + // Check if user granted permission and close bottom sheet + LaunchedEffect(key1 = permissionState?.status, isClicked) { + if (!isClicked.value) return@LaunchedEffect + if (permissionState?.status?.isGranted == true) { + onAllow() + } else { + onDeny() + } + } + + return if (permissionState == null) { + onOpenSettings + } else { + permissionState::launchPermissionRequest + } +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt new file mode 100644 index 0000000000..6881437671 --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.pushnotifications.api.navigation + +import androidx.fragment.app.Fragment + +interface PushNotificationsRouter { + + fun entryFragment(): Fragment +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt new file mode 100644 index 0000000000..6092954f2a --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt @@ -0,0 +1,10 @@ +package com.tangem.features.pushnotifications.api.utils + +import android.Manifest +import android.os.Build + +val PUSH_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.POST_NOTIFICATIONS +} else { + "android.permission.POST_NOTIFICATIONS" +} \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index 4524106ece..35c5535827 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -29,6 +29,9 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) + /** Common modules */ + implementation(projects.common.routing) + /** Domain module */ implementation(projects.domain.settings) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt new file mode 100644 index 0000000000..0b8fedc562 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt @@ -0,0 +1,39 @@ +package com.tangem.features.pushnotifications.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen +import com.tangem.features.pushnotifications.impl.presentation.viewmodel.PushNotificationViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class PushNotificationsFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + private val viewModel by viewModels() + + @Composable + override fun ScreenContent(modifier: Modifier) { + BackHandler(onBack = requireActivity()::finish) + NavigationBar3ButtonsScrim() + PushNotificationsScreen( + onShowAllow = viewModel::onAllowPermission, + onAllow = viewModel::onAllowedPermission, + onLater = viewModel::onAskLater, + onOpenSettings = viewModel::openSettings, + ) + } + + companion object { + /** Create push notifications fragment instance */ + fun create(): PushNotificationsFragment = PushNotificationsFragment() + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt new file mode 100644 index 0000000000..b568880483 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.pushnotifications.impl.di + +import com.tangem.common.routing.AppRouter +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter +import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [PushNotificationsRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +object PushNotificationsModule { + + @Provides + @ActivityScoped + fun provideDisclaimerRouter(appRouter: AppRouter): PushNotificationsRouter { + return DefaultPushNotificationsRouter(appRouter) + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt new file mode 100644 index 0000000000..b2536cfe47 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt @@ -0,0 +1,18 @@ +package com.tangem.features.pushnotifications.impl.navigation + +import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.features.pushnotifications.impl.PushNotificationsFragment +import javax.inject.Inject + +internal class DefaultPushNotificationsRouter @Inject constructor( + private val appRouter: AppRouter, +) : InnerPushNotificationsRouter { + + override fun entryFragment(): Fragment = PushNotificationsFragment.create() + + override fun openHome() { + appRouter.push(AppRoute.Home) + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt new file mode 100644 index 0000000000..20050eca1e --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.pushnotifications.impl.navigation + +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter + +interface InnerPushNotificationsRouter : PushNotificationsRouter { + + fun openHome() +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt new file mode 100644 index 0000000000..02c7036aad --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -0,0 +1,59 @@ +package com.tangem.features.pushnotifications.impl.presentation.ui + +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.showcase.Showcase +import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.requestPushPermission +import com.tangem.feature.pushnotifications.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PushNotificationsScreen( + onShowAllow: () -> Unit, + onAllow: () -> Unit, + onLater: () -> Unit, + onOpenSettings: () -> Unit, +) { + val isClicked = remember { mutableStateOf(false) } + val requestPushPermission = requestPushPermission( + isFirstTimeAsking = true, + isClicked = isClicked, + onAllow = onAllow, + onDeny = onLater, + onOpenSettings = onOpenSettings, + ) + + Showcase( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + R.drawable.ic_rocket_launch_24, + resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + R.drawable.ic_storefront_24, + resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + primaryButton = ShowcaseButtonModel( + buttonText = resourceReference(R.string.common_allow), + onClick = { + isClicked.value = true + onShowAllow() + requestPushPermission() + }, + ), + secondaryButton = ShowcaseButtonModel( + buttonText = resourceReference(R.string.common_later), + onClick = onLater, + ), + modifier = Modifier.systemBarsPadding(), + ) +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt new file mode 100644 index 0000000000..6653043296 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -0,0 +1,46 @@ +package com.tangem.features.pushnotifications.impl.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.domain.settings.DelayPermissionRequestUseCase +import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class PushNotificationViewModel @Inject constructor( + private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, + private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, + private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val router: DefaultPushNotificationsRouter, + private val settingsManager: SettingsManager, +) : ViewModel(), PushNotificationsClickIntents { + + override fun onAskLater() { + viewModelScope.launch { + delayPermissionRequestUseCase(PUSH_PERMISSION) + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + router.openHome() + } + + override fun onAllowPermission() { + viewModelScope.launch { + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + } + + override fun onAllowedPermission() { + viewModelScope.launch { + neverRequestPermissionUseCase(PUSH_PERMISSION) + } + router.openHome() + } + + override fun openSettings() = settingsManager.openSettings() +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt new file mode 100644 index 0000000000..f101e15e7a --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt @@ -0,0 +1,11 @@ +package com.tangem.features.pushnotifications.impl.presentation.viewmodel + +internal interface PushNotificationsClickIntents { + fun onAskLater() + + fun onAllowPermission() + + fun onAllowedPermission() + + fun openSettings() +} \ No newline at end of file From 6369a3f33af81e0a3df27f9a9fc9f485f3af59e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Jun 2024 10:52:08 +0500 Subject: [PATCH 064/223] Updated on 2026-08-14 --- .../atoms/radiobutton/TangemRadioButton.kt | 78 +++++++++++ .../components/inputrow/InputRowBestRate.kt | 31 +---- .../components/inputrow/InputRowImageBase.kt | 54 ++++++++ .../inputrow/InputRowImageChevron.kt | 46 ++++++ .../inputrow/InputRowImageSelector.kt | 131 ++++++++++++++++++ .../inputrow/inner/InputRowAsyncImage.kt | 41 ++++++ .../core/ui/extensions/TextReference.kt | 23 ++- .../com/tangem/core/ui/res/TangemDimens.kt | 1 + .../main/res/drawable/ic_check_circle_24.xml | 5 + .../state/stub/StakingValidatorStateStub.kt | 37 +++++ .../ui/StakingValidatorListContent.kt | 127 +++++++++++++++++ 11 files changed, 544 insertions(+), 30 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt create mode 100644 core/ui/src/main/res/drawable/ic_check_circle_24.xml create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt new file mode 100644 index 0000000000..20d61ebe87 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt @@ -0,0 +1,78 @@ +package com.tangem.core.ui.components.atoms.radiobutton + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [Radio button](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=101-119&t=FH84ljLBk1vmGAei-4) + * + * @param isSelected Whether the radio button is selected + * @param onClick Called when the user clicks the button + * @param modifier Modifier to be applied to the button + */ +@Composable +fun TangemRadioButton(isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16), + onClick = onClick, + ), + ) { + val color = TangemTheme.colors.stroke.secondary + val radius = with(LocalDensity.current) { TangemTheme.dimens.size9.toPx() } + val width = with(LocalDensity.current) { TangemTheme.dimens.size2.toPx() } + Canvas( + modifier = Modifier + .size(TangemTheme.dimens.size24) + .padding(TangemTheme.dimens.spacing2), + ) { + drawCircle( + color = color, + radius = radius, + style = Stroke(width), + ) + } + AnimatedVisibility( + visible = isSelected, + label = "Radio button animation", + modifier = modifier + .size(TangemTheme.dimens.size24), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_circle_24), + contentDescription = null, + tint = TangemTheme.colors.control.checked, + ) + } + } +} + +// region Preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemRadioButton_Preview() { + var isSelected by remember { mutableStateOf(false) } + TangemThemePreview { + TangemRadioButton(isSelected, onClick = { isSelected = !isSelected }) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index 3f711de362..0a17c85b13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon @@ -14,18 +13,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource 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.PreviewParameterProvider -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview @@ -63,7 +59,7 @@ fun InputRowBestRate( modifier = Modifier .padding(TangemTheme.dimens.spacing12), ) { - InnerIcon(imageUrl = imageUrl) + InputRowAsyncImage(imageUrl = imageUrl, modifier = Modifier.size(TangemTheme.dimens.spacing40)) Column( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12), @@ -131,29 +127,6 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: } } -@Composable -private fun InnerIcon(imageUrl: String) { - SubcomposeAsyncImage( - modifier = Modifier.size(TangemTheme.dimens.size40), - model = ImageRequest.Builder(context = LocalContext.current) - .data(imageUrl) - .crossfade(enable = true) - .allowHardware(enable = false) - .build(), - loading = { LoadingIcon() }, - error = { - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.background.tertiary, - shape = CircleShape, - ), - ) - }, - contentDescription = null, - ) -} - //region preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt new file mode 100644 index 0000000000..51f7b29ac6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -0,0 +1,54 @@ +package com.tangem.core.ui.components.inputrow + +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.graphics.Color +import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun InputRowImageBase( + subtitle: TextReference, + caption: TextReference, + imageUrl: String, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, + extraContent: @Composable RowScope.() -> Unit = {}, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + InputRowAsyncImage( + imageUrl = imageUrl, + modifier = Modifier + .size(TangemTheme.dimens.spacing36) + .padding(vertical = TangemTheme.dimens.size1), + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = TangemTheme.dimens.spacing12), + ) { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = subtitleColor, + ) + Text( + text = caption.resolveAnnotatedReference(), + style = TangemTheme.typography.caption2, + color = captionColor, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), + ) + } + extraContent() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt new file mode 100644 index 0000000000..9c8fcff521 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt @@ -0,0 +1,46 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.material.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2100-842&t=hoBXmDX8NeLrp4p6-4) + * + * @param subtitle subtitle text + * @param caption caption text + * @param imageUrl icon to load + * @param modifier modifier + * @param subtitleColor subtitle text color + * @param captionColor caption text color + */ +@Composable +fun InputRowImageChevron( + subtitle: TextReference, + caption: TextReference, + imageUrl: String, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, +) { + InputRowImageBase( + subtitle = subtitle, + caption = caption, + imageUrl = imageUrl, + modifier = modifier, + subtitleColor = subtitleColor, + captionColor = captionColor, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt new file mode 100644 index 0000000000..9e672eb56b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -0,0 +1,131 @@ +package com.tangem.core.ui.components.inputrow + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.annotatedReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal + +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2772-905&t=FH84ljLBk1vmGAei-4) + * + * @param subtitle subtitle text + * @param caption caption text + * @param imageUrl icon to load + * @param onSelect callback when selected + * @param modifier modifier + * @param subtitleColor subtitle text color + * @param captionColor caption text color + * @param isSelected true if selected + */ +@Composable +fun InputRowImageSelector( + subtitle: TextReference, + caption: TextReference, + imageUrl: String, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, + isSelected: Boolean = false, +) { + InputRowImageBase( + subtitle = subtitle, + caption = caption, + imageUrl = imageUrl, + subtitleColor = subtitleColor, + captionColor = captionColor, + modifier = modifier + .clickable( + onClick = onSelect, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + ) + .padding(TangemTheme.dimens.spacing12), + ) { + TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect) + } +} + +//region preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageSelectorPreview( + @PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData, +) { + TangemThemePreview { + InputRowImageSelector( + modifier = Modifier.background(TangemTheme.colors.background.action), + subtitle = data.subtitle, + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true), + ) + } + }, + ), + ), + imageUrl = "", + isSelected = false, + onSelect = {}, + ) + } +} + +private data class InputRowImageSelectorPreviewData( + val subtitle: TextReference, + val caption: TextReference, + val showDivider: Boolean, + val actionIconRes: Int?, + val isSelected: Boolean, +) + +private class InputRowImageSelectorPreviewDataProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowImageSelectorPreviewData( + subtitle = TextReference.Str("subtitle"), + caption = TextReference.Str("caption"), + actionIconRes = null, + showDivider = false, + isSelected = false, + ), + InputRowImageSelectorPreviewData( + subtitle = TextReference.Str("subtitle"), + caption = TextReference.Str("caption"), + actionIconRes = R.drawable.ic_chevron_right_24, + showDivider = true, + isSelected = true, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt new file mode 100644 index 0000000000..90bf7e86b8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.components.inputrow.inner + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon +import com.tangem.core.ui.res.TangemTheme + +/** + * Loads image by url for icon in the input row + * + * @param imageUrl url of the image + * @param modifier modifier + */ +@Composable +internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier) { + SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { LoadingIcon() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 13373f947b..386f69b66b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -48,6 +48,13 @@ sealed interface TextReference { */ data class Str(val value: String) : TextReference + /** + * Annotated text + * + * @property value annotated string + */ + data class Annotated(val value: AnnotatedString) : TextReference + /** * Combined reference. It concatenates all [refs]. * @@ -83,6 +90,16 @@ fun stringReference(value: String): TextReference { return TextReference.Str(value) } +/** + * Creates a [TextReference] using an annotated string value. + * + * @param value The annotated string value. + * @return A [TextReference] representing the provided annotated string value. + */ +fun annotatedReference(value: AnnotatedString): TextReference { + return TextReference.Annotated(value) +} + /** * Creates a [TextReference] using a plural string resource ID with count and optional format arguments. * @@ -133,6 +150,7 @@ fun TextReference.resolveReference(): String { } is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value + is TextReference.Annotated -> value.text is TextReference.Combined -> { buildString { refs.forEach { @@ -155,6 +173,7 @@ fun TextReference.resolveReference(resources: Resources): String { } is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value + is TextReference.Annotated -> value.text is TextReference.Combined -> { buildString { refs.forEach { @@ -180,9 +199,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString { pluralStringResource(id, count, *formatArgs.toTypedArray()), ) is TextReference.Str -> formatAnnotated(value) + is TextReference.Annotated -> value is TextReference.Combined -> buildAnnotatedString { refs.forEach { - append(formatAnnotated(it.resolveReference())) + append(it.resolveAnnotatedReference()) } } } @@ -195,6 +215,7 @@ operator fun TextReference.plus(ref: TextReference): TextReference { is TextReference.PluralRes, is TextReference.Res, is TextReference.Str, + is TextReference.Annotated, -> TextReference.Combined(refs = wrappedList(this, ref)) } } 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 a2649901ad..cc51084e66 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 @@ -47,6 +47,7 @@ data class TangemDimens internal constructor( val size5: Dp = 5.dp, val size7: Dp = 7.dp, val size8: Dp = 8.dp, + val size9: Dp = 9.dp, val size10: Dp = 10.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, diff --git a/core/ui/src/main/res/drawable/ic_check_circle_24.xml b/core/ui/src/main/res/drawable/ic_check_circle_24.xml new file mode 100644 index 0000000000..21b193accf --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_check_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt new file mode 100644 index 0000000000..3c1c646537 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt @@ -0,0 +1,37 @@ +package com.tangem.features.staking.impl.presentation.state.stub + +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.StakingStates + +internal object StakingValidatorStateStub { + private val validator = Yield.Validator( + address = "address", + status = "status", + name = "Binance", + image = null, + website = null, + apr = "0.0354".toBigDecimal(), + commission = null, + stakedBalance = null, + votingPower = null, + preferred = false, + ) + + val state = StakingStates.ValidatorState.Data( + isPrimaryButtonEnabled = false, + validators = listOf( + validator, + validator.copy( + address = "TRONLink", + name = "TRONLink", + apr = "0.0506".toBigDecimal(), + ), + validator.copy( + address = "1inch", + name = "1inch", + apr = "0.0306".toBigDecimal(), + ), + ), + selectedValidator = validator, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt new file mode 100644 index 0000000000..88186729cf --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -0,0 +1,127 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.inputrow.InputRowImageSelector +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.extensions.annotatedReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.state.stub.StakingValidatorStateStub +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import java.math.BigDecimal + +/** + * Staking screen with validators + */ +@Composable +internal fun StakingValidatorListContent( + state: StakingStates.ValidatorState, + clickIntents: StakingClickIntents, + modifier: Modifier = Modifier, +) { + if (state !is StakingStates.ValidatorState.Data) return + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + LazyColumn( + contentPadding = PaddingValues(bottom = bottomBarHeight), + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = "HEADER") { + Text( + text = stringResource(R.string.staking_validator), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .clip(CornersToRound.TOP_2.getShape()) + .background(TangemTheme.colors.background.action) + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing8, + ), + ) + } + items( + count = state.validators.size, + key = { state.validators[it] }, + contentType = { state.validators[it]::class.java }, + ) { index -> + val item = state.validators[index] + + InputRowImageSelector( + subtitle = stringReference(item.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true), + ) + } + }, + ), + ), + imageUrl = item.image.orEmpty(), + isSelected = item == state.selectedValidator, + onSelect = { clickIntents.onValidatorSelect(item) }, + modifier = Modifier + .clip( + if (index == state.validators.lastIndex) { + CornersToRound.BOTTOM_2 + } else { + CornersToRound.ZERO + }.getShape(), + ) + .background(TangemTheme.colors.background.action), + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingValidatorListContent_Preview( + @PreviewParameter(StakingValidatorListContentPreviewProvider::class) + data: StakingStates.ValidatorState.Data, +) { + TangemThemePreview { + StakingValidatorListContent( + state = data, + clickIntents = StakingClickIntentsStub, + ) + } +} + +private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(StakingValidatorStateStub.state) +} +// endregion \ No newline at end of file From 97f63586b22c5152728e7d0748e95592be1b2d67 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jun 2024 12:55:02 +0500 Subject: [PATCH 065/223] Updated on 2026-08-14 --- .../atoms/radiobutton/TangemRadioButton.kt | 9 +- .../presentation/state/StakingStateRouter.kt | 8 +- .../impl/presentation/state/StakingUiState.kt | 7 +- ...nerValidatorState.kt => ValidatorState.kt} | 9 +- .../ConfirmStakingStatePreviewData.kt | 8 +- .../state/stub/StakingClickIntentsStub.kt | 5 ++ .../state/stub/StakingValidatorStateStub.kt | 37 -------- .../SetInitialDataStateTransformer.kt | 14 +++- .../ValidatorSelectChangeTransformer.kt | 24 ++++++ .../presentation/ui/StakingConfirmContent.kt | 11 ++- .../ui/StakingNavigationButtons.kt | 12 ++- .../impl/presentation/ui/StakingScreen.kt | 17 +++- .../ui/StakingValidatorListContent.kt | 84 ++++++++++--------- .../presentation/ui/block/ValidatorBlock.kt | 69 +++++++++++++++ .../viewmodel/StakingClickIntents.kt | 5 ++ .../viewmodel/StakingViewModel.kt | 45 +++------- 16 files changed, 229 insertions(+), 135 deletions(-) rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/{InnerValidatorState.kt => ValidatorState.kt} (53%) delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt index 20d61ebe87..777062e8bd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt @@ -26,11 +26,18 @@ import com.tangem.core.ui.res.TangemThemePreview * @param isSelected Whether the radio button is selected * @param onClick Called when the user clicks the button * @param modifier Modifier to be applied to the button + * @param isEnabled Whether the button click is enabled */ @Composable -fun TangemRadioButton(isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { +fun TangemRadioButton( + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { Box( modifier = modifier.clickable( + enabled = isEnabled, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16), onClick = onClick, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 7d70ebd667..79fff44a09 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -30,7 +30,9 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.uiState.value.currentStep) { StakingStep.InitialInfo -> showAmount() - StakingStep.Amount -> showConfirm() + StakingStep.Validators, + StakingStep.Amount, + -> showConfirm() StakingStep.Confirm -> showSuccess() StakingStep.Success -> onBackClick() } @@ -51,6 +53,10 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Amount) } } + fun showValidators() { + stateController.update { it.copy(currentStep = StakingStep.Validators) } + } + fun showConfirm() { stateController.update { it.copy(currentStep = StakingStep.Confirm) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index bd01183407..1f6e81e3b3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -6,7 +6,6 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.Yield import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -85,16 +84,12 @@ internal sealed class StakingStates { val appCurrency: AppCurrency, val isFeeApproximate: Boolean, ) - - data class ValidatorState( - val validatorState: InnerValidatorState, - val availableValidators: List, - ) } enum class StakingStep { InitialInfo, Amount, + Validators, Confirm, Success, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt similarity index 53% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt index 8a3c91fa45..d99ed81d8e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerValidatorState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt @@ -4,13 +4,14 @@ import androidx.compose.runtime.Immutable import com.tangem.domain.staking.model.Yield @Immutable -internal sealed class InnerValidatorState { +internal sealed class ValidatorState { data class Content( val chosenValidator: Yield.Validator, - ) : InnerValidatorState() + val availableValidators: List, + ) : ValidatorState() - data object Loading : InnerValidatorState() + data object Loading : ValidatorState() - data object Error : InnerValidatorState() + data object Error : ValidatorState() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt index 13d3cc4a1c..3e81a056fc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt @@ -7,9 +7,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.Yield import com.tangem.features.staking.impl.presentation.state.InnerFeeState -import com.tangem.features.staking.impl.presentation.state.InnerValidatorState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.ValidatorState import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -73,10 +73,8 @@ internal object ConfirmStakingStatePreviewData { isFeeApproximate = false, isFeeConvertibleToFiat = true, ), - validatorState = StakingStates.ValidatorState( - validatorState = InnerValidatorState.Content( - chosenValidator = validatorList[0], - ), + validatorState = ValidatorState.Content( + chosenValidator = validatorList[0], availableValidators = validatorList, ), footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 7c01094036..f59a64f975 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.stub +import com.tangem.domain.staking.model.Yield import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents object StakingClickIntentsStub : StakingClickIntents { @@ -19,4 +20,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onCurrencyChangeClick(isFiat: Boolean) {} override fun onAmountNext() {} + + override fun openValidators() {} + + override fun onValidatorSelect(validator: Yield.Validator) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt deleted file mode 100644 index 3c1c646537..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingValidatorStateStub.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.stub - -import com.tangem.domain.staking.model.Yield -import com.tangem.features.staking.impl.presentation.state.StakingStates - -internal object StakingValidatorStateStub { - private val validator = Yield.Validator( - address = "address", - status = "status", - name = "Binance", - image = null, - website = null, - apr = "0.0354".toBigDecimal(), - commission = null, - stakedBalance = null, - votingPower = null, - preferred = false, - ) - - val state = StakingStates.ValidatorState.Data( - isPrimaryButtonEnabled = false, - validators = listOf( - validator, - validator.copy( - address = "TRONLink", - name = "TRONLink", - apr = "0.0506".toBigDecimal(), - ), - validator.copy( - address = "1inch", - name = "1inch", - apr = "0.0306".toBigDecimal(), - ), - ), - selectedValidator = validator, - ) -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index c06765c6ed..7a0d619a46 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -14,8 +14,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer @@ -47,6 +49,7 @@ internal class SetInitialDataStateTransformer( currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), amountState = createInitialAmountState(), + confirmStakingState = createInitialConfirmationState(), ) } @@ -75,6 +78,15 @@ internal class SetInitialDataStateTransformer( return amountStateConverter.convert("") } + private fun createInitialConfirmationState(): StakingStates.ConfirmStakingState { + return ConfirmStakingStatePreviewData.confirmStakingState.copy( + validatorState = ValidatorState.Content( + chosenValidator = yield.validators.first(), + availableValidators = yield.validators, + ), + ) + } + private fun getAprRange(): TextReference { val aprValues = yield.validators.mapNotNull { it.apr } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt new file mode 100644 index 0000000000..b72f227693 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.validator + +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.utils.transformer.Transformer + +internal class ValidatorSelectChangeTransformer( + private val selectedValidator: Yield.Validator, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data ?: return prevState + val validatorState = confirmState.validatorState as? ValidatorState.Content ?: return prevState + + return prevState.copy( + confirmStakingState = confirmState.copy( + validatorState = validatorState.copy( + chosenValidator = selectedValidator, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt index 9c78086d8f..be8589fa2f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt @@ -18,11 +18,18 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock +import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @Composable -internal fun StakingConfirmContent(amountState: AmountState, state: StakingStates.ConfirmStakingState) { +internal fun StakingConfirmContent( + amountState: AmountState, + state: StakingStates.ConfirmStakingState, + clickIntents: StakingClickIntents, +) { if (state !is StakingStates.ConfirmStakingState.Data) return Column( @@ -39,6 +46,7 @@ internal fun StakingConfirmContent(amountState: AmountState, state: StakingState isEditingDisabled = true, onClick = {}, ) + ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators) StakingFeeBlock(feeState = state.feeState) NotificationsBlock(notifications = state.notifications) SpacerHMax() @@ -66,6 +74,7 @@ private fun Preview_StakingConfirmContent() { StakingConfirmContent( amountState = AmountStatePreviewData.amountState, state = ConfirmStakingStatePreviewData.confirmStakingState, + clickIntents = StakingClickIntentsStub, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 21105e3419..9ae31fc482 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.ui -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -9,7 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,8 +26,8 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState @Composable internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) { @@ -105,7 +107,8 @@ private fun getButtonData(currentState: StakingUiState): Pair Unit> { StakingStep.Amount, StakingStep.Confirm, StakingStep.Success, - -> R.string.common_next to { currentState.clickIntents.onNextClick() } + -> R.string.common_next to currentState.clickIntents::onNextClick + StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick } } @@ -113,6 +116,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean { return when (uiState.currentStep) { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled + StakingStep.Validators -> uiState.confirmStakingState.isPrimaryButtonEnabled StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled StakingStep.Success -> true } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index a8f36b6e5e..4a41fba77f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import kotlinx.coroutines.delay @@ -50,13 +51,16 @@ internal fun StakingScreen(uiState: StakingUiState) { @Composable private fun SendAppBar(uiState: StakingUiState) { val titleRes = when (uiState.currentStep) { - StakingStep.InitialInfo -> stringResource(id = R.string.common_stake) StakingStep.Amount -> stringResource(id = R.string.send_amount_label) - StakingStep.Confirm -> stringResource(id = R.string.common_stake) + StakingStep.InitialInfo, + StakingStep.Validators, + StakingStep.Confirm, + -> stringResource(id = R.string.common_stake) StakingStep.Success -> "" } val backIcon = when (uiState.currentStep) { StakingStep.Amount, + StakingStep.Validators, StakingStep.Confirm, StakingStep.Success, -> { @@ -130,7 +134,16 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M StakingStep.Confirm -> StakingConfirmContent( amountState = uiState.amountState, state = uiState.confirmStakingState, + clickIntents = uiState.clickIntents, ) + StakingStep.Validators -> { + val confirmState = uiState.confirmStakingState + if (confirmState !is StakingStates.ConfirmStakingState.Data) return@AnimatedContent + StakingValidatorListContent( + state = confirmState.validatorState, + clickIntents = uiState.clickIntents, + ) + } else -> TODO() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 88186729cf..1d6e05103d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -26,9 +26,9 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.state.stub.StakingValidatorStateStub import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import java.math.BigDecimal @@ -37,11 +37,10 @@ import java.math.BigDecimal */ @Composable internal fun StakingValidatorListContent( - state: StakingStates.ValidatorState, + state: ValidatorState, clickIntents: StakingClickIntents, modifier: Modifier = Modifier, ) { - if (state !is StakingStates.ValidatorState.Data) return val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( @@ -65,41 +64,44 @@ internal fun StakingValidatorListContent( ), ) } - items( - count = state.validators.size, - key = { state.validators[it] }, - contentType = { state.validators[it]::class.java }, - ) { index -> - val item = state.validators[index] + if (state is ValidatorState.Content) { + val validators = state.availableValidators + items( + count = validators.size, + key = { validators[it].address }, + contentType = { validators[it]::class.java }, + ) { index -> + val item = validators[index] - InputRowImageSelector( - subtitle = stringReference(item.name), - caption = combinedReference( - resourceReference(R.string.staking_details_apr), - annotatedReference( - buildAnnotatedString { - append(" ") - withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { - append( - BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true), - ) - } - }, + InputRowImageSelector( + subtitle = stringReference(item.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true), + ) + } + }, + ), ), - ), - imageUrl = item.image.orEmpty(), - isSelected = item == state.selectedValidator, - onSelect = { clickIntents.onValidatorSelect(item) }, - modifier = Modifier - .clip( - if (index == state.validators.lastIndex) { - CornersToRound.BOTTOM_2 - } else { - CornersToRound.ZERO - }.getShape(), - ) - .background(TangemTheme.colors.background.action), - ) + imageUrl = item.image.orEmpty(), + isSelected = item == state.chosenValidator, + onSelect = { clickIntents.onValidatorSelect(item) }, + modifier = Modifier + .clip( + if (index == validators.lastIndex) { + CornersToRound.BOTTOM_2 + } else { + CornersToRound.ZERO + }.getShape(), + ) + .background(TangemTheme.colors.background.action), + ) + } } } } @@ -110,7 +112,7 @@ internal fun StakingValidatorListContent( @Composable private fun StakingValidatorListContent_Preview( @PreviewParameter(StakingValidatorListContentPreviewProvider::class) - data: StakingStates.ValidatorState.Data, + data: ValidatorState, ) { TangemThemePreview { StakingValidatorListContent( @@ -120,8 +122,8 @@ private fun StakingValidatorListContent_Preview( } } -private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf(StakingValidatorStateStub.state) +private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(ConfirmStakingStatePreviewData.confirmStakingState.validatorState) } // endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt new file mode 100644 index 0000000000..4cee3bcba5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -0,0 +1,69 @@ +package com.tangem.features.staking.impl.presentation.ui.block + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.inputrow.InputRowImageChevron +import com.tangem.core.ui.extensions.annotatedReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import java.math.BigDecimal + +@Composable +internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onClick, + ) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResource(R.string.staking_validator), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + ) + if (validatorState is ValidatorState.Content) { + InputRowImageChevron( + subtitle = stringReference(validatorState.chosenValidator.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + val apr = validatorState.chosenValidator.apr ?: BigDecimal.ZERO + append(BigDecimalFormatter.formatPercent(apr, true)) + } + }, + ), + ), + imageUrl = validatorState.chosenValidator.image.orEmpty(), + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 27cd56dced..235659fa3e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.domain.staking.model.Yield internal interface StakingClickIntents : AmountScreenClickIntents { @@ -11,4 +12,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onPrevClick() override fun onAmountNext() = onNextClick() + + fun openValidators() + + fun onValidatorSelect(validator: Yield.Validator) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 15d7803cc7..ee086db4f2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -15,22 +15,20 @@ import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter -import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmStateDataStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -48,7 +46,6 @@ internal class StakingViewModel @Inject constructor( private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val isFeeApproximateUseCase: IsFeeApproximateUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -58,18 +55,17 @@ internal class StakingViewModel @Inject constructor( var stakingStateRouter: StakingStateRouter by Delegates.notNull() private set - private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle.get( - AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY, - ) - ?.let { it.unbundle(CryptoCurrency.ID.serializer()) } - ?: error("This screen can't be opened without `CryptoCurrency.ID`") + private val cryptoCurrencyId: CryptoCurrency.ID = + savedStateHandle.get(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY) + ?.unbundle(CryptoCurrency.ID.serializer()) + ?: error("This screen can't be opened without `CryptoCurrency.ID`") private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Staking.USER_WALLET_ID_KEY) - ?.let { it.unbundle(UserWalletId.serializer()) } + ?.unbundle(UserWalletId.serializer()) ?: error("This screen can't be opened without `UserWalletId`") private val yield: Yield = savedStateHandle.get(AppRoute.Staking.YIELD_KEY) - ?.let { it.unbundle(Yield.serializer()) } + ?.unbundle(Yield.serializer()) ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() @@ -90,27 +86,6 @@ internal class StakingViewModel @Inject constructor( override fun onNextClick() { stakingStateRouter.onNextClick() - when (value.currentStep) { - StakingStep.Confirm -> { - stateController.update( - SetConfirmStateDataStateTransformer( - yield = yield, - appCurrencyProvider = Provider { appCurrency }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - isFeeApproximateUseCase = isFeeApproximateUseCase, - ), - ) - } - StakingStep.InitialInfo -> { - // TODO staking - } - StakingStep.Amount -> { - // TODO staking - } - StakingStep.Success -> { - // TODO staking - } - } } override fun onPrevClick() { @@ -133,6 +108,12 @@ internal class StakingViewModel @Inject constructor( stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat)) } + override fun openValidators() = stakingStateRouter.showValidators() + + override fun onValidatorSelect(validator: Yield.Validator) { + stateController.update(ValidatorSelectChangeTransformer(validator)) + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter From f506db9c0e9affaba6ed50996bc17cb4dcbd320a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jun 2024 02:26:36 +0300 Subject: [PATCH 066/223] Updated on 2026-08-14 --- .../common/adapter/UnknownEnumMoshiAdapter.kt | 14 +- .../datasource/api/stakekit/StakeKitApi.kt | 26 +- .../api/stakekit/models/request/Address.kt | 39 +++ .../ConstructTransactionRequestBody.kt | 24 ++ .../models/request/EnterActionRequestBody.kt | 47 ++++ .../SubmitTransactionHashRequestBody.kt | 8 + .../models/request/YieldBalanceRequestBody.kt | 17 -- .../models/response/EnterActionResponse.kt | 33 +++ .../models/response/model/NetworkTypeDTO.kt | 205 ++++++++++++++++ .../models/response/model/TokenDTO.kt | 228 ++---------------- .../response/model/YieldBalanceWrapperDTO.kt | 1 + .../model/action/StakingActionStatusDTO.kt | 25 ++ .../{ => action}/StakingActionTypeDTO.kt | 2 +- .../transaction/StakingGasEstimateDTO.kt | 16 ++ .../transaction/StakingTransactionDTO.kt | 37 +++ .../StakingTransactionStatusDTO.kt | 37 +++ .../transaction/StakingTransactionTypeDTO.kt | 124 ++++++++++ .../data/staking/DefaultStakingRepository.kt | 11 +- .../converters/StakingNetworkTypeConverter.kt | 81 +++++++ .../data/staking/converters/TokenConverter.kt | 23 ++ .../data/staking/converters/YieldConverter.kt | 101 +------- .../action/ActionStatusConverter.kt | 19 ++ .../action/EnterActionResponseConverter.kt | 28 +++ .../action/StakingActionTypeConverter.kt | 30 +++ .../transaction/GasEstimateConverter.kt | 19 ++ .../StakingTransactionConverter.kt | 33 +++ .../StakingTransactionStatusConverter.kt | 24 ++ .../StakingTransactionTypeConverter.kt | 54 +++++ domain/staking/models/build.gradle.kts | 1 + .../domain/staking/model/NetworkType.kt | 71 ++++++ .../com/tangem/domain/staking/model/Yield.kt | 72 +----- .../staking/model/action/EnterAction.kt | 18 ++ .../model/action/StakingActionStatus.kt | 11 + .../staking/model/action/StakingActionType.kt | 20 ++ .../model/transaction/StakingGasEstimate.kt | 10 + .../model/transaction/StakingTransaction.kt | 20 ++ .../transaction/StakingTransactionStatus.kt | 15 ++ .../transaction/StakingTransactionType.kt | 44 ++++ 38 files changed, 1182 insertions(+), 406 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/{ => action}/StakingActionTypeDTO.kt (99%) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index 1d8fc5ffc1..2d46248977 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -2,10 +2,11 @@ package com.tangem.datasource.api.common.adapter import com.squareup.moshi.* import com.squareup.moshi.adapters.EnumJsonAdapter -import com.tangem.datasource.api.stakekit.models.response.model.StakingActionTypeDTO -import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.* +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO /** * Object to create a adapter for enum types with support for unknown enum values. @@ -20,11 +21,14 @@ object UnknownEnumMoshiAdapter { fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { val map = mapOf( - TokenDTO.NetworkTypeDTO::class.java to TokenDTO.NetworkTypeDTO.UNKNOWN, + NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN, StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN, YieldBalanceWrapperDTO.BalanceDTO.BalanceType::class.java to YieldBalanceWrapperDTO.BalanceDTO.BalanceType.UNKNOWN, + StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, + StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, + StakingActionStatusDTO::class.java to StakingActionStatusDTO.UNKNOWN, ) return apply { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 6f1df5b3f4..9818ec8742 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -1,17 +1,14 @@ package com.tangem.datasource.api.stakekit import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody -import com.tangem.datasource.api.stakekit.models.request.RevenueOption -import com.tangem.datasource.api.stakekit.models.request.YieldType +import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse +import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query +import retrofit2.http.* @Suppress("LongParameterList") interface StakeKitApi { @@ -45,4 +42,19 @@ interface StakeKitApi { @GET("tokens") suspend fun getTokens(): ApiResponse> + + @POST("actions/enter") + suspend fun createEnterAction(@Body body: EnterActionRequestBody): ApiResponse + + @PATCH("transactions/{transactionId}") + suspend fun constructTransaction( + @Path("transactionId") transactionId: String, + @Body body: ConstructTransactionRequestBody, + ): ApiResponse + + @POST("transactions/{transactionId}/submit_hash") + suspend fun submitTransactionHash( + @Path("transactionId") transactionId: String, + @Body body: SubmitTransactionHashRequestBody, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt new file mode 100644 index 0000000000..03921fa39c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt @@ -0,0 +1,39 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json + +data class Address( + @Json(name = "address") + val address: String, + @Json(name = "additionalAddresses") + val additionalAddresses: AdditionalAddresses? = null, + @Json(name = "explorerUrl") + val explorerUrl: String? = null, +) { + + data class AdditionalAddresses( + // cosmos-specific + @Json(name = "cosmosPubKey") + val cosmosPubKey: String? = null, + + // binance-specific + @Json(name = "binanceBeaconAddress") + val binanceBeaconAddress: String? = null, + + // solana-specific + @Json(name = "stakeAccounts") + val stakeAccounts: List? = null, + @Json(name = "lidoStakeAccounts") + val lidoStakeAccounts: List? = null, + + // tezos-specific + @Json(name = "tezosPubKey") + val tezosPubKey: String? = null, + + // avalanche-specific + @Json(name = "cAddressBech") + val cAddressBech: String? = null, + @Json(name = "pAddressBech") + val pAddressBech: String? = null, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt new file mode 100644 index 0000000000..60ee34b310 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class ConstructTransactionRequestBody( + @Json(name = "gasArgs") + val gasArgs: GasArgs? = null, + @Json(name = "ledgerWalletAPICompatible") + val ledgerWalletAPICompatible: Boolean? = null, +) { + data class GasArgs( + // cosmos-specific + @Json(name = "gasPrice") + val gasPrice: BigDecimal? = null, + // EVM eip 1559 specific + @Json(name = "type") + val type: Int? = null, + @Json(name = "maxFeePerGas") + val maxFeePerGas: BigDecimal? = null, + @Json(name = "maxPriorityFeePerGas") + val maxPriorityFeePerGas: BigDecimal? = null, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt new file mode 100644 index 0000000000..677ff1a33f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt @@ -0,0 +1,47 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO + +data class EnterActionRequestBody( + @Json(name = "integrationId") + val integrationId: String, + @Json(name = "addresses") + val addresses: Address, + @Json(name = "args") + val args: EnterActionRequestBodyArgs, + @Json(name = "referralCode") + val referralCode: String? = null, +) { + + data class EnterActionRequestBodyArgs( + @Json(name = "amount") + val amount: String, + @Json(name = "validatorAddress") + val validatorAddress: String? = null, + @Json(name = "validatorAddresses") + val validatorAddresses: List? = null, + @Json(name = "providerId") + val providerId: String? = null, + @Json(name = "duration") + val duration: String? = null, + @Json(name = "nfts") + val nfts: List? = null, + @Json(name = "ledgerWalletAPICompatible") + val ledgerWalletAPICompatible: Boolean? = null, + @Json(name = "tronResource") + val tronResource: String? = null, + @Json(name = "signatureVerification") + val signatureVerification: SignatureVerification? = null, + @Json(name = "inputToken") + val inputToken: TokenDTO? = null, + ) + + data class SignatureVerification( + @Json(name = "message") + val message: String, + @Json(name = "signed") + val signed: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt new file mode 100644 index 0000000000..c07a103fe1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json + +data class SubmitTransactionHashRequestBody( + @Json(name = "hash") + val hash: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index 7433386e73..bbfe66a6af 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -8,23 +8,6 @@ data class YieldBalanceRequestBody( @Json(name = "integrationId") val integrationId: String? = null, ) { - data class Address( - @Json(name = "address") val address: String, - @Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null, - @Json(name = "explorerUrl") val explorerUrl: String, - ) { - - data class AdditionalAddresses( - @Json(name = "cosmosPubKey") val cosmosPubKey: String? = null, - @Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null, - @Json(name = "stakeAccounts") val stakeAccounts: List? = null, - @Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List? = null, - @Json(name = "tezosPubKey") val tezosPubKey: String? = null, - @Json(name = "cAddressBech") val cAddressBech: String? = null, - @Json(name = "pAddressBech") val pAddressBech: String? = null, - ) - } - data class YieldBalanceRequestArgs( @Json(name = "validatorAddresses") val validatorAddresses: List, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt new file mode 100644 index 0000000000..9413c43398 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.api.stakekit.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO +import org.joda.time.DateTime +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class EnterActionResponse( + @Json(name = "id") + val id: String, + @Json(name = "integrationId") + val integrationId: String, + @Json(name = "status") + val status: StakingActionStatusDTO, + @Json(name = "type") + val type: StakingActionTypeDTO, + @Json(name = "currentStepIndex") + val currentStepIndex: Int, + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @Json(name = "transactions") + val transactions: List?, + @Json(name = "createdAt") + val createdAt: DateTime, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt new file mode 100644 index 0000000000..c7fedd5ec8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt @@ -0,0 +1,205 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json + +enum class NetworkTypeDTO { + @Json(name = "avalanche-c") + AVALANCHE_C, + + @Json(name = "avalanche-atomic") + AVALANCHE_ATOMIC, + + @Json(name = "avalanche-p") + AVALANCHE_P, + + @Json(name = "arbitrum") + ARBITRUM, + + @Json(name = "binance") + BINANCE, + + @Json(name = "celo") + CELO, + + @Json(name = "ethereum") + ETHEREUM, + + @Json(name = "ethereum-goerli") + ETHEREUM_GOERLI, + + @Json(name = "ethereum-holesky") + ETHEREUM_HOLESKY, + + @Json(name = "fantom") + FANTOM, + + @Json(name = "harmony") + HARMONY, + + @Json(name = "optimism") + OPTIMISM, + + @Json(name = "polygon") + POLYGON, + + @Json(name = "gnosis") + GNOSIS, + + @Json(name = "moonriver") + MOONRIVER, + + @Json(name = "okc") + OKC, + + @Json(name = "zksync") + ZKSYNC, + + @Json(name = "viction") + VICTION, + + @Json(name = "agoric") + AGORIC, + + @Json(name = "akash") + AKASH, + + @Json(name = "axelar") + AXELAR, + + @Json(name = "band-protocol") + BAND_PROTOCOL, + + @Json(name = "bitsong") + BITSONG, + + @Json(name = "canto") + CANTO, + + @Json(name = "chihuahua") + CHIHUAHUA, + + @Json(name = "comdex") + COMDEX, + + @Json(name = "coreum") + COREUM, + + @Json(name = "cosmos") + COSMOS, + + @Json(name = "crescent") + CRESCENT, + + @Json(name = "cronos") + CRONOS, + + @Json(name = "cudos") + CUDOS, + + @Json(name = "desmos") + DESMOS, + + @Json(name = "dydx") + DYDX, + + @Json(name = "evmos") + EVMOS, + + @Json(name = "fetch-ai") + FETCH_AI, + + @Json(name = "gravity-bridge") + GRAVITY_BRIDGE, + + @Json(name = "injective") + INJECTIVE, + + @Json(name = "irisnet") + IRISNET, + + @Json(name = "juno") + JUNO, + + @Json(name = "kava") + KAVA, + + @Json(name = "ki-network") + KI_NETWORK, + + @Json(name = "mars-protocol") + MARS_PROTOCOL, + + @Json(name = "nym") + NYM, + + @Json(name = "okex-chain") + OKEX_CHAIN, + + @Json(name = "onomy") + ONOMY, + + @Json(name = "osmosis") + OSMOSIS, + + @Json(name = "persistence") + PERSISTENCE, + + @Json(name = "quicksilver") + QUICKSILVER, + + @Json(name = "regen") + REGEN, + + @Json(name = "secret") + SECRET, + + @Json(name = "sentinel") + SENTINEL, + + @Json(name = "sommelier") + SOMMELIER, + + @Json(name = "stafi") + STAFI, + + @Json(name = "stargaze") + STARGAZE, + + @Json(name = "stride") + STRIDE, + + @Json(name = "teritori") + TERITORI, + + @Json(name = "tgrade") + TGRADE, + + @Json(name = "umee") + UMEE, + + @Json(name = "polkadot") + POLKADOT, + + @Json(name = "kusama") + KUSAMA, + + @Json(name = "westend") + WESTEND, + + @Json(name = "binancebeacon") + BINANCEBEACON, + + @Json(name = "near") + NEAR, + + @Json(name = "solana") + SOLANA, + + @Json(name = "tezos") + TEZOS, + + @Json(name = "tron") + TRON, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt index 6321b54ecb..62eb0a4c95 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt @@ -5,214 +5,20 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class TokenDTO( - @Json(name = "name") val name: String, - @Json(name = "network") val network: NetworkTypeDTO, - @Json(name = "symbol") val symbol: String, - @Json(name = "decimals") val decimals: Int, - @Json(name = "address") val address: String?, - @Json(name = "coinGeckoId") val coinGeckoId: String?, - @Json(name = "logoURI") val logoURI: String?, - @Json(name = "isPoints") val isPoints: Boolean?, -) { - enum class NetworkTypeDTO { - @Json(name = "avalanche-c") - AVALANCHE_C, - - @Json(name = "avalanche-atomic") - AVALANCHE_ATOMIC, - - @Json(name = "avalanche-p") - AVALANCHE_P, - - @Json(name = "arbitrum") - ARBITRUM, - - @Json(name = "binance") - BINANCE, - - @Json(name = "celo") - CELO, - - @Json(name = "ethereum") - ETHEREUM, - - @Json(name = "ethereum-goerli") - ETHEREUM_GOERLI, - - @Json(name = "ethereum-holesky") - ETHEREUM_HOLESKY, - - @Json(name = "fantom") - FANTOM, - - @Json(name = "harmony") - HARMONY, - - @Json(name = "optimism") - OPTIMISM, - - @Json(name = "polygon") - POLYGON, - - @Json(name = "gnosis") - GNOSIS, - - @Json(name = "moonriver") - MOONRIVER, - - @Json(name = "okc") - OKC, - - @Json(name = "zksync") - ZKSYNC, - - @Json(name = "viction") - VICTION, - - @Json(name = "agoric") - AGORIC, - - @Json(name = "akash") - AKASH, - - @Json(name = "axelar") - AXELAR, - - @Json(name = "band-protocol") - BAND_PROTOCOL, - - @Json(name = "bitsong") - BITSONG, - - @Json(name = "canto") - CANTO, - - @Json(name = "chihuahua") - CHIHUAHUA, - - @Json(name = "comdex") - COMDEX, - - @Json(name = "coreum") - COREUM, - - @Json(name = "cosmos") - COSMOS, - - @Json(name = "crescent") - CRESCENT, - - @Json(name = "cronos") - CRONOS, - - @Json(name = "cudos") - CUDOS, - - @Json(name = "desmos") - DESMOS, - - @Json(name = "dydx") - DYDX, - - @Json(name = "evmos") - EVMOS, - - @Json(name = "fetch-ai") - FETCH_AI, - - @Json(name = "gravity-bridge") - GRAVITY_BRIDGE, - - @Json(name = "injective") - INJECTIVE, - - @Json(name = "irisnet") - IRISNET, - - @Json(name = "juno") - JUNO, - - @Json(name = "kava") - KAVA, - - @Json(name = "ki-network") - KI_NETWORK, - - @Json(name = "mars-protocol") - MARS_PROTOCOL, - - @Json(name = "nym") - NYM, - - @Json(name = "okex-chain") - OKEX_CHAIN, - - @Json(name = "onomy") - ONOMY, - - @Json(name = "osmosis") - OSMOSIS, - - @Json(name = "persistence") - PERSISTENCE, - - @Json(name = "quicksilver") - QUICKSILVER, - - @Json(name = "regen") - REGEN, - - @Json(name = "secret") - SECRET, - - @Json(name = "sentinel") - SENTINEL, - - @Json(name = "sommelier") - SOMMELIER, - - @Json(name = "stafi") - STAFI, - - @Json(name = "stargaze") - STARGAZE, - - @Json(name = "stride") - STRIDE, - - @Json(name = "teritori") - TERITORI, - - @Json(name = "tgrade") - TGRADE, - - @Json(name = "umee") - UMEE, - - @Json(name = "polkadot") - POLKADOT, - - @Json(name = "kusama") - KUSAMA, - - @Json(name = "westend") - WESTEND, - - @Json(name = "binancebeacon") - BINANCEBEACON, - - @Json(name = "near") - NEAR, - - @Json(name = "solana") - SOLANA, - - @Json(name = "tezos") - TEZOS, - - @Json(name = "tron") - TRON, - - UNKNOWN, - } -} \ No newline at end of file + @Json(name = "name") + val name: String, + @Json(name = "network") + val network: NetworkTypeDTO, + @Json(name = "symbol") + val symbol: String, + @Json(name = "decimals") + val decimals: Int, + @Json(name = "address") + val address: String?, + @Json(name = "coinGeckoId") + val coinGeckoId: String?, + @Json(name = "logoURI") + val logoURI: String?, + @Json(name = "isPoints") + val isPoints: Boolean?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index c64bdd591d..2fc44ede6e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import org.joda.time.DateTime import java.math.BigDecimal diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt new file mode 100644 index 0000000000..040cab01ea --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.api.stakekit.models.response.model.action + +import com.squareup.moshi.Json + +enum class StakingActionStatusDTO { + @Json(name = "CANCELED") + CANCELED, + + @Json(name = "CREATED") + CREATED, + + @Json(name = "WAITING_FOR_NEXT") + WAITING_FOR_NEXT, + + @Json(name = "PROCESSING") + PROCESSING, + + @Json(name = "FAILED") + FAILED, + + @Json(name = "SUCCESS") + SUCCESS, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt similarity index 99% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt index 5f310f04a8..3ba035dc64 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionTypeDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt @@ -1,4 +1,4 @@ -package com.tangem.datasource.api.stakekit.models.response.model +package com.tangem.datasource.api.stakekit.models.response.model.action import com.squareup.moshi.Json diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt new file mode 100644 index 0000000000..0e22ee6d9f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class StakingGasEstimateDTO( + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "token") + val token: TokenDTO, + @Json(name = "gasLimit") + val gasLimit: BigDecimal, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt new file mode 100644 index 0000000000..0a28baaeee --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt @@ -0,0 +1,37 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO + +@JsonClass(generateAdapter = true) +data class StakingTransactionDTO( + @Json(name = "id") + val id: String, + @Json(name = "network") + val network: NetworkTypeDTO, + @Json(name = "status") + val status: StakingTransactionStatusDTO, + @Json(name = "type") + val type: StakingTransactionTypeDTO, + @Json(name = "hash") + val hash: String?, + @Json(name = "signedTransaction") + val signedTransaction: String?, + @Json(name = "unsignedTransaction") + val unsignedTransaction: String?, + @Json(name = "stepIndex") + val stepIndex: Int, + @Json(name = "error") + val error: String?, + @Json(name = "gasEstimate") + val gasEstimate: StakingGasEstimateDTO?, + @Json(name = "stakeId") + val stakeId: String?, + @Json(name = "explorerUrl") + val explorerUrl: String?, + @Json(name = "ledgerHwAppId") + val ledgerHwAppId: String?, + @Json(name = "isMessage") + val isMessage: Boolean, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt new file mode 100644 index 0000000000..f7a4f6d76d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt @@ -0,0 +1,37 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json + +enum class StakingTransactionStatusDTO { + @Json(name = "NOT_FOUND") + NOT_FOUND, + + @Json(name = "CREATED") + CREATED, + + @Json(name = "BLOCKED") + BLOCKED, + + @Json(name = "WAITING_FOR_SIGNATURE") + WAITING_FOR_SIGNATURE, + + @Json(name = "SIGNED") + SIGNED, + + @Json(name = "BROADCASTED") + BROADCASTED, + + @Json(name = "PENDING") + PENDING, + + @Json(name = "CONFIRMED") + CONFIRMED, + + @Json(name = "FAILED") + FAILED, + + @Json(name = "SKIPPED") + SKIPPED, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt new file mode 100644 index 0000000000..e79e28e7bb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt @@ -0,0 +1,124 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json + +enum class StakingTransactionTypeDTO { + @Json(name = "SWAP") + SWAP, + + @Json(name = "DEPOSIT") + DEPOSIT, + + @Json(name = "APPROVAL") + APPROVAL, + + @Json(name = "STAKE") + STAKE, + + @Json(name = "CLAIM_UNSTAKED") + CLAIM_UNSTAKED, + + @Json(name = "CLAIM_REWARDS") + CLAIM_REWARDS, + + @Json(name = "RESTAKE_REWARDS") + RESTAKE_REWARDS, + + @Json(name = "UNSTAKE") + UNSTAKE, + + @Json(name = "SPLIT") + SPLIT, + + @Json(name = "MERGE") + MERGE, + + @Json(name = "LOCK") + LOCK, + + @Json(name = "UNLOCK") + UNLOCK, + + @Json(name = "SUPPLY") + SUPPLY, + + @Json(name = "BRIDGE") + BRIDGE, + + @Json(name = "VOTE") + VOTE, + + @Json(name = "REVOKE") + REVOKE, + + @Json(name = "RESTAKE") + RESTAKE, + + @Json(name = "REBOND") + REBOND, + + @Json(name = "WITHDRAW") + WITHDRAW, + + @Json(name = "CREATE_ACCOUNT") + CREATE_ACCOUNT, + + @Json(name = "REVEAL") + REVEAL, + + @Json(name = "MIGRATE") + MIGRATE, + + @Json(name = "UTXO_P_TO_C_IMPORT") + UTXO_P_TO_C_IMPORT, + + @Json(name = "UTXO_C_TO_P_IMPORT") + UTXO_C_TO_P_IMPORT, + + @Json(name = "UNFREEZE_LEGACY") + UNFREEZE_LEGACY, + + @Json(name = "UNFREEZE_LEGACY_BANDWIDTH") + UNFREEZE_LEGACY_BANDWIDTH, + + @Json(name = "UNFREEZE_LEGACY_ENERGY") + UNFREEZE_LEGACY_ENERGY, + + @Json(name = "UNFREEZE_BANDWIDTH") + UNFREEZE_BANDWIDTH, + + @Json(name = "UNFREEZE_ENERGY") + UNFREEZE_ENERGY, + + @Json(name = "FREEZE_BANDWIDTH") + FREEZE_BANDWIDTH, + + @Json(name = "FREEZE_ENERGY") + FREEZE_ENERGY, + + @Json(name = "UNDELEGATE_BANDWIDTH") + UNDELEGATE_BANDWIDTH, + + @Json(name = "UNDELEGATE_ENERGY") + UNDELEGATE_ENERGY, + + @Json(name = "P2P_NODE_REQUEST") + P2P_NODE_REQUEST, + + @Json(name = "LUGANODES_PROVISION") + LUGANODES_PROVISION, + + @Json(name = "LUGANODES_EXIT_REQUEST") + LUGANODES_EXIT_REQUEST, + + @Json(name = "INFSTONES_PROVISION") + INFSTONES_PROVISION, + + @Json(name = "INFSTONES_EXIT_REQUEST") + INFSTONES_EXIT_REQUEST, + + @Json(name = "INFSTONES_CLAIM_REQUEST") + INFSTONES_CLAIM_REQUEST, + + UNKNOWN, +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index cc0d2ba7b3..f0e5a33941 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,6 +2,8 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.data.staking.converters.StakingNetworkTypeConverter +import com.tangem.data.staking.converters.TokenConverter import com.tangem.data.staking.converters.YieldConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi @@ -20,7 +22,14 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { - private val yieldConverter = YieldConverter() + private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() + + private val tokenConverter = TokenConverter( + stakingNetworkTypeConverter = stakingNetworkTypeConverter, + ) + private val yieldConverter = YieldConverter( + tokenConverter = tokenConverter, + ) override fun isStakingSupported(currencyId: String): Boolean { return integrationIds.contains(currencyId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt new file mode 100644 index 0000000000..f7cd02c340 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt @@ -0,0 +1,81 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO +import com.tangem.domain.staking.model.NetworkType +import com.tangem.utils.converter.Converter + +@Suppress("CyclomaticComplexMethod", "LongMethod") +class StakingNetworkTypeConverter : Converter { + + override fun convert(value: NetworkTypeDTO): NetworkType { + return when (value) { + NetworkTypeDTO.AVALANCHE_C -> NetworkType.AVALANCHE_C + NetworkTypeDTO.AVALANCHE_ATOMIC -> NetworkType.AVALANCHE_ATOMIC + NetworkTypeDTO.AVALANCHE_P -> NetworkType.AVALANCHE_P + NetworkTypeDTO.ARBITRUM -> NetworkType.ARBITRUM + NetworkTypeDTO.BINANCE -> NetworkType.BINANCE + NetworkTypeDTO.CELO -> NetworkType.CELO + NetworkTypeDTO.ETHEREUM -> NetworkType.ETHEREUM + NetworkTypeDTO.ETHEREUM_GOERLI -> NetworkType.ETHEREUM_GOERLI + NetworkTypeDTO.ETHEREUM_HOLESKY -> NetworkType.ETHEREUM_HOLESKY + NetworkTypeDTO.FANTOM -> NetworkType.FANTOM + NetworkTypeDTO.HARMONY -> NetworkType.HARMONY + NetworkTypeDTO.OPTIMISM -> NetworkType.OPTIMISM + NetworkTypeDTO.POLYGON -> NetworkType.POLYGON + NetworkTypeDTO.GNOSIS -> NetworkType.GNOSIS + NetworkTypeDTO.MOONRIVER -> NetworkType.MOONRIVER + NetworkTypeDTO.OKC -> NetworkType.OKC + NetworkTypeDTO.ZKSYNC -> NetworkType.ZKSYNC + NetworkTypeDTO.VICTION -> NetworkType.VICTION + NetworkTypeDTO.AGORIC -> NetworkType.AGORIC + NetworkTypeDTO.AKASH -> NetworkType.AKASH + NetworkTypeDTO.AXELAR -> NetworkType.AXELAR + NetworkTypeDTO.BAND_PROTOCOL -> NetworkType.BAND_PROTOCOL + NetworkTypeDTO.BITSONG -> NetworkType.BITSONG + NetworkTypeDTO.CANTO -> NetworkType.CANTO + NetworkTypeDTO.CHIHUAHUA -> NetworkType.CHIHUAHUA + NetworkTypeDTO.COMDEX -> NetworkType.COMDEX + NetworkTypeDTO.COREUM -> NetworkType.COREUM + NetworkTypeDTO.COSMOS -> NetworkType.COSMOS + NetworkTypeDTO.CRESCENT -> NetworkType.CRESCENT + NetworkTypeDTO.CRONOS -> NetworkType.CRONOS + NetworkTypeDTO.CUDOS -> NetworkType.CUDOS + NetworkTypeDTO.DESMOS -> NetworkType.DESMOS + NetworkTypeDTO.DYDX -> NetworkType.DYDX + NetworkTypeDTO.EVMOS -> NetworkType.EVMOS + NetworkTypeDTO.FETCH_AI -> NetworkType.FETCH_AI + NetworkTypeDTO.GRAVITY_BRIDGE -> NetworkType.GRAVITY_BRIDGE + NetworkTypeDTO.INJECTIVE -> NetworkType.INJECTIVE + NetworkTypeDTO.IRISNET -> NetworkType.IRISNET + NetworkTypeDTO.JUNO -> NetworkType.JUNO + NetworkTypeDTO.KAVA -> NetworkType.KAVA + NetworkTypeDTO.KI_NETWORK -> NetworkType.KI_NETWORK + NetworkTypeDTO.MARS_PROTOCOL -> NetworkType.MARS_PROTOCOL + NetworkTypeDTO.NYM -> NetworkType.NYM + NetworkTypeDTO.OKEX_CHAIN -> NetworkType.OKEX_CHAIN + NetworkTypeDTO.ONOMY -> NetworkType.ONOMY + NetworkTypeDTO.OSMOSIS -> NetworkType.OSMOSIS + NetworkTypeDTO.PERSISTENCE -> NetworkType.PERSISTENCE + NetworkTypeDTO.QUICKSILVER -> NetworkType.QUICKSILVER + NetworkTypeDTO.REGEN -> NetworkType.REGEN + NetworkTypeDTO.SECRET -> NetworkType.SECRET + NetworkTypeDTO.SENTINEL -> NetworkType.SENTINEL + NetworkTypeDTO.SOMMELIER -> NetworkType.SOMMELIER + NetworkTypeDTO.STAFI -> NetworkType.STAFI + NetworkTypeDTO.STARGAZE -> NetworkType.STARGAZE + NetworkTypeDTO.STRIDE -> NetworkType.STRIDE + NetworkTypeDTO.TERITORI -> NetworkType.TERITORI + NetworkTypeDTO.TGRADE -> NetworkType.TGRADE + NetworkTypeDTO.UMEE -> NetworkType.UMEE + NetworkTypeDTO.POLKADOT -> NetworkType.POLKADOT + NetworkTypeDTO.KUSAMA -> NetworkType.KUSAMA + NetworkTypeDTO.WESTEND -> NetworkType.WESTEND + NetworkTypeDTO.BINANCEBEACON -> NetworkType.BINANCEBEACON + NetworkTypeDTO.NEAR -> NetworkType.NEAR + NetworkTypeDTO.SOLANA -> NetworkType.SOLANA + NetworkTypeDTO.TEZOS -> NetworkType.TEZOS + NetworkTypeDTO.TRON -> NetworkType.TRON + else -> NetworkType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt new file mode 100644 index 0000000000..480d4574bc --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.domain.staking.model.Token +import com.tangem.utils.converter.Converter + +class TokenConverter( + private val stakingNetworkTypeConverter: StakingNetworkTypeConverter, +) : Converter { + + override fun convert(value: TokenDTO): Token { + return Token( + name = value.name, + network = stakingNetworkTypeConverter.convert(value.network), + symbol = value.symbol, + decimals = value.decimals, + address = value.address, + coinGeckoId = value.coinGeckoId, + logoURI = value.logoURI, + isPoints = value.isPoints, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index d7300234f0..8dd5a5536f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -1,18 +1,19 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO -import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.domain.staking.model.* import com.tangem.utils.converter.Converter -class YieldConverter : Converter { +class YieldConverter( + private val tokenConverter: TokenConverter, +) : Converter { override fun convert(value: YieldDTO): Yield { return Yield( id = value.id, - token = convertToken(value.token), - tokens = value.tokens.map { convertToken(it) }, + token = tokenConverter.convert(value.token), + tokens = value.tokens.map { tokenConverter.convert(it) }, args = convertArgs(value.args), status = convertStatus(value.status), apy = value.apy, @@ -24,92 +25,6 @@ class YieldConverter : Converter { ) } - private fun convertToken(tokenDTO: TokenDTO): Token { - return Token( - name = tokenDTO.name, - network = convertNetworkType(tokenDTO.network), - symbol = tokenDTO.symbol, - decimals = tokenDTO.decimals, - address = tokenDTO.address, - coinGeckoId = tokenDTO.coinGeckoId, - logoURI = tokenDTO.logoURI, - isPoints = tokenDTO.isPoints, - ) - } - - @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun convertNetworkType(networkTypeDTO: TokenDTO.NetworkTypeDTO): Token.NetworkType { - return when (networkTypeDTO) { - TokenDTO.NetworkTypeDTO.AVALANCHE_C -> Token.NetworkType.AVALANCHE_C - TokenDTO.NetworkTypeDTO.AVALANCHE_ATOMIC -> Token.NetworkType.AVALANCHE_ATOMIC - TokenDTO.NetworkTypeDTO.AVALANCHE_P -> Token.NetworkType.AVALANCHE_P - TokenDTO.NetworkTypeDTO.ARBITRUM -> Token.NetworkType.ARBITRUM - TokenDTO.NetworkTypeDTO.BINANCE -> Token.NetworkType.BINANCE - TokenDTO.NetworkTypeDTO.CELO -> Token.NetworkType.CELO - TokenDTO.NetworkTypeDTO.ETHEREUM -> Token.NetworkType.ETHEREUM - TokenDTO.NetworkTypeDTO.ETHEREUM_GOERLI -> Token.NetworkType.ETHEREUM_GOERLI - TokenDTO.NetworkTypeDTO.ETHEREUM_HOLESKY -> Token.NetworkType.ETHEREUM_HOLESKY - TokenDTO.NetworkTypeDTO.FANTOM -> Token.NetworkType.FANTOM - TokenDTO.NetworkTypeDTO.HARMONY -> Token.NetworkType.HARMONY - TokenDTO.NetworkTypeDTO.OPTIMISM -> Token.NetworkType.OPTIMISM - TokenDTO.NetworkTypeDTO.POLYGON -> Token.NetworkType.POLYGON - TokenDTO.NetworkTypeDTO.GNOSIS -> Token.NetworkType.GNOSIS - TokenDTO.NetworkTypeDTO.MOONRIVER -> Token.NetworkType.MOONRIVER - TokenDTO.NetworkTypeDTO.OKC -> Token.NetworkType.OKC - TokenDTO.NetworkTypeDTO.ZKSYNC -> Token.NetworkType.ZKSYNC - TokenDTO.NetworkTypeDTO.VICTION -> Token.NetworkType.VICTION - TokenDTO.NetworkTypeDTO.AGORIC -> Token.NetworkType.AGORIC - TokenDTO.NetworkTypeDTO.AKASH -> Token.NetworkType.AKASH - TokenDTO.NetworkTypeDTO.AXELAR -> Token.NetworkType.AXELAR - TokenDTO.NetworkTypeDTO.BAND_PROTOCOL -> Token.NetworkType.BAND_PROTOCOL - TokenDTO.NetworkTypeDTO.BITSONG -> Token.NetworkType.BITSONG - TokenDTO.NetworkTypeDTO.CANTO -> Token.NetworkType.CANTO - TokenDTO.NetworkTypeDTO.CHIHUAHUA -> Token.NetworkType.CHIHUAHUA - TokenDTO.NetworkTypeDTO.COMDEX -> Token.NetworkType.COMDEX - TokenDTO.NetworkTypeDTO.COREUM -> Token.NetworkType.COREUM - TokenDTO.NetworkTypeDTO.COSMOS -> Token.NetworkType.COSMOS - TokenDTO.NetworkTypeDTO.CRESCENT -> Token.NetworkType.CRESCENT - TokenDTO.NetworkTypeDTO.CRONOS -> Token.NetworkType.CRONOS - TokenDTO.NetworkTypeDTO.CUDOS -> Token.NetworkType.CUDOS - TokenDTO.NetworkTypeDTO.DESMOS -> Token.NetworkType.DESMOS - TokenDTO.NetworkTypeDTO.DYDX -> Token.NetworkType.DYDX - TokenDTO.NetworkTypeDTO.EVMOS -> Token.NetworkType.EVMOS - TokenDTO.NetworkTypeDTO.FETCH_AI -> Token.NetworkType.FETCH_AI - TokenDTO.NetworkTypeDTO.GRAVITY_BRIDGE -> Token.NetworkType.GRAVITY_BRIDGE - TokenDTO.NetworkTypeDTO.INJECTIVE -> Token.NetworkType.INJECTIVE - TokenDTO.NetworkTypeDTO.IRISNET -> Token.NetworkType.IRISNET - TokenDTO.NetworkTypeDTO.JUNO -> Token.NetworkType.JUNO - TokenDTO.NetworkTypeDTO.KAVA -> Token.NetworkType.KAVA - TokenDTO.NetworkTypeDTO.KI_NETWORK -> Token.NetworkType.KI_NETWORK - TokenDTO.NetworkTypeDTO.MARS_PROTOCOL -> Token.NetworkType.MARS_PROTOCOL - TokenDTO.NetworkTypeDTO.NYM -> Token.NetworkType.NYM - TokenDTO.NetworkTypeDTO.OKEX_CHAIN -> Token.NetworkType.OKEX_CHAIN - TokenDTO.NetworkTypeDTO.ONOMY -> Token.NetworkType.ONOMY - TokenDTO.NetworkTypeDTO.OSMOSIS -> Token.NetworkType.OSMOSIS - TokenDTO.NetworkTypeDTO.PERSISTENCE -> Token.NetworkType.PERSISTENCE - TokenDTO.NetworkTypeDTO.QUICKSILVER -> Token.NetworkType.QUICKSILVER - TokenDTO.NetworkTypeDTO.REGEN -> Token.NetworkType.REGEN - TokenDTO.NetworkTypeDTO.SECRET -> Token.NetworkType.SECRET - TokenDTO.NetworkTypeDTO.SENTINEL -> Token.NetworkType.SENTINEL - TokenDTO.NetworkTypeDTO.SOMMELIER -> Token.NetworkType.SOMMELIER - TokenDTO.NetworkTypeDTO.STAFI -> Token.NetworkType.STAFI - TokenDTO.NetworkTypeDTO.STARGAZE -> Token.NetworkType.STARGAZE - TokenDTO.NetworkTypeDTO.STRIDE -> Token.NetworkType.STRIDE - TokenDTO.NetworkTypeDTO.TERITORI -> Token.NetworkType.TERITORI - TokenDTO.NetworkTypeDTO.TGRADE -> Token.NetworkType.TGRADE - TokenDTO.NetworkTypeDTO.UMEE -> Token.NetworkType.UMEE - TokenDTO.NetworkTypeDTO.POLKADOT -> Token.NetworkType.POLKADOT - TokenDTO.NetworkTypeDTO.KUSAMA -> Token.NetworkType.KUSAMA - TokenDTO.NetworkTypeDTO.WESTEND -> Token.NetworkType.WESTEND - TokenDTO.NetworkTypeDTO.BINANCEBEACON -> Token.NetworkType.BINANCEBEACON - TokenDTO.NetworkTypeDTO.NEAR -> Token.NetworkType.NEAR - TokenDTO.NetworkTypeDTO.SOLANA -> Token.NetworkType.SOLANA - TokenDTO.NetworkTypeDTO.TEZOS -> Token.NetworkType.TEZOS - TokenDTO.NetworkTypeDTO.TRON -> Token.NetworkType.TRON - else -> Token.NetworkType.UNKNOWN - } - } - private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args { return Yield.Args( enter = convertEnter(argsDTO.enter), @@ -153,9 +68,9 @@ class YieldConverter : Converter { logoUri = metadataDTO.logoUri, description = metadataDTO.description, documentation = metadataDTO.documentation, - gasFeeToken = convertToken(metadataDTO.gasFeeTokenDTO), - token = convertToken(metadataDTO.tokenDTO), - tokens = metadataDTO.tokensDTO.map { convertToken(it) }, + gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO), + token = tokenConverter.convert(metadataDTO.tokenDTO), + tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) }, type = metadataDTO.type, rewardSchedule = metadataDTO.rewardSchedule, cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod), diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt new file mode 100644 index 0000000000..360ebbbbf6 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.data.staking.converters.action + +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO +import com.tangem.domain.staking.model.action.StakingActionStatus +import com.tangem.utils.converter.Converter + +class ActionStatusConverter : Converter { + override fun convert(value: StakingActionStatusDTO): StakingActionStatus { + return when (value) { + StakingActionStatusDTO.CANCELED -> StakingActionStatus.CANCELED + StakingActionStatusDTO.CREATED -> StakingActionStatus.CREATED + StakingActionStatusDTO.WAITING_FOR_NEXT -> StakingActionStatus.WAITING_FOR_NEXT + StakingActionStatusDTO.PROCESSING -> StakingActionStatus.PROCESSING + StakingActionStatusDTO.FAILED -> StakingActionStatus.FAILED + StakingActionStatusDTO.SUCCESS -> StakingActionStatus.SUCCESS + else -> StakingActionStatus.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt new file mode 100644 index 0000000000..888d805305 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -0,0 +1,28 @@ +package com.tangem.data.staking.converters.action + +import com.tangem.data.staking.converters.transaction.StakingTransactionConverter +import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.utils.converter.Converter + +class EnterActionResponseConverter( + private val actionStatusConverter: ActionStatusConverter, + private val stakingActionTypeConverter: StakingActionTypeConverter, + private val transactionConverter: StakingTransactionConverter, +) : Converter { + + override fun convert(value: EnterActionResponse): EnterAction { + return EnterAction( + id = value.id, + integrationId = value.integrationId, + status = actionStatusConverter.convert(value.status), + type = stakingActionTypeConverter.convert(value.type), + currentStepIndex = value.currentStepIndex, + amount = value.amount, + validatorAddress = value.validatorAddress, + validatorAddresses = value.validatorAddresses, + transactions = value.transactions?.map(transactionConverter::convert), + createdAt = value.createdAt, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt new file mode 100644 index 0000000000..941020c57d --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.data.staking.converters.action + +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.utils.converter.Converter + +@Suppress("CyclomaticComplexMethod") +class StakingActionTypeConverter : Converter { + + override fun convert(value: StakingActionTypeDTO): StakingActionType { + return when (value) { + StakingActionTypeDTO.STAKE -> StakingActionType.STAKE + StakingActionTypeDTO.UNSTAKE -> StakingActionType.UNSTAKE + StakingActionTypeDTO.CLAIM_REWARDS -> StakingActionType.CLAIM_REWARDS + StakingActionTypeDTO.RESTAKE_REWARDS -> StakingActionType.RESTAKE_REWARDS + StakingActionTypeDTO.WITHDRAW -> StakingActionType.WITHDRAW + StakingActionTypeDTO.RESTAKE -> StakingActionType.RESTAKE + StakingActionTypeDTO.CLAIM_UNSTAKED -> StakingActionType.CLAIM_UNSTAKED + StakingActionTypeDTO.UNLOCK_LOCKED -> StakingActionType.UNLOCK_LOCKED + StakingActionTypeDTO.STAKE_LOCKED -> StakingActionType.STAKE_LOCKED + StakingActionTypeDTO.VOTE -> StakingActionType.VOTE + StakingActionTypeDTO.REVOKE -> StakingActionType.REVOKE + StakingActionTypeDTO.VOTE_LOCKED -> StakingActionType.VOTE_LOCKED + StakingActionTypeDTO.REVOTE -> StakingActionType.REVOTE + StakingActionTypeDTO.REBOND -> StakingActionType.REBOND + StakingActionTypeDTO.MIGRATE -> StakingActionType.MIGRATE + StakingActionTypeDTO.UNKNOWN -> StakingActionType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt new file mode 100644 index 0000000000..3219d87359 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.data.staking.converters.TokenConverter +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.utils.converter.Converter + +class GasEstimateConverter( + private val tokenConverter: TokenConverter, +) : Converter { + + override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate { + return StakingGasEstimate( + amount = value.amount, + token = tokenConverter.convert(value.token), + gasLimit = value.gasLimit, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt new file mode 100644 index 0000000000..1ad9ca3198 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.data.staking.converters.StakingNetworkTypeConverter +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.utils.converter.Converter + +class StakingTransactionConverter( + private val networkTypeConverter: StakingNetworkTypeConverter, + private val transactionStatusConverter: StakingTransactionStatusConverter, + private val transactionTypeConverter: StakingTransactionTypeConverter, + private val gasEstimateConverter: GasEstimateConverter, +) : Converter { + + override fun convert(value: StakingTransactionDTO): StakingTransaction { + return StakingTransaction( + id = value.id, + network = networkTypeConverter.convert(value.network), + status = transactionStatusConverter.convert(value.status), + type = transactionTypeConverter.convert(value.type), + hash = value.hash, + signedTransaction = value.signedTransaction, + unsignedTransaction = value.unsignedTransaction, + stepIndex = value.stepIndex, + error = value.error, + gasEstimate = value.gasEstimate?.let { gasEstimateConverter.convert(it) }, + stakeId = value.stakeId, + explorerUrl = value.explorerUrl, + ledgerHwAppId = value.ledgerHwAppId, + isMessage = value.isMessage, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt new file mode 100644 index 0000000000..9569f61534 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt @@ -0,0 +1,24 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO +import com.tangem.domain.staking.model.transaction.StakingTransactionStatus +import com.tangem.utils.converter.Converter + +class StakingTransactionStatusConverter : Converter { + + override fun convert(value: StakingTransactionStatusDTO): StakingTransactionStatus { + return when (value) { + StakingTransactionStatusDTO.NOT_FOUND -> StakingTransactionStatus.NOT_FOUND + StakingTransactionStatusDTO.CREATED -> StakingTransactionStatus.CREATED + StakingTransactionStatusDTO.BLOCKED -> StakingTransactionStatus.BLOCKED + StakingTransactionStatusDTO.WAITING_FOR_SIGNATURE -> StakingTransactionStatus.WAITING_FOR_SIGNATURE + StakingTransactionStatusDTO.SIGNED -> StakingTransactionStatus.SIGNED + StakingTransactionStatusDTO.BROADCASTED -> StakingTransactionStatus.BROADCASTED + StakingTransactionStatusDTO.PENDING -> StakingTransactionStatus.PENDING + StakingTransactionStatusDTO.CONFIRMED -> StakingTransactionStatus.CONFIRMED + StakingTransactionStatusDTO.FAILED -> StakingTransactionStatus.FAILED + StakingTransactionStatusDTO.SKIPPED -> StakingTransactionStatus.SKIPPED + StakingTransactionStatusDTO.UNKNOWN -> StakingTransactionStatus.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt new file mode 100644 index 0000000000..15e156f691 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt @@ -0,0 +1,54 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO +import com.tangem.domain.staking.model.transaction.StakingTransactionType +import com.tangem.utils.converter.Converter + +@Suppress("CyclomaticComplexMethod") +class StakingTransactionTypeConverter : Converter { + + override fun convert(value: StakingTransactionTypeDTO): StakingTransactionType { + return when (value) { + StakingTransactionTypeDTO.SWAP -> StakingTransactionType.SWAP + StakingTransactionTypeDTO.DEPOSIT -> StakingTransactionType.DEPOSIT + StakingTransactionTypeDTO.APPROVAL -> StakingTransactionType.APPROVAL + StakingTransactionTypeDTO.STAKE -> StakingTransactionType.STAKE + StakingTransactionTypeDTO.CLAIM_UNSTAKED -> StakingTransactionType.CLAIM_UNSTAKED + StakingTransactionTypeDTO.CLAIM_REWARDS -> StakingTransactionType.CLAIM_REWARDS + StakingTransactionTypeDTO.RESTAKE_REWARDS -> StakingTransactionType.RESTAKE_REWARDS + StakingTransactionTypeDTO.UNSTAKE -> StakingTransactionType.UNSTAKE + StakingTransactionTypeDTO.SPLIT -> StakingTransactionType.SPLIT + StakingTransactionTypeDTO.MERGE -> StakingTransactionType.MERGE + StakingTransactionTypeDTO.LOCK -> StakingTransactionType.LOCK + StakingTransactionTypeDTO.UNLOCK -> StakingTransactionType.UNLOCK + StakingTransactionTypeDTO.SUPPLY -> StakingTransactionType.SUPPLY + StakingTransactionTypeDTO.BRIDGE -> StakingTransactionType.BRIDGE + StakingTransactionTypeDTO.VOTE -> StakingTransactionType.VOTE + StakingTransactionTypeDTO.REVOKE -> StakingTransactionType.REVOKE + StakingTransactionTypeDTO.RESTAKE -> StakingTransactionType.RESTAKE + StakingTransactionTypeDTO.REBOND -> StakingTransactionType.REBOND + StakingTransactionTypeDTO.WITHDRAW -> StakingTransactionType.WITHDRAW + StakingTransactionTypeDTO.CREATE_ACCOUNT -> StakingTransactionType.CREATE_ACCOUNT + StakingTransactionTypeDTO.REVEAL -> StakingTransactionType.REVEAL + StakingTransactionTypeDTO.MIGRATE -> StakingTransactionType.MIGRATE + StakingTransactionTypeDTO.UTXO_P_TO_C_IMPORT -> StakingTransactionType.UTXO_P_TO_C_IMPORT + StakingTransactionTypeDTO.UTXO_C_TO_P_IMPORT -> StakingTransactionType.UTXO_C_TO_P_IMPORT + StakingTransactionTypeDTO.UNFREEZE_LEGACY -> StakingTransactionType.UNFREEZE_LEGACY + StakingTransactionTypeDTO.UNFREEZE_LEGACY_BANDWIDTH -> StakingTransactionType.UNFREEZE_LEGACY_BANDWIDTH + StakingTransactionTypeDTO.UNFREEZE_LEGACY_ENERGY -> StakingTransactionType.UNFREEZE_LEGACY_ENERGY + StakingTransactionTypeDTO.UNFREEZE_BANDWIDTH -> StakingTransactionType.UNFREEZE_BANDWIDTH + StakingTransactionTypeDTO.UNFREEZE_ENERGY -> StakingTransactionType.UNFREEZE_ENERGY + StakingTransactionTypeDTO.FREEZE_BANDWIDTH -> StakingTransactionType.FREEZE_BANDWIDTH + StakingTransactionTypeDTO.FREEZE_ENERGY -> StakingTransactionType.FREEZE_ENERGY + StakingTransactionTypeDTO.UNDELEGATE_BANDWIDTH -> StakingTransactionType.UNDELEGATE_BANDWIDTH + StakingTransactionTypeDTO.UNDELEGATE_ENERGY -> StakingTransactionType.UNDELEGATE_ENERGY + StakingTransactionTypeDTO.P2P_NODE_REQUEST -> StakingTransactionType.P2P_NODE_REQUEST + StakingTransactionTypeDTO.LUGANODES_PROVISION -> StakingTransactionType.LUGANODES_PROVISION + StakingTransactionTypeDTO.LUGANODES_EXIT_REQUEST -> StakingTransactionType.LUGANODES_EXIT_REQUEST + StakingTransactionTypeDTO.INFSTONES_PROVISION -> StakingTransactionType.INFSTONES_PROVISION + StakingTransactionTypeDTO.INFSTONES_EXIT_REQUEST -> StakingTransactionType.INFSTONES_EXIT_REQUEST + StakingTransactionTypeDTO.INFSTONES_CLAIM_REQUEST -> StakingTransactionType.INFSTONES_CLAIM_REQUEST + StakingTransactionTypeDTO.UNKNOWN -> StakingTransactionType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts index 2a478f5d98..7558c5ca61 100644 --- a/domain/staking/models/build.gradle.kts +++ b/domain/staking/models/build.gradle.kts @@ -8,4 +8,5 @@ dependencies { implementation(projects.domain.core) implementation(deps.kotlin.serialization) + implementation(deps.jodatime) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt new file mode 100644 index 0000000000..4359b0b39c --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.staking.model + +enum class NetworkType { + AVALANCHE_C, + AVALANCHE_ATOMIC, + AVALANCHE_P, + ARBITRUM, + BINANCE, + CELO, + ETHEREUM, + ETHEREUM_GOERLI, + ETHEREUM_HOLESKY, + FANTOM, + HARMONY, + OPTIMISM, + POLYGON, + GNOSIS, + MOONRIVER, + OKC, + ZKSYNC, + VICTION, + AGORIC, + AKASH, + AXELAR, + BAND_PROTOCOL, + BITSONG, + CANTO, + CHIHUAHUA, + COMDEX, + COREUM, + COSMOS, + CRESCENT, + CRONOS, + CUDOS, + DESMOS, + DYDX, + EVMOS, + FETCH_AI, + GRAVITY_BRIDGE, + INJECTIVE, + IRISNET, + JUNO, + KAVA, + KI_NETWORK, + MARS_PROTOCOL, + NYM, + OKEX_CHAIN, + ONOMY, + OSMOSIS, + PERSISTENCE, + QUICKSILVER, + REGEN, + SECRET, + SENTINEL, + SOMMELIER, + STAFI, + STARGAZE, + STRIDE, + TERITORI, + TGRADE, + UMEE, + POLKADOT, + KUSAMA, + WESTEND, + BINANCEBEACON, + NEAR, + SOLANA, + TEZOS, + TRON, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt index aa55291bb5..023b7ba52b 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt @@ -107,77 +107,7 @@ data class Token( val coinGeckoId: String?, val logoURI: String?, val isPoints: Boolean?, -) { - enum class NetworkType { - AVALANCHE_C, - AVALANCHE_ATOMIC, - AVALANCHE_P, - ARBITRUM, - BINANCE, - CELO, - ETHEREUM, - ETHEREUM_GOERLI, - ETHEREUM_HOLESKY, - FANTOM, - HARMONY, - OPTIMISM, - POLYGON, - GNOSIS, - MOONRIVER, - OKC, - ZKSYNC, - VICTION, - AGORIC, - AKASH, - AXELAR, - BAND_PROTOCOL, - BITSONG, - CANTO, - CHIHUAHUA, - COMDEX, - COREUM, - COSMOS, - CRESCENT, - CRONOS, - CUDOS, - DESMOS, - DYDX, - EVMOS, - FETCH_AI, - GRAVITY_BRIDGE, - INJECTIVE, - IRISNET, - JUNO, - KAVA, - KI_NETWORK, - MARS_PROTOCOL, - NYM, - OKEX_CHAIN, - ONOMY, - OSMOSIS, - PERSISTENCE, - QUICKSILVER, - REGEN, - SECRET, - SENTINEL, - SOMMELIER, - STAFI, - STARGAZE, - STRIDE, - TERITORI, - TGRADE, - UMEE, - POLKADOT, - KUSAMA, - WESTEND, - BINANCEBEACON, - NEAR, - SOLANA, - TEZOS, - TRON, - UNKNOWN, - } -} +) @Serializable data class AddressArgument( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt new file mode 100644 index 0000000000..c8b8a639f5 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.model.action + +import com.tangem.domain.staking.model.transaction.StakingTransaction +import org.joda.time.DateTime +import java.math.BigDecimal + +data class EnterAction( + val id: String, + val integrationId: String, + val status: StakingActionStatus, + val type: StakingActionType, + val currentStepIndex: Int, + val amount: BigDecimal, + val validatorAddress: String?, + val validatorAddresses: List?, + val transactions: List?, + val createdAt: DateTime, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt new file mode 100644 index 0000000000..a3e5da8836 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.staking.model.action + +enum class StakingActionStatus { + CANCELED, + CREATED, + WAITING_FOR_NEXT, + PROCESSING, + FAILED, + SUCCESS, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt new file mode 100644 index 0000000000..7346957e59 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.staking.model.action + +enum class StakingActionType { + STAKE, + UNSTAKE, + CLAIM_REWARDS, + RESTAKE_REWARDS, + WITHDRAW, + RESTAKE, + CLAIM_UNSTAKED, + UNLOCK_LOCKED, + STAKE_LOCKED, + VOTE, + REVOKE, + VOTE_LOCKED, + REVOTE, + REBOND, + MIGRATE, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt new file mode 100644 index 0000000000..9dacd68d30 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.staking.model.transaction + +import com.tangem.domain.staking.model.Token +import java.math.BigDecimal + +data class StakingGasEstimate( + val amount: BigDecimal, + val token: Token, + val gasLimit: BigDecimal, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt new file mode 100644 index 0000000000..0e937d1243 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.staking.model.transaction + +import com.tangem.domain.staking.model.NetworkType + +data class StakingTransaction( + val id: String, + val network: NetworkType, + val status: StakingTransactionStatus, + val type: StakingTransactionType, + val hash: String?, + val signedTransaction: String?, + val unsignedTransaction: String?, + val stepIndex: Int, + val error: String?, + val gasEstimate: StakingGasEstimate?, + val stakeId: String?, + val explorerUrl: String?, + val ledgerHwAppId: String?, + val isMessage: Boolean, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt new file mode 100644 index 0000000000..9fc6d4464f --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.staking.model.transaction + +enum class StakingTransactionStatus { + NOT_FOUND, + CREATED, + BLOCKED, + WAITING_FOR_SIGNATURE, + SIGNED, + BROADCASTED, + PENDING, + CONFIRMED, + FAILED, + SKIPPED, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt new file mode 100644 index 0000000000..cb52db3b39 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.staking.model.transaction + +enum class StakingTransactionType { + SWAP, + DEPOSIT, + APPROVAL, + STAKE, + CLAIM_UNSTAKED, + CLAIM_REWARDS, + RESTAKE_REWARDS, + UNSTAKE, + SPLIT, + MERGE, + LOCK, + UNLOCK, + SUPPLY, + BRIDGE, + VOTE, + REVOKE, + RESTAKE, + REBOND, + WITHDRAW, + CREATE_ACCOUNT, + REVEAL, + MIGRATE, + UTXO_P_TO_C_IMPORT, + UTXO_C_TO_P_IMPORT, + UNFREEZE_LEGACY, + UNFREEZE_LEGACY_BANDWIDTH, + UNFREEZE_LEGACY_ENERGY, + UNFREEZE_BANDWIDTH, + UNFREEZE_ENERGY, + FREEZE_BANDWIDTH, + FREEZE_ENERGY, + UNDELEGATE_BANDWIDTH, + UNDELEGATE_ENERGY, + P2P_NODE_REQUEST, + LUGANODES_PROVISION, + LUGANODES_EXIT_REQUEST, + INFSTONES_PROVISION, + INFSTONES_EXIT_REQUEST, + INFSTONES_CLAIM_REQUEST, + UNKNOWN, +} \ No newline at end of file From 5566331bdb0c475c0da0f4cbef7042323d0d3d9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jun 2024 21:10:01 +0500 Subject: [PATCH 067/223] Updated on 2026-08-14 --- app/build.gradle.kts | 4 ++++ .../java/com/tangem/tap/ApplicationEntryPoint.kt | 3 +++ .../main/java/com/tangem/tap/TangemApplication.kt | 5 +++++ .../tangem/tap/proxy/redux/DaggerGraphAction.kt | 4 ++++ .../tangem/tap/proxy/redux/DaggerGraphReducer.kt | 2 ++ .../tangem/tap/proxy/redux/DaggerGraphState.kt | 6 ++++++ .../com/tangem/tap/routing/utils/ChildFactory.kt | 15 ++++++++++++++- .../kotlin/com/tangem/common/routing/AppRoute.kt | 3 +++ features/details/impl/build.gradle.kts | 1 + features/disclaimer/impl/build.gradle.kts | 3 ++- .../PushNotificationsFeatureToggles.kt | 2 +- features/push-notifications/impl/build.gradle.kts | 5 ++++- .../DefaultPushNotificationsFeatureToggles.kt | 2 +- features/wallet/impl/build.gradle.kts | 1 + 14 files changed, 51 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3362aa5bbb..fa383da707 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,6 +136,10 @@ dependencies { implementation(projects.features.staking.impl) implementation(projects.features.details.api) implementation(projects.features.details.impl) + implementation(projects.features.disclaimer.api) + implementation(projects.features.disclaimer.impl) + implementation(projects.features.pushNotifications.api) + implementation(projects.features.pushNotifications.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 24ce4e1822..038f6f9a6a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -31,6 +31,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles @@ -116,4 +117,6 @@ interface ApplicationEntryPoint { fun getShareManager(): ShareManager fun getAppRouter(): AppRouter + + fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index e4c530273b..2b87ffb1f0 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -46,6 +46,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -193,6 +194,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val appRouter: AppRouter get() = entryPoint.getAppRouter() + + private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles + get() = entryPoint.getPushNotificationsFeatureToggles() // endregion override fun onCreate() { @@ -284,6 +288,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { urlOpener = urlOpener, shareManager = shareManager, appRouter = appRouter, + pushNotificationsFeatureToggles = pushNotificationsFeatureToggles, ), ), ) 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 3b479b590c..cfd8b41b5f 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 @@ -4,7 +4,9 @@ import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.managetokens.navigation.ManageTokensUi +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter @@ -27,5 +29,7 @@ sealed interface DaggerGraphAction : Action { val qrScanningRouter: QrScanningRouter, val emailSender: EmailSender, val stakingRouter: StakingRouter, + val disclaimerRouter: DisclaimerRouter, + val pushNotificationsRouter: PushNotificationsRouter, ) : 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 9f1f5fddca..0f98fea5e6 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 @@ -24,6 +24,8 @@ object DaggerGraphReducer { qrScanningRouter = action.qrScanningRouter, emailSender = action.emailSender, stakingRouter = action.stakingRouter, + disclaimerRouter = action.disclaimerRouter, + pushNotificationsRouter = action.pushNotificationsRouter, ) } } 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 8a555f904d..1e0ddd0477 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 @@ -28,8 +28,11 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.details.DetailsFeatureToggles +import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensUi +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -85,4 +88,7 @@ data class DaggerGraphState( val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, + val disclaimerRouter: DisclaimerRouter? = null, + val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles? = null, + val pushNotificationsRouter: PushNotificationsRouter? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 3199a16968..3a6bcba601 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -7,6 +7,9 @@ import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter @@ -48,6 +51,9 @@ internal class ChildFactory @Inject constructor( private val stakingRouter: StakingRouter, private val testerRouter: TesterRouter, private val detailsFeatureToggles: DetailsFeatureToggles, + private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, + private val pushNotificationRouter: PushNotificationsRouter, + private val disclaimerRouter: DisclaimerRouter, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -97,7 +103,11 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { SecurityModeFragment() }) } is AppRoute.Disclaimer -> { - route.asFragmentChild(Provider { DisclaimerFragment() }) + if (pushNotificationsFeatureToggles.isPushNotificationsEnabled) { + route.asFragmentChild(Provider { disclaimerRouter.entryFragment() }) + } else { + route.asFragmentChild(Provider { DisclaimerFragment() }) + } } is AppRoute.Home -> { route.asFragmentChild(Provider { HomeFragment() }) @@ -147,6 +157,9 @@ internal class ChildFactory @Inject constructor( is AppRoute.Staking -> { route.asFragmentChild(Provider { stakingRouter.getEntryFragment() }) } + is AppRoute.PushNotification -> { + route.asFragmentChild(Provider { pushNotificationRouter.entryFragment() }) + } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 0720f58f9d..2b6831a967 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -197,4 +197,7 @@ sealed class AppRoute(val path: String) : Route { const val YIELD_KEY = "yield" } } + + @Serializable + data object PushNotification : AppRoute(path = "/push_notification") } \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 8b6951e1e9..1edff72903 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Project - API */ implementation(projects.features.details.api) + implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) /* Project - Core */ diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index a2053b346f..c82364e03f 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.accompanist.permission) implementation(deps.compose.material3) + implementation(deps.compose.material) /** Core modules */ implementation(projects.core.ui) @@ -35,12 +36,12 @@ dependencies { /** Domain modules */ implementation(projects.domain.models) implementation(projects.domain.card) + implementation(projects.domain.settings) /** Feature modules */ implementation(projects.features.disclaimer.api) /** DI */ implementation(deps.hilt.android) - implementation(project(":domain:settings")) kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt index 06346f3b01..18024f464f 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt @@ -5,5 +5,5 @@ package com.tangem.features.pushnotifications.api.featuretoggles */ interface PushNotificationsFeatureToggles { /** Availability of push notifications */ - val isRedesignedSendEnabled: Boolean + val isPushNotificationsEnabled: Boolean } \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index 35c5535827..7cf8d0915d 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -28,6 +28,10 @@ dependencies { /** Core modules */ implementation(projects.core.ui) implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + + /** Common modules */ + implementation(projects.common.routing) /** Common modules */ implementation(projects.common.routing) @@ -40,6 +44,5 @@ dependencies { /** DI */ implementation(deps.hilt.android) - implementation(project(":core:navigation")) kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt index 05c8d6dfc4..296ae7ca81 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt @@ -6,6 +6,6 @@ import com.tangem.features.pushnotifications.api.featuretoggles.PushNotification internal class DefaultPushNotificationsFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : PushNotificationsFeatureToggles { - override val isRedesignedSendEnabled: Boolean + override val isPushNotificationsEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED") } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 7022b31c98..f9f232db02 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -89,4 +89,5 @@ dependencies { implementation(projects.features.tester.api) implementation(projects.features.manageTokens.api) implementation(projects.features.details.api) + implementation(projects.features.pushNotifications.api) } \ No newline at end of file From 76c5fd40323f65eb428f4bd2a6e0d15bad9a4b2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Jun 2024 21:11:19 +0500 Subject: [PATCH 068/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 39 +++--- .../domain/scanCard/UseCaseScanProcessor.kt | 6 +- .../com/tangem/core/ui/components/Buttons.kt | 3 +- .../appbar/AppBarWithAdditionalButtons.kt | 13 +- .../disclaimer/api/DisclaimerRouter.kt | 4 - .../disclaimer/impl/DisclaimerFragment.kt | 4 +- .../navigation/DefaultDisclaimerRouter.kt | 5 +- .../impl/presentation/ui/DisclaimerScreen.kt | 84 +++++++------ .../ui/DisclaimerWebViewClient.kt | 24 +++- .../viewmodel/DisclaimerViewModel.kt | 5 +- .../PushNotificationsBottomSheetConfig.kt | 11 ++ .../presentation/wallet/ui/WalletScreen.kt | 29 +++-- .../PushNotificationsBottomSheet.kt | 117 ++++++++++++++++++ .../wallet/viewmodels/WalletViewModel.kt | 37 +++++- .../viewmodels/intents/WalletClickIntents.kt | 5 +- .../WalletPushPermissionClickIntents.kt | 44 +++++++ 16 files changed, 338 insertions(+), 92 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index cf81714141..dddfa0a863 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,15 +1,12 @@ package com.tangem.tap -import android.Manifest import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.content.IntentFilter import android.content.pm.ActivityInfo -import android.content.pm.PackageManager import android.content.res.Configuration import android.nfc.NfcAdapter -import android.os.Build import android.os.Bundle import android.view.View import androidx.activity.SystemBarStyle @@ -21,8 +18,6 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle @@ -46,6 +41,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.data.card.sdk.CardSdkLifecycleObserver import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase @@ -53,7 +49,9 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.managetokens.navigation.ManageTokensUi +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter @@ -67,6 +65,7 @@ import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.sdk.TangemSdkManager @@ -79,10 +78,10 @@ import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.routing.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint @@ -180,6 +179,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject internal lateinit var routingComponentFactory: RoutingComponent.Factory + @Inject + lateinit var disclaimerRouter: DisclaimerRouter + + @Inject + lateinit var pushNotificationsRouter: PushNotificationsRouter + + @Inject + lateinit var cardRepository: CardRepository + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -216,7 +224,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac installRouting() initContent() - checkForNotificationPermission() observeStateUpdates() observePolkadotAccountHealthCheck() @@ -296,6 +303,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac qrScanningRouter = qrScanningRouter, emailSender = emailSender, stakingRouter = stakingRouter, + disclaimerRouter = disclaimerRouter, + pushNotificationsRouter = pushNotificationsRouter, ), ) } @@ -527,8 +536,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) } } else { - store.dispatchNavigationAction { replaceAll(AppRoute.Home) } lifecycleScope.launch { + val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) + val isEnabled = toggles.isPushNotificationsEnabled && !cardRepository.isTangemTOSAccepted() + val route = if (isEnabled) AppRoute.Disclaimer(isTosAccepted = false) else AppRoute.Home + + store.dispatchNavigationAction { replaceAll(route) } intentProcessor.handleIntent(intentWhichStartedActivity, false) } } @@ -536,16 +549,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(BackupAction.CheckForUnfinishedBackup) } - private fun checkForNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - BuildConfig.LOG_ENABLED && - ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != - PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0) - } - } - private fun observePolkadotAccountHealthCheck() { lifecycleScope.launch { getPolkadotCheckHasResetUseCase() 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 10646e2bfa..381d348980 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 @@ -59,7 +59,11 @@ internal object UseCaseScanProcessor { ), ) add(AnalyticsChain(Basic.CardWasScanned(analyticsSource))) - add(DisclaimerChain(store, disclaimerWillShow)) + val pushNotificationsToggles = + store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) + if (pushNotificationsToggles.isPushNotificationsEnabled) { + add(DisclaimerChain(store, disclaimerWillShow)) + } add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager)) } 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 bb17fcf7b6..7ef56f015e 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 @@ -86,6 +86,7 @@ fun PrimaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, size: TangemButtonSize = TangemButtonSize.Default, + colors: ButtonColors = TangemButtonsDefaults.primaryButtonColors, showProgress: Boolean = false, enabled: Boolean = true, ) { @@ -94,7 +95,7 @@ fun PrimaryButton( text = text, icon = TangemButtonIconPosition.None, onClick = onClick, - colors = TangemButtonsDefaults.primaryButtonColors, + colors = colors, enabled = enabled, showProgress = showProgress, size = size, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index dd23347e63..07f48d4787 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -14,6 +14,7 @@ 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.res.painterResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview @@ -39,12 +40,16 @@ fun AppBarWithAdditionalButtons( modifier: Modifier = Modifier, startButton: AdditionalButton? = null, endButton: AdditionalButton? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconColor: Color = TangemTheme.colors.icon.primary1, ) { AppBarWithAdditionalButtons( text = text.resolveReference(), startButton = startButton, endButton = endButton, modifier = modifier, + textColor = textColor, + iconColor = iconColor, ) } @@ -63,6 +68,8 @@ fun AppBarWithAdditionalButtons( modifier: Modifier = Modifier, startButton: AdditionalButton? = null, endButton: AdditionalButton? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconColor: Color = TangemTheme.colors.icon.primary1, ) { Box( modifier = modifier @@ -73,7 +80,7 @@ fun AppBarWithAdditionalButtons( Icon( painter = painterResource(id = startButton.iconRes), contentDescription = null, - tint = TangemTheme.colors.icon.primary1, + tint = iconColor, modifier = Modifier .size(TangemTheme.dimens.size24) .align(Alignment.CenterStart) @@ -92,7 +99,7 @@ fun AppBarWithAdditionalButtons( Text( text = text, modifier = Modifier.align(Alignment.Center), - color = TangemTheme.colors.text.primary1, + color = textColor, maxLines = 1, style = TangemTheme.typography.subtitle1, ) @@ -101,7 +108,7 @@ fun AppBarWithAdditionalButtons( Icon( painter = painterResource(id = endButton.iconRes), contentDescription = null, - tint = TangemTheme.colors.icon.primary1, + tint = iconColor, modifier = Modifier .size(TangemTheme.dimens.size24) .align(Alignment.CenterEnd) diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt index 5575d38b51..e990639fc8 100644 --- a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt @@ -5,8 +5,4 @@ import androidx.fragment.app.Fragment interface DisclaimerRouter { fun entryFragment(): Fragment - - companion object { - const val IS_TOS_ACCEPTED_KEY = "is_tos_accepted" - } } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt index e17e88299f..b53e0353d2 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt @@ -5,10 +5,10 @@ import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen import com.tangem.features.disclaimer.impl.presentation.viewmodel.DisclaimerViewModel import dagger.hilt.android.AndroidEntryPoint @@ -26,7 +26,7 @@ internal class DisclaimerFragment : ComposeFragment() { private val viewModel by viewModels() private val isTosAccepted: Boolean - get() = arguments?.getBoolean(DisclaimerRouter.IS_TOS_ACCEPTED_KEY) ?: false + get() = arguments?.getBoolean(IS_TOS_ACCEPTED_KEY) ?: false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt index a49d4a94c4..2897635472 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt @@ -4,15 +4,16 @@ import androidx.fragment.app.Fragment import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.features.disclaimer.impl.DisclaimerFragment +import javax.inject.Inject -internal class DefaultDisclaimerRouter( +internal class DefaultDisclaimerRouter @Inject constructor( private val appRouter: AppRouter, ) : InnerDisclaimerRouter { override fun entryFragment(): Fragment = DisclaimerFragment.create() override fun openPushNotificationPermission() { - // todo [REDACTED_JIRA] + appRouter.push(AppRoute.PushNotification) } override fun openHome() { diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt index e9a40a7a03..7b89328a8c 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt @@ -1,6 +1,7 @@ package com.tangem.features.disclaimer.impl.presentation.ui import android.Manifest +import android.annotation.SuppressLint import android.content.res.Configuration import android.os.Build import android.view.View @@ -9,12 +10,12 @@ import android.webkit.WebView import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf 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.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource @@ -28,6 +29,7 @@ import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -39,36 +41,37 @@ import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer @Composable internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - + val bottomPadding = if (state.isTosAccepted) { + bottomBarHeight + TangemTheme.dimens.size16 + } else { + bottomBarHeight + TangemTheme.dimens.size64 + } + val backgroundColor = if (state.isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 + val (textColor, iconColor) = if (state.isTosAccepted) { + TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1 + } else { + TangemColorPalette.Light4 to TangemColorPalette.Light4 + } Box( modifier = Modifier - .background(TangemColorPalette.Dark6) + .background(backgroundColor) .statusBarsPadding(), ) { - Column { + Column(modifier = Modifier.padding(bottom = bottomPadding)) { AppBarWithAdditionalButtons( text = resourceReference(R.string.disclaimer_title), startButton = AdditionalButton( iconRes = R.drawable.ic_back_24, onIconClicked = onBackClick, ).takeIf { state.isTosAccepted }, + textColor = textColor, + iconColor = iconColor, ) - DisclaimerContent(state.url) - Spacer( - modifier = Modifier - .background(TangemColorPalette.Dark6) - .height( - height = if (!state.isTosAccepted) { - bottomBarHeight - } else { - bottomBarHeight + TangemTheme.dimens.size64 - }, - ), - ) + DisclaimerContent(state.url, state.isTosAccepted) } if (!state.isTosAccepted) { - BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = TangemColorPalette.Dark6) + BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = backgroundColor) DisclaimerButton(state.onAccept) } else { NavigationBar3ButtonsScrim() @@ -76,10 +79,13 @@ internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { } } +@SuppressLint("SetJavaScriptEnabled") @Composable -private fun ColumnScope.DisclaimerContent(url: String) { +private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { val progressState = remember { mutableStateOf(ProgressState.Loading) } val webClient = remember { DisclaimerWebViewClient(progressState) } + val transparent = Color.Transparent + val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 Box( modifier = Modifier, ) { @@ -90,12 +96,17 @@ private fun ColumnScope.DisclaimerContent(url: String) { ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, ) - setBackgroundColor(TangemColorPalette.Dark6.toArgb()) + setBackgroundColor(transparent.toArgb()) settings.allowFileAccess = false - settings.javaScriptEnabled = false - + // to inject css style to display only in dark theme + settings.javaScriptEnabled = !isTosAccepted overScrollMode = View.OVER_SCROLL_NEVER webViewClient = webClient + + clearHistory() + clearFormData() + clearCache(true) + loadUrl(url) } }, @@ -103,7 +114,11 @@ private fun ColumnScope.DisclaimerContent(url: String) { when (progressState.value) { ProgressState.Loading -> { - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor), + ) { CircularProgressIndicator( color = TangemTheme.colors.icon.informative, modifier = Modifier @@ -112,25 +127,6 @@ private fun ColumnScope.DisclaimerContent(url: String) { ) } } - ProgressState.Error -> Box(modifier = Modifier.fillMaxSize()) { - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing16) - .align(Alignment.Center), - ) { - Text( - text = stringResource(id = R.string.disclaimer_error_loading), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.constantWhite, - ) - PrimaryButton( - text = stringResource(R.string.common_retry), - onClick = { webClient.reset() }, - ) - } - } else -> Unit } } @@ -147,6 +143,12 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { PrimaryButton( text = stringResource(id = R.string.common_accept), onClick = { onAccept(shouldAskPushPermission) }, + colors = TangemButtonColors( + backgroundColor = TangemColorPalette.Light4, + contentColor = TangemColorPalette.Dark6, + disabledBackgroundColor = TangemColorPalette.Light4, + disabledContentColor = TangemColorPalette.Dark6, + ), modifier = Modifier .align(Alignment.BottomCenter) .navigationBarsPadding() diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt index 348cad4107..a0b568833b 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt @@ -10,6 +10,22 @@ internal enum class ProgressState { Error, } +/** + * Workaround to display web view with ToS only in dark theme + */ +private fun WebView.injectCSS() { + val code = "javascript:(function() {" + + "var node = document.createElement('style');" + + "node.type = 'text/css';" + + " node.innerHTML = 'body, label,th,p,a, td, tr,li,ul,span,table,h1,h2,h3,h4,h5,h6,h7,div,small {" + + " color: #C9C9C9;" + + "background-color: #1E1E1E;" + + " } ';" + + " document.head.appendChild(node);})();" + + evaluateJavascript(code, null) +} + internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { private var loadingUrl: String? = null @@ -22,20 +38,23 @@ internal class DisclaimerWebViewClient(private val progressState: MutableState

Unit, + val onAllowed: () -> Unit, + val onDeny: () -> Unit, + val openSettings: () -> Unit, +) : TangemBottomSheetConfigContent \ 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 3dde33fa2b..0a72f0cf4e 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 @@ -37,6 +37,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.components.* import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet @@ -53,6 +54,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder +import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificationsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* @@ -204,17 +206,7 @@ private fun WalletContent( organizeTokens(state = selectedWallet, itemModifier = itemModifier) } - val bottomSheetConfig = selectedWallet.bottomSheetConfig - if (bottomSheetConfig != null) { - when (bottomSheetConfig.content) { - is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) - is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) - is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) - is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) - is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig) - is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig) - } - } + ShowBottomSheet(bottomSheetConfig = selectedWallet.bottomSheetConfig) WalletsListEffects( lazyListState = walletsListState, @@ -606,6 +598,21 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi } } +@Composable +private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { + if (bottomSheetConfig != null) { + when (bottomSheetConfig.content) { + is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) + is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) + is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) + is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) + is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig) + is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig) + is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig) + } + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt new file mode 100644 index 0000000000..8522dc3b72 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -0,0 +1,117 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.showcase.ShowcaseContent +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.requestPushPermission +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config = config) { + PushNotificationsSheetContent(content = it, onDismiss = config.onDismissRequest) + } +} + +@Composable +private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) { + val isClicked = remember { mutableStateOf(false) } + val requestPushPermission = requestPushPermission( + isFirstTimeAsking = content.isFirstTimeAsking, + isClicked = isClicked, + onAllow = { + content.onAllowed() + onDismiss() + }, + onDeny = { + content.onDeny() + onDismiss() + }, + onOpenSettings = content.openSettings, + ) + + Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + ShowcaseContent( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + iconRes = R.drawable.ic_rocket_launch_24, + text = resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + iconRes = R.drawable.ic_storefront_24, + text = resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing40), + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing40, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + SecondaryButton( + text = stringResource(R.string.common_later), + onClick = { + content.onDeny() + onDismiss() + }, + modifier = Modifier.weight(1f), + ) + PrimaryButton( + text = stringResource(R.string.common_allow), + onClick = { + isClicked.value = true + content.onAllow() + requestPushPermission() + }, + modifier = Modifier.weight(1f), + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PushNotificationsSheetContent_Preview() { + TangemThemePreview { + PushNotificationsSheetContent( + PushNotificationsBottomSheetConfig( + isFirstTimeAsking = false, + onAllow = {}, + onAllowed = {}, + onDeny = {}, + openSettings = {}, + ), + onDismiss = {}, + ) + } +} +// endregion \ 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 6d6961b577..c4242a3c48 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,11 +4,11 @@ import androidx.compose.runtime.MutableState import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled -import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.settings.* import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig @@ -28,6 +29,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSend import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.features.managetokens.navigation.ExpandableState +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -60,6 +62,10 @@ internal class WalletViewModel @Inject constructor( private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, + private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, + private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val settingsManager: SettingsManager, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -80,6 +86,7 @@ internal class WalletViewModel @Inject constructor( subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() subscribeToScreenBackgroundState() + subscribeOnPushNotificationsPermission() } private fun maybeMigrateNames() { @@ -145,6 +152,30 @@ internal class WalletViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun subscribeOnPushNotificationsPermission() { + viewModelScope.launch { + if (!shouldAskPermissionUseCase(PUSH_PERMISSION)) return@launch + withContext(dispatchers.io) { delay(timeMillis = 1_800) } + + val isFirstTimeAsking = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } + val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } + val onDenyClick: () -> Unit = if (wasInitiallyAsk) { + clickIntents::onDelayAskPushPermission + } else { + clickIntents::onNeverAskPushPermission + } + stateHolder.showBottomSheet( + PushNotificationsBottomSheetConfig( + isFirstTimeAsking = isFirstTimeAsking, + onAllow = clickIntents::onAllowPushPermission, + onAllowed = clickIntents::onNeverAskPushPermission, + onDeny = onDenyClick, + openSettings = settingsManager::openSettings, + ), + ) + } + } + private fun subscribeOnSelectedWalletFlow() { getSelectedWalletUseCase().onRight { it diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index 3266406422..712975635b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -30,6 +30,7 @@ internal class WalletClickIntents @Inject constructor( private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val visaWalletIntentsImplementor: VisaWalletIntentsImplementor, + private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor, private val stateHolder: WalletStateController, private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @@ -45,7 +46,8 @@ internal class WalletClickIntents @Inject constructor( WalletWarningsClickIntents by warningsClickIntentsImplementer, WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, WalletContentClickIntents by contentClickIntentsImplementor, - VisaWalletIntents by visaWalletIntentsImplementor { + VisaWalletIntents by visaWalletIntentsImplementor, + WalletPushPermissionClickIntents by pushPermissionClickIntentsImplementor { override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { super.initialize(router, coroutineScope) @@ -55,6 +57,7 @@ internal class WalletClickIntents @Inject constructor( currencyActionsClickIntentsImplementor.initialize(router, coroutineScope) contentClickIntentsImplementor.initialize(router, coroutineScope) visaWalletIntentsImplementor.initialize(router, coroutineScope) + pushPermissionClickIntentsImplementor.initialize(router, coroutineScope) } fun onWalletChange(index: Int) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt new file mode 100644 index 0000000000..3f5138ca6d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -0,0 +1,44 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.settings.DelayPermissionRequestUseCase +import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal interface WalletPushPermissionClickIntents { + + fun onAllowPushPermission() + + fun onDelayAskPushPermission() + + fun onNeverAskPushPermission() +} + +@ViewModelScoped +internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( + private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, + private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, +) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { + + override fun onAllowPushPermission() { + viewModelScope.launch { + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + } + + override fun onDelayAskPushPermission() { + viewModelScope.launch { + delayPermissionRequestUseCase(PUSH_PERMISSION) + } + } + + override fun onNeverAskPushPermission() { + viewModelScope.launch { + neverRequestPermissionUseCase(PUSH_PERMISSION) + } + } +} \ No newline at end of file From 9b665a76b340ccf9ff6c0646f5b1c45a3dc8b92f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jun 2024 12:30:53 +0500 Subject: [PATCH 069/223] Updated on 2026-08-14 --- .../tangem/core/ui/utils/RequestPushPermission.kt | 9 +++------ features/disclaimer/impl/build.gradle.kts | 1 + .../impl/presentation/ui/DisclaimerScreen.kt | 11 ++++------- .../pushnotifications/api/utils/PermissionUtils.kt | 12 +++++++++++- .../impl/presentation/ui/PushNotificationsScreen.kt | 2 ++ .../model/PushNotificationsBottomSheetConfig.kt | 2 +- .../ui/components/PushNotificationsBottomSheet.kt | 8 +++++--- .../wallet/viewmodels/WalletViewModel.kt | 7 ++++--- .../intents/WalletPushPermissionClickIntents.kt | 4 ++-- 9 files changed, 33 insertions(+), 23 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt index 134039ed07..575485aae9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt @@ -1,7 +1,5 @@ package com.tangem.core.ui.utils -import android.Manifest -import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -14,17 +12,18 @@ import com.google.accompanist.permissions.shouldShowRationale * Returns push permission requester. * Handles granting permission from app settings. */ +@Suppress("LongParameterList") @OptIn(ExperimentalPermissionsApi::class) @Composable fun requestPushPermission( isFirstTimeAsking: Boolean, + pushPermission: String?, isClicked: MutableState, onAllow: () -> Unit, onDeny: () -> Unit, onOpenSettings: () -> Unit, ): () -> Unit { - val permissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val permission = Manifest.permission.POST_NOTIFICATIONS + val permissionState = pushPermission?.let { permission -> val tempPermissionState = rememberPermissionState(permission = permission) rememberPermissionState(permission = permission) { when { @@ -33,8 +32,6 @@ fun requestPushPermission( else -> onDeny() } } - } else { - null } // Check if user granted permission and close bottom sheet diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index c82364e03f..b9bb0c5263 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { /** Feature modules */ implementation(projects.features.disclaimer.api) + implementation(projects.features.pushNotifications.api) /** DI */ implementation(deps.hilt.android) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt index 7b89328a8c..516eab4ef4 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt @@ -1,9 +1,7 @@ package com.tangem.features.disclaimer.impl.presentation.ui -import android.Manifest import android.annotation.SuppressLint import android.content.res.Configuration -import android.os.Build import android.view.View import android.view.ViewGroup import android.webkit.WebView @@ -37,6 +35,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.disclaimer.impl.R import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull @Composable internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { @@ -135,11 +134,9 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { @OptIn(ExperimentalPermissionsApi::class) @Composable private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { - val shouldAskPushPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS).status.isGranted - } else { - true - } + val shouldAskPushPermission = getPushPermissionOrNull()?.let { permission -> + rememberPermissionState(permission = permission).status.isGranted + } ?: true PrimaryButton( text = stringResource(id = R.string.common_accept), onClick = { onAccept(shouldAskPushPermission) }, diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt index 6092954f2a..c3d27e3ef5 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt @@ -2,9 +2,19 @@ package com.tangem.features.pushnotifications.api.utils import android.Manifest import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast -val PUSH_PERMISSION = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) +private val isRequirePushPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + +val PUSH_PERMISSION = if (isRequirePushPermission) { Manifest.permission.POST_NOTIFICATIONS } else { "android.permission.POST_NOTIFICATIONS" +} + +fun getPushPermissionOrNull() = if (isRequirePushPermission) { + Manifest.permission.POST_NOTIFICATIONS +} else { + null } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt index 02c7036aad..9fe0f81030 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.requestPushPermission import com.tangem.feature.pushnotifications.impl.R +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import kotlinx.collections.immutable.persistentListOf @Composable @@ -27,6 +28,7 @@ internal fun PushNotificationsScreen( onAllow = onAllow, onDeny = onLater, onOpenSettings = onOpenSettings, + pushPermission = getPushPermissionOrNull(), ) Showcase( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt index ed962bf9bd..71638bc6ce 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent data class PushNotificationsBottomSheetConfig( val isFirstTimeAsking: Boolean, + val onRequest: () -> Unit, val onAllow: () -> Unit, - val onAllowed: () -> Unit, val onDeny: () -> Unit, val openSettings: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index 8522dc3b72..15035df238 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -24,6 +24,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.requestPushPermission import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import kotlinx.collections.immutable.persistentListOf @Composable @@ -37,10 +38,11 @@ internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) { private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) { val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( + pushPermission = getPushPermissionOrNull(), isFirstTimeAsking = content.isFirstTimeAsking, isClicked = isClicked, onAllow = { - content.onAllowed() + content.onAllow() onDismiss() }, onDeny = { @@ -87,7 +89,7 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC text = stringResource(R.string.common_allow), onClick = { isClicked.value = true - content.onAllow() + content.onRequest() requestPushPermission() }, modifier = Modifier.weight(1f), @@ -105,8 +107,8 @@ private fun PushNotificationsSheetContent_Preview() { PushNotificationsSheetContent( PushNotificationsBottomSheetConfig( isFirstTimeAsking = false, + onRequest = {}, onAllow = {}, - onAllowed = {}, onDeny = {}, openSettings = {}, ), 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 c4242a3c48..131bc32cd4 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 @@ -155,7 +155,8 @@ internal class WalletViewModel @Inject constructor( private fun subscribeOnPushNotificationsPermission() { viewModelScope.launch { if (!shouldAskPermissionUseCase(PUSH_PERMISSION)) return@launch - withContext(dispatchers.io) { delay(timeMillis = 1_800) } + + delay(timeMillis = 1_800) val isFirstTimeAsking = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } @@ -167,8 +168,8 @@ internal class WalletViewModel @Inject constructor( stateHolder.showBottomSheet( PushNotificationsBottomSheetConfig( isFirstTimeAsking = isFirstTimeAsking, - onAllow = clickIntents::onAllowPushPermission, - onAllowed = clickIntents::onNeverAskPushPermission, + onRequest = clickIntents::onRequestPushPermission, + onAllow = clickIntents::onNeverAskPushPermission, onDeny = onDenyClick, openSettings = settingsManager::openSettings, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index 3f5138ca6d..d2f05b0f2a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -10,7 +10,7 @@ import javax.inject.Inject internal interface WalletPushPermissionClickIntents { - fun onAllowPushPermission() + fun onRequestPushPermission() fun onDelayAskPushPermission() @@ -24,7 +24,7 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, ) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { - override fun onAllowPushPermission() { + override fun onRequestPushPermission() { viewModelScope.launch { setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) } From 3a6668eee007d601d78fd658c7d9575bb42e670b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:26:18 +0400 Subject: [PATCH 070/223] Updated on 2026-08-14 --- .../java/com/tangem/tap/DecomposeFragment.kt | 4 +- .../tangem/tap/routing/utils/RouteMappers.kt | 2 +- .../core/decompose/di/DecomposeComponent.kt | 9 ++++ .../decompose/factory/ComponentFactory.kt | 4 +- .../core/decompose/model/ModelEntryPoint.kt | 29 +++++++++--- .../core/decompose/model/ParamsContainer.kt | 45 +++++++++++++++++++ features/details/api/build.gradle.kts | 3 -- .../details/component/DetailsComponent.kt | 8 +--- .../component/impl/DefaultDetailsComponent.kt | 8 +--- .../features/details/model/DetailsModel.kt | 13 ++++-- 10 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt diff --git a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt index d76bd6cd67..0a78c5558c 100644 --- a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt +++ b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt @@ -35,7 +35,7 @@ internal class DecomposeFragment : ComposeFragment() { ) } - private class ComponentBuilder>( + private class ComponentBuilder>( private val contextProvider: Provider, private val params: P, private val componentFactory: F, @@ -48,7 +48,7 @@ internal class DecomposeFragment : ComposeFragment() { private var componentBuilder: ComponentBuilder<*, *, *>? = null - fun > newInstance( + fun > newInstance( contextProvider: Provider, params: P, componentFactory: F, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt index c1203a4758..201f65ecad 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -22,7 +22,7 @@ internal fun AppRoute.asFragmentChild(fragmentProvider: Provider): Chi return Child.LegacyFragment(path, provider) } -internal fun > AppRoute.asComponentChild( +internal fun > AppRoute.asComponentChild( contextProvider: Provider, params: P, componentFactory: F, diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt index b5df451866..74fba5a48e 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.core.decompose.di +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import dagger.BindsInstance @@ -37,6 +38,14 @@ interface DecomposeComponent { */ fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder + /** + * Sets the parameters container for the component. + * + * @param container The parameters container to set. + * @return The builder instance. + */ + fun paramsContainer(@BindsInstance container: ParamsContainer): Builder + /** * Builds the Decompose component. * diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt index b365fcea59..20c1159324 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt @@ -2,7 +2,7 @@ package com.tangem.core.decompose.factory import com.tangem.core.decompose.context.AppComponentContext -interface ComponentFactory { +interface ComponentFactory

{ - fun create(context: AppComponentContext, params: Params): Component + fun create(context: AppComponentContext, params: P): C } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt index 58cad4cf7a..286678b4c3 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt @@ -22,24 +22,43 @@ interface ModelsEntryPoint { } /** - * Gets or creates a component's [Model]. + * Gets or creates a component's [Model] with no parameters. */ -inline fun AppComponentContext.getOrCreateModel(): M { - val modelKey = "model_${M::class.simpleName}" +inline fun AppComponentContext.getOrCreateModel(): M = getOrCreateModel(params = null) +/** + * Gets or creates a component's [Model]. + * + * Be careful with objects you pass in parameters as they will be used in [Model] lifecycle. + * If you pass a object with different lifecycle than the [Model] then you can face memory leaks. + * + * @param params The parameters to store in the [ParamsContainer], + + */ +inline fun AppComponentContext.getOrCreateModel(params: P?): M { val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") { val hiltComponent = hiltComponentBuilder .router(router) .uiMessageSender(messageSender) + .let { builder -> + if (params != null) { + val container = MutableParamsContainer(params) + + builder.paramsContainer(container) + } else { + builder + } + } .build() EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java) } + val modelKey = "model_${M::class.simpleName}" val model = instanceKeeper.getOrCreate(modelKey) { requireNotNull(entryPoint.models()[M::class.java]?.get()) { "Model ${M::class.simpleName} is not provided" - } + } as M } val isModelExist = tags.getOrElse(modelKey) { false } as Boolean @@ -47,5 +66,5 @@ inline fun AppComponentContext.getOrCreateModel(): M { tags[modelKey] = true } - return model as M + return model } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt new file mode 100644 index 0000000000..910764d661 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt @@ -0,0 +1,45 @@ +package com.tangem.core.decompose.model + +/** + * Lazy container for [Model] params. + * + * This contrainer can be accessed by DI because it's provided via [com.tangem.core.decompose.di.DecomposeComponent]. + * */ +interface ParamsContainer { + + /** Returns stored value if it is of type [T], otherwise returns null. */ + fun get(): T? + + /** Returns stored value if it is of type [T], otherwise throws an exception. */ + fun require(): T +} + +/** + * Mutable implementation of [ParamsContainer]. + * + * ***You should not use this class directly in other modules, use immutable [ParamsContainer] instead.*** + * */ +class MutableParamsContainer private constructor() : ParamsContainer { + + private var value: Any? = null + + /** Stores [value] inside a container, replaces any previous stored value. */ + fun set(value: Any) { + this.value = value + } + + @Suppress("UNCHECKED_CAST") + override fun get(): T? = value as? T + + override fun require(): T = get() ?: error("Contrainer is empty or contains a value of a different type.") + + companion object { + + /** Creates a new [MutableParamsContainer] and stores [value] inside it. */ + operator fun invoke(value: T): MutableParamsContainer { + return MutableParamsContainer().apply { + set(value) + } + } + } +} \ No newline at end of file diff --git a/features/details/api/build.gradle.kts b/features/details/api/build.gradle.kts index 103bd2c268..ce267fe5d1 100644 --- a/features/details/api/build.gradle.kts +++ b/features/details/api/build.gradle.kts @@ -16,7 +16,4 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - - /* AndroidX */ - implementation(deps.androidx.fragment.ktx) } \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index f53bc84d85..8634199b45 100644 --- a/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -1,18 +1,14 @@ package com.tangem.features.details.component -import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId interface DetailsComponent : ComposableContentComponent { - interface Factory : ComponentFactory { - - override fun create(context: AppComponentContext, params: Params): DetailsComponent - } + interface Factory : ComponentFactory data class Params( - val selectedUserWalletId: UserWalletId, + val userWalletId: UserWalletId, ) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt index 08d302a183..9a9c5575d1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt @@ -17,20 +17,16 @@ import dagger.assisted.AssistedInject internal class DefaultDetailsComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted private val params: DetailsComponent.Params, + @Assisted params: DetailsComponent.Params, userWalletListComponentFactory: UserWalletListComponent.Factory, ) : DetailsComponent, AppComponentContext by context { - private val model: DetailsModel = getOrCreateModel() + private val model: DetailsModel = getOrCreateModel(params) private val userWalletListComponent = userWalletListComponentFactory.create( context = child(key = "user_wallet_list"), ) - init { - model.provideUserWalletId(params.selectedUserWalletId) - } - @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 189172b899..f1bbd73bdb 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -3,9 +3,10 @@ package com.tangem.features.details.model import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsUM @@ -24,15 +25,19 @@ import timber.log.Timber import javax.inject.Inject @ComponentScoped +@Suppress("LongParameterList") internal class DetailsModel @Inject constructor( private val socialsBuilder: SocialsBuilder, private val itemsBuilder: ItemsBuilder, private val appVersionProvider: AppVersionProvider, private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, + private val paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { + private val params: DetailsComponent.Params = paramsContainer.require() + private val items: MutableStateFlow> = MutableStateFlow(value = persistentListOf()) val state: MutableStateFlow = MutableStateFlow( @@ -50,10 +55,12 @@ internal class DetailsModel @Inject constructor( items .onEach(::updateState) .launchIn(modelScope) + + checkWalletConnectAvailability() } - fun provideUserWalletId(userWalletId: UserWalletId) = modelScope.launch { - val isWalletConnectAvailable = checkIsWalletConnectAvailableUseCase(userWalletId).getOrElse { + private fun checkWalletConnectAvailability() = modelScope.launch { + val isWalletConnectAvailable = checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { Timber.w("Unable to check WalletConnect availability: $it") false From 25f7ea1644fe761f0ec90b4bc288ef24e6d89d07 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:30:30 +0400 Subject: [PATCH 071/223] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Dialogs.kt | 6 +- .../appbar/models/TopAppBarMedium.kt | 75 +++++++++++++++++++ .../core/ui/components/block}/BlockCard.kt | 9 ++- .../core/ui/components/block}/BlockItem.kt | 20 +++-- .../core/ui/components/block/model/BlockUM.kt | 16 ++++ .../features/details/entity/DetailsItemUM.kt | 8 +- .../features/details/ui/DetailsScreen.kt | 65 +++------------- .../details/ui/UserWalletListBlock.kt | 1 + .../features/details/ui/WalletConnectBlock.kt | 1 + .../features/details/utils/ItemsBuilder.kt | 41 ++++++---- 10 files changed, 154 insertions(+), 88 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt rename {features/details/impl/src/main/kotlin/com/tangem/features/details/ui => core/ui/src/main/java/com/tangem/core/ui/components/block}/BlockCard.kt (85%) rename {features/details/impl/src/main/kotlin/com/tangem/features/details/ui => core/ui/src/main/java/com/tangem/core/ui/components/block}/BlockItem.kt (61%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 18ca78091a..97a3b692cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -27,8 +27,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleDialogTextField -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -100,7 +100,7 @@ fun TextInputDialog( confirmButton: DialogButton, onDismissDialog: () -> Unit, onValueChange: (TextFieldValue) -> Unit, - textFieldParams: AdditionalTextInputDialogParams, + textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, title: String? = null, dismissButton: DialogButton? = null, isDismissable: Boolean = true, @@ -128,7 +128,7 @@ fun TextInputDialog( confirmButton: DialogButton, onDismissDialog: () -> Unit, onValueChange: (String) -> Unit, - textFieldParams: AdditionalTextInputDialogParams, + textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, title: String? = null, dismissButton: DialogButton? = null, isDismissable: Boolean = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt new file mode 100644 index 0000000000..d50a8f5c75 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt @@ -0,0 +1,75 @@ +package com.tangem.core.ui.components.appbar.models + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBarMedium( + title: TextReference, + scrollBehavior: TopAppBarScrollBehavior, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + navigationIconResId: Int = R.drawable.ic_back_24, + colors: TopAppBarColors = TangemTopAppBarColors, +) { + MediumTopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = colors, + title = { + val collapsedStyle = TangemTheme.typography.subtitle1 + val expandedStyle = TangemTheme.typography.h1 + val style by remember(scrollBehavior.state.collapsedFraction) { + derivedStateOf { + if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) { + collapsedStyle + } else { + expandedStyle + } + } + } + + Text( + text = title.resolveReference(), + style = style, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onBackClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = navigationIconResId), + contentDescription = null, + ) + } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +internal val TangemTopAppBarColors: TopAppBarColors + @Composable + @ReadOnlyComposable + get() = TopAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + scrolledContainerColor = TangemTheme.colors.background.secondary, + navigationIconContentColor = TangemTheme.colors.icon.primary1, + titleContentColor = TangemTheme.colors.text.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt similarity index 85% rename from features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt index e11a41c41e..24f64babdc 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt @@ -1,4 +1,4 @@ -package com.tangem.features.details.ui +package com.tangem.core.ui.components.block import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.Card @@ -9,9 +9,10 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme @Composable -internal fun BlockCard( +fun BlockCard( modifier: Modifier = Modifier, enabled: Boolean = true, + colors: CardColors = TangemBlockCardColors, onClick: () -> Unit = {}, content: @Composable ColumnScope.() -> Unit = {}, ) { @@ -19,13 +20,13 @@ internal fun BlockCard( modifier = modifier, onClick = onClick, shape = TangemTheme.shapes.roundedCornersXMedium, - colors = BlockColors, + colors = colors, enabled = enabled, content = content, ) } -private val BlockColors: CardColors +val TangemBlockCardColors: CardColors @Composable @ReadOnlyComposable get() = CardColors( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt similarity index 61% rename from features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt index 80d99b3b20..d729408a97 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.details.ui +package com.tangem.core.ui.components.block import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -11,12 +11,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.details.entity.DetailsItemUM @Composable -internal fun BlockItem(model: DetailsItemUM.Basic.Item, modifier: Modifier = Modifier) { +fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { BlockCard( modifier = modifier, onClick = model.onClick, @@ -29,14 +29,22 @@ internal fun BlockItem(model: DetailsItemUM.Basic.Item, modifier: Modifier = Mod Icon( modifier = Modifier.size(TangemTheme.dimens.size24), painter = painterResource(id = model.iconRes), - tint = TangemTheme.colors.icon.secondary, + tint = when (model.accentType) { + BlockUM.AccentType.NONE -> TangemTheme.colors.icon.secondary + BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent + BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning + }, contentDescription = null, ) Text( - text = model.title.resolveReference(), + text = model.text.resolveReference(), style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, + color = when (model.accentType) { + BlockUM.AccentType.NONE -> TangemTheme.colors.text.primary1 + BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent + BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt new file mode 100644 index 0000000000..df7e04af33 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.components.block.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +data class BlockUM( + val text: TextReference, + @DrawableRes val iconRes: Int, + val onClick: () -> Unit, + val accentType: AccentType = AccentType.NONE, +) { + + enum class AccentType { + NONE, ACCENT, WARNING, + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt index ea5da389e8..99c416a63a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt @@ -1,8 +1,7 @@ package com.tangem.features.details.entity -import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.block.model.BlockUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -17,10 +16,7 @@ internal sealed class DetailsItemUM { data class Item( val id: String, - val title: TextReference, - @DrawableRes - val iconRes: Int, - val onClick: () -> Unit, + val block: BlockUM, ) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 02c43a0396..7146703e70 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -15,11 +15,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.ComposableContentComponent +import com.tangem.core.ui.components.appbar.models.TopAppBarMedium +import com.tangem.core.ui.components.block.BlockItem import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -29,8 +30,6 @@ import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.impl.R -private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f - @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun DetailsScreen( @@ -52,7 +51,13 @@ internal fun DetailsScreen( hostState = LocalSnackbarHostState.current, ) }, - topBar = { TopBar(state, scrollBehavior) }, + topBar = { + TopAppBarMedium( + title = resourceReference(R.string.details_title), + scrollBehavior = scrollBehavior, + onBackClick = state.popBack, + ) + }, ) { paddingValues -> Content( modifier = Modifier.padding(paddingValues), @@ -62,54 +67,6 @@ internal fun DetailsScreen( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier) { - MediumTopAppBar( - modifier = modifier, - scrollBehavior = scrollBehavior, - colors = TopAppBarColors( - containerColor = TangemTheme.colors.background.secondary, - scrolledContainerColor = TangemTheme.colors.background.secondary, - navigationIconContentColor = TangemTheme.colors.icon.primary1, - titleContentColor = TangemTheme.colors.text.primary1, - actionIconContentColor = TangemTheme.colors.icon.primary1, - ), - title = { - val collapsedStyle = TangemTheme.typography.subtitle1 - val expandedStyle = TangemTheme.typography.h1 - val style by remember(scrollBehavior.state.collapsedFraction) { - derivedStateOf { - if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) { - collapsedStyle - } else { - expandedStyle - } - } - } - - Text( - text = stringResource(id = R.string.details_title), - style = style, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - navigationIcon = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = state.popBack, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - ) - } - }, - ) -} - @Composable private fun Content( state: DetailsUM, @@ -168,7 +125,7 @@ private fun Block( key(item.id) { BlockItem( modifier = itemModifier, - model = item, + model = item.block, ) } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 578e14df1f..cb235d20fa 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt index 11a28b3a75..b2e05c0504 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.impl.R diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index dbc63d89f5..6ce516821b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -6,6 +6,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.feedback.FeedbackManager import com.tangem.core.navigation.feedback.FeedbackType import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.features.details.entity.DetailsItemUM @@ -48,9 +49,11 @@ internal class ItemsBuilder @Inject constructor( items = persistentListOf( DetailsItemUM.Basic.Item( id = "buy_tangem_wallet", - title = resourceReference(R.string.details_buy_wallet), - iconRes = R.drawable.ic_tangem_24, - onClick = { urlOpener.openUrl(BUY_TANGEM_URL) }, + block = BlockUM( + text = resourceReference(R.string.details_buy_wallet), + iconRes = R.drawable.ic_tangem_24, + onClick = { urlOpener.openUrl(BUY_TANGEM_URL) }, + ), ), ), ) @@ -60,17 +63,21 @@ internal class ItemsBuilder @Inject constructor( items = buildList { DetailsItemUM.Basic.Item( id = "app_settings", - title = resourceReference(R.string.app_settings_title), - iconRes = R.drawable.ic_settings_24, - onClick = { router.push(AppRoute.AppSettings) }, + block = BlockUM( + text = resourceReference(R.string.app_settings_title), + iconRes = R.drawable.ic_settings_24, + onClick = { router.push(AppRoute.AppSettings) }, + ), ).let(::add) if (BuildConfig.TESTER_MENU_ENABLED) { DetailsItemUM.Basic.Item( id = "tester_menu", - title = stringReference(value = "Tester menu"), - iconRes = R.drawable.ic_alert_24, - onClick = { router.push(AppRoute.TesterMenu) }, + block = BlockUM( + text = stringReference(value = "Tester menu"), + iconRes = R.drawable.ic_alert_24, + onClick = { router.push(AppRoute.TesterMenu) }, + ), ).let(::add) } }.toImmutableList(), @@ -81,15 +88,19 @@ internal class ItemsBuilder @Inject constructor( items = persistentListOf( DetailsItemUM.Basic.Item( id = "send_feedback", - title = resourceReference(R.string.details_send_feedback), - iconRes = R.drawable.ic_comment_24, - onClick = { feedbackManager.sendEmail(FeedbackType.Feedback) }, + block = BlockUM( + text = resourceReference(R.string.details_send_feedback), + iconRes = R.drawable.ic_comment_24, + onClick = { feedbackManager.sendEmail(FeedbackType.Feedback) }, + ), ), DetailsItemUM.Basic.Item( id = "disclaimer", - title = resourceReference(R.string.disclaimer_title), - iconRes = R.drawable.ic_text_24, - onClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) }, + block = BlockUM( + text = resourceReference(R.string.disclaimer_title), + iconRes = R.drawable.ic_text_24, + onClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) }, + ), ), ), ) From 6b01eacf9da1f01c0fb417194a67e9622de3c0a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jun 2024 22:19:05 +0500 Subject: [PATCH 072/223] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 27 ++++++++++++++----- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 2 +- .../tokens/repository/CurrenciesRepository.kt | 7 +++-- .../repository/MockCurrenciesRepository.kt | 2 +- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ff644fd95c..110d28ddf6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -61,6 +61,18 @@ internal class DefaultCurrenciesRepository( private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( value = emptyMap(), ) + private val parallelTransactionsEnabledBlockchains = setOf( + Blockchain.Ethereum, + Blockchain.EthereumTestnet, + Blockchain.Polygon, + Blockchain.PolygonTestnet, + Blockchain.Arbitrum, + Blockchain.ArbitrumTestnet, + Blockchain.Binance, + Blockchain.BinanceTestnet, + Blockchain.Tron, + Blockchain.TronTestnet, + ) override suspend fun saveTokens( userWalletId: UserWalletId, @@ -366,18 +378,19 @@ internal class DefaultCurrenciesRepository( } } - override fun hasPendingTransactions( + override fun isSendBlockedByPendingTransactions( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, ): Boolean { val blockchain = Blockchain.fromId(cryptoCurrencyStatus.currency.network.id.value) val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet - - return if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain) { - val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } - outgoingTransactions.isNotEmpty() - } else { - coinStatus?.value?.hasCurrentNetworkTransactions == true + return when { + cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain -> { + val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } + outgoingTransactions.isNotEmpty() + } + parallelTransactionsEnabledBlockchains.contains(blockchain) -> false + else -> coinStatus?.value?.hasCurrentNetworkTransactions == true } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index ffef6ff04d..871067c3ec 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -261,7 +261,7 @@ class GetCryptoCurrencyActionsUseCase( cryptoCurrencyStatus.value.amount.isNullOrZero() -> { ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) } - currenciesRepository.hasPendingTransactions( + currenciesRepository.isSendBlockedByPendingTransactions( cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus, ) -> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 5bdf7b4067..782c16153e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -194,12 +194,15 @@ interface CurrenciesRepository { fun getMissedAddressesCryptoCurrencies(userWalletId: UserWalletId): Flow> /** - * Determines whether the currency has pending transaction or currency network has pending transaction + * Determines whether the currency sending is blocked by network pending transaction * * @param cryptoCurrencyStatus currency status * @param coinStatus main currency status in [cryptoCurrencyStatus] network */ - fun hasPendingTransactions(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean + fun isSendBlockedByPendingTransactions( + cryptoCurrencyStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus?, + ): Boolean /** * Retrieves fee paid currency for specific [currency]. diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index d4d616d88a..e81b9e83d1 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -122,7 +122,7 @@ internal class MockCurrenciesRepository( return isSortedByBalance.map { it.getOrElse { e -> throw e } } } - override fun hasPendingTransactions( + override fun isSendBlockedByPendingTransactions( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, ): Boolean { From b192949d27f8016979710446b493d9a13c6d34fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jun 2024 17:58:31 +0500 Subject: [PATCH 073/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 3 +- core/res/src/main/res/values/strings.xml | 6 +- .../segmentedbutton/SegmentedButton.kt | 14 ++- .../ui/components/rows/SelectorRowItem.kt | 3 +- .../com/tangem/core/ui/res/TangemDimens.kt | 2 + .../src/main/java/com/tangem/utils/Strings.kt | 1 + .../presentation/ui/fee/SendSpeedSelector.kt | 2 +- .../impl/presentation/ui/send/FeeBlock.kt | 2 +- .../tokendetails/TokenDetailsPreviewData.kt | 60 ++++++++++-- .../tokendetails/state/StakingBlockState.kt | 26 ++++- .../TokenBalanceSegmentedButtonConfig.kt | 13 +++ .../state/TokenDetailsBalanceBlockState.kt | 10 ++ .../tokendetails/state/TokenDetailsState.kt | 2 +- .../TokenDetailsLoadedBalanceConverter.kt | 24 ++++- .../TokenDetailsSkeletonStateConverter.kt | 24 ++++- .../state/factory/TokenDetailsStateFactory.kt | 19 +++- .../factory/TokenStakingStateConverter.kt | 34 ++++--- .../tokendetails/ui/TokenDetailsScreen.kt | 29 +++++- .../ui/components/TokenDetailsBalanceBlock.kt | 40 +++++++- .../components/staking/StakingBalanceBlock.kt | 98 +++++++++++++++++++ .../{ => staking}/TokenStakingBlock.kt | 38 +++---- .../viewmodels/TokenDetailsClickIntents.kt | 3 + .../viewmodels/TokenDetailsViewModel.kt | 10 +- .../VisaTxHistoryItemStateConverter.kt | 3 +- 24 files changed, 387 insertions(+), 79 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt rename features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/{ => staking}/TokenStakingBlock.kt (86%) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 7ad7dd2099..8c1d366c83 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -84,6 +84,7 @@ Копировать Скопировать адрес Создать + Свое Удалить Отключено Готово @@ -95,7 +96,6 @@ Обозреватель Комиссия Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s - Свое Быстро По рынку Медленно @@ -302,6 +302,7 @@ %1$d из %2$d кошельков %1$d из %2$d кошельков + Удалить например Bitcoin Ваш портфель был обновлен Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 752c42de5a..38c7f41345 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -67,6 +67,7 @@ Not enough ADA Accept Access denied + All Allow Apply Approval @@ -84,6 +85,7 @@ Copy Copy address Create + Custom Delete Disabled Done @@ -95,7 +97,6 @@ Explorer Fee Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s - Custom Fast Market Slow @@ -304,6 +305,7 @@ %1$d of %2$d wallet %1$d of %2$d wallets + Remove e.g. BTC I trust, hodl I must Your portfolio has been updated The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. @@ -562,9 +564,11 @@ On stake Reward claiming Reward schedule + Rewards to claim: %s Staking %s Unbonding Period Warmup period + Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards Rewards diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index 0bfeb822a6..1101935641 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -8,10 +8,7 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -21,8 +18,9 @@ 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.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -42,7 +40,7 @@ import kotlinx.collections.immutable.persistentListOf */ @Composable inline fun SegmentedButtons( - config: PersistentList, + config: ImmutableList, crossinline onClick: (T) -> Unit, modifier: Modifier = Modifier, color: Color = TangemTheme.colors.background.tertiary, @@ -51,7 +49,7 @@ inline fun SegmentedButtons( showIndication: Boolean = true, initialSelectedItem: T? = null, isEnabled: Boolean = true, - crossinline buttonContent: @Composable (T) -> Unit, + crossinline buttonContent: @Composable BoxScope.(T) -> Unit, ) { if (config.isEmpty() || config.size == 1) return @@ -97,7 +95,7 @@ inline fun SegmentedButtons( onClick(config[index]) }, ) { - buttonContent.invoke(config[index]) + buttonContent.invoke(this, config[index]) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index c1e06e7270..5eb6b5f287 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.utils.Strings @Composable fun SelectorRowItem( @@ -127,7 +128,7 @@ private fun RowScope.SelectorValueContent( ) if (postDot != null) { Text( - text = "•", + text = Strings.DOT, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, 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 cc51084e66..2ad32a8f6f 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,9 +102,11 @@ data class TangemDimens internal constructor( val spacing2: Dp = 2.dp, val spacing3: Dp = 3.dp, val spacing4: Dp = 4.dp, + val spacing5: Dp = 5.dp, val spacing6: Dp = 6.dp, val spacing8: Dp = 8.dp, val spacing10: Dp = 10.dp, + val spacing11: Dp = 11.dp, val spacing12: Dp = 12.dp, val spacing14: Dp = 14.dp, val spacing15: Dp = 15.dp, diff --git a/core/utils/src/main/java/com/tangem/utils/Strings.kt b/core/utils/src/main/java/com/tangem/utils/Strings.kt index 59adbdab5b..f6cc72a41a 100644 --- a/core/utils/src/main/java/com/tangem/utils/Strings.kt +++ b/core/utils/src/main/java/com/tangem/utils/Strings.kt @@ -3,4 +3,5 @@ package com.tangem.utils object Strings { const val STARS = "\u2217\u2217\u2217" + const val DOT = "•" } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index f403cf423b..096fbe1d8b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -63,7 +63,7 @@ internal fun SendSpeedSelector( onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) }, ) SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_custom, + titleRes = R.string.common_custom, iconRes = R.drawable.ic_edit_24, feeType = FeeType.Custom, state = state, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 6873ed2b63..11c4f4ebab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -53,7 +53,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24 FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 - FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24 + FeeType.Custom -> R.string.common_custom to R.drawable.ic_edit_24 } } else { R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 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 f99dbb1cd3..79b16936c9 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 @@ -9,7 +9,9 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton @@ -18,6 +20,7 @@ import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow +@Suppress("LargeClass") internal object TokenDetailsPreviewData { val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig( @@ -100,17 +103,47 @@ internal object TokenDetailsPreviewData { TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) - val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons) + private val balanceSegmentedButtonConfig = persistentListOf( + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.common_all), + type = BalanceType.ALL, + ), + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.staking_details_available), + type = BalanceType.AVAILABLE, + ), + ) + + val balanceLoading = TokenDetailsBalanceBlockState.Loading( + actionButtons = actionButtons, + balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, + ) val balanceContent = TokenDetailsBalanceBlockState.Content( actionButtons = actionButtons, fiatBalance = "91,50$", cryptoBalance = "966,96 XLM", + isStakingEnabled = true, + balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, + onBalanceSelect = {}, + ) + val balanceError = TokenDetailsBalanceBlockState.Error( + actionButtons = actionButtons, + balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, ) - val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons) private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT") - private val stakingLoading = StakingBlockState.Loading(iconState = iconState) + private val stakingLoading = StakingBlocksState( + stakingAvailable = StakingAvailable.Loading(iconState), + stakingBalance = StakingBalance.Content( + cryptoAmount = stringReference("5 SOL"), + fiatAmount = stringReference("456.34 $"), + rewardAmount = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + ), + ) private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( isRefreshing = false, @@ -246,7 +279,7 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, - stakingBlockState = stakingLoading, + stakingBlocksState = stakingLoading, notifications = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -278,12 +311,19 @@ internal object TokenDetailsPreviewData { type = PriceChangeType.UP, ), ), - stakingBlockState = StakingBlockState.Content( - interestRate = "7.38", - periodInDays = 4, - tokenSymbol = "XLM", - iconState = iconState, - onStakeClicked = {}, + stakingBlocksState = StakingBlocksState( + stakingAvailable = StakingAvailable.Content( + interestRate = "7.38", + periodInDays = 4, + tokenSymbol = "XLM", + iconState = iconState, + onStakeClicked = {}, + ), + stakingBalance = StakingBalance.Content( + cryptoAmount = stringReference("5 SOL"), + fiatAmount = stringReference("456.34 $"), + rewardAmount = resourceReference(R.string.staking_details_rewards_to_claim, wrappedList("0.43 $")), + ), ), notifications = persistentListOf(), txHistoryState = TxHistoryState.NotSupported( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt index 76d9483c9b..6b5802a796 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -1,15 +1,20 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +internal data class StakingBlocksState( + val stakingAvailable: StakingAvailable, + val stakingBalance: StakingBalance, +) @Immutable -internal sealed interface StakingBlockState { - +internal sealed interface StakingAvailable { val iconState: IconState - data class Error(override val iconState: IconState) : StakingBlockState + data class Error(override val iconState: IconState) : StakingAvailable - data class Loading(override val iconState: IconState) : StakingBlockState + data class Loading(override val iconState: IconState) : StakingAvailable data class Content( override val iconState: IconState, @@ -17,5 +22,16 @@ internal sealed interface StakingBlockState { val periodInDays: Int, val tokenSymbol: String, val onStakeClicked: () -> Unit, - ) : StakingBlockState + ) : StakingAvailable +} + +@Immutable +sealed class StakingBalance { + data object Empty : StakingBalance() + + data class Content( + val cryptoAmount: TextReference, + val fiatAmount: TextReference, + val rewardAmount: TextReference, + ) : StakingBalance() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt new file mode 100644 index 0000000000..d741d3e609 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.ui.extensions.TextReference + +data class TokenBalanceSegmentedButtonConfig( + val title: TextReference, + val type: BalanceType, +) + +enum class BalanceType { + ALL, + AVAILABLE, +} \ 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 index 2f066dae01..21ee681241 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -6,19 +6,29 @@ import kotlinx.collections.immutable.ImmutableList internal sealed class TokenDetailsBalanceBlockState { abstract val actionButtons: ImmutableList + abstract val balanceSegmentedButtonConfig: ImmutableList + abstract val selectedBalanceType: BalanceType data class Loading( override val actionButtons: ImmutableList, + override val balanceSegmentedButtonConfig: ImmutableList, + override val selectedBalanceType: BalanceType, ) : TokenDetailsBalanceBlockState() data class Content( override val actionButtons: ImmutableList, + override val balanceSegmentedButtonConfig: ImmutableList, + override val selectedBalanceType: BalanceType, val fiatBalance: String, val cryptoBalance: String, + val isStakingEnabled: Boolean, + val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit, ) : TokenDetailsBalanceBlockState() data class Error( override val actionButtons: ImmutableList, + override val balanceSegmentedButtonConfig: ImmutableList, + override val selectedBalanceType: BalanceType, ) : TokenDetailsBalanceBlockState() fun copyActionButtons(buttons: ImmutableList): TokenDetailsBalanceBlockState { 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 d1ec291d51..b9594669da 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 @@ -17,7 +17,7 @@ internal data class TokenDetailsState( val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, - val stakingBlockState: StakingBlockState, + val stakingBlocksState: StakingBlocksState, val notifications: ImmutableList, val pendingTxs: PersistentList, val swapTxs: PersistentList, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 63d735d3ad..87e2d83683 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType 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.components.TokenDetailsNotification @@ -23,6 +24,7 @@ import kotlinx.collections.immutable.toPersistentList internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val isStakingEnabled: Boolean, private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, @@ -39,7 +41,11 @@ internal class TokenDetailsLoadedBalanceConverter( private fun convertError(): TokenDetailsState { val state = currentStateProvider() return state.copy( - tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons), + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error( + actionButtons = state.tokenBalanceBlockState.actionButtons, + balanceSegmentedButtonConfig = state.tokenBalanceBlockState.balanceSegmentedButtonConfig, + selectedBalanceType = state.tokenBalanceBlockState.selectedBalanceType, + ), marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencySymbol), notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable), ) @@ -74,12 +80,24 @@ internal class TokenDetailsLoadedBalanceConverter( actionButtons = currentState.actionButtons, fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), cryptoBalance = formatCryptoAmount(status), + isStakingEnabled = isStakingEnabled, + balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, + onBalanceSelect = clickIntents::onBalanceSelect, + selectedBalanceType = currentState.selectedBalanceType, + ) + is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading( + actionButtons = currentState.actionButtons, + balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, ) - is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(currentState.actionButtons) is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.Unreachable, is CryptoCurrencyStatus.NoAmount, - -> TokenDetailsBalanceBlockState.Error(currentState.actionButtons) + -> TokenDetailsBalanceBlockState.Error( + actionButtons = currentState.actionButtons, + balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, + ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 9cc42f3c2d..3268e37904 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -48,9 +48,16 @@ internal class TokenDetailsSkeletonStateConverter( ) }, ), - tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()), + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading( + actionButtons = createButtons(), + balanceSegmentedButtonConfig = createBalanceSegmentedButtonConfig(), + selectedBalanceType = BalanceType.ALL, + ), marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), - stakingBlockState = StakingBlockState.Loading(iconState = iconState), + stakingBlocksState = StakingBlocksState( + stakingAvailable = StakingAvailable.Loading(iconState), + stakingBalance = StakingBalance.Empty, + ), notifications = persistentListOf(), pendingTxs = persistentListOf(), swapTxs = persistentListOf(), @@ -99,6 +106,19 @@ internal class TokenDetailsSkeletonStateConverter( ) } + private fun createBalanceSegmentedButtonConfig(): ImmutableList { + return persistentListOf( + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.common_all), + type = BalanceType.ALL, + ), + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.staking_details_available), + type = BalanceType.AVAILABLE, + ), + ) + } + private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig( isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 9f8de13d25..54210912e8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -24,9 +24,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter @@ -46,6 +44,7 @@ internal class TokenDetailsStateFactory( private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, + private val isStakingEnabled: Boolean, symbol: String, decimals: Int, ) { @@ -65,6 +64,7 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, + isStakingEnabled = isStakingEnabled, symbol = symbol, decimals = decimals, clickIntents = clickIntents, @@ -218,7 +218,7 @@ internal class TokenDetailsStateFactory( fun getStateWithStaking(stakingEither: Either): TokenDetailsState { return currentStateProvider().copy( - stakingBlockState = stakingStateConverter.convert(stakingEither), + stakingBlocksState = stakingStateConverter.convert(stakingEither), ) } @@ -348,6 +348,17 @@ internal class TokenDetailsStateFactory( } } + fun getStateWithUpdatedBalanceSegmentedButtonConfig( + buttonConfig: TokenBalanceSegmentedButtonConfig, + ): TokenDetailsState { + return with(currentStateProvider()) { + val updatedState = (tokenBalanceBlockState as? TokenDetailsBalanceBlockState.Content) + ?.copy(selectedBalanceType = buttonConfig.type) + ?: tokenBalanceBlockState + copy(tokenBalanceBlockState = updatedState) + } + } + private fun TokenDetailsAppBarMenuConfig.updateMenu( cardTypesResolver: CardTypesResolver, isBitcoin: Boolean, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 0bdf52927f..29fe830039 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -3,7 +3,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlocksState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider @@ -12,25 +14,31 @@ import com.tangem.utils.converter.Converter internal class TokenStakingStateConverter( private val clickIntents: TokenDetailsClickIntents, private val currentStateProvider: Provider, -) : Converter, StakingBlockState> { +) : Converter, StakingBlocksState> { - override fun convert(value: Either): StakingBlockState { + override fun convert(value: Either): StakingBlocksState { value.fold( ifLeft = { - return StakingBlockState.Error( - iconState = currentStateProvider().tokenInfoBlockState.iconState, + return StakingBlocksState( + stakingAvailable = StakingAvailable.Error( + iconState = currentStateProvider().tokenInfoBlockState.iconState, + ), + stakingBalance = StakingBalance.Empty, ) }, ifRight = { - return StakingBlockState.Content( - interestRate = BigDecimalFormatter.formatPercent( - percent = it.interestRate, - useAbsoluteValue = true, + return StakingBlocksState( + stakingAvailable = StakingAvailable.Content( + interestRate = BigDecimalFormatter.formatPercent( + percent = it.interestRate, + useAbsoluteValue = true, + ), + periodInDays = it.periodInDays, + tokenSymbol = it.tokenSymbol, + iconState = currentStateProvider().tokenInfoBlockState.iconState, + onStakeClicked = clickIntents::onStakeBannerClick, ), - periodInDays = it.periodInDays, - tokenSymbol = it.tokenSymbol, - iconState = currentStateProvider().tokenInfoBlockState.iconState, - onStakeClicked = clickIntents::onStakeBannerClick, + stakingBalance = StakingBalance.Empty, ) }, ) 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 e4574897ac..f076f05240 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 @@ -41,10 +41,10 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.* import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar @@ -52,6 +52,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.StakingBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") @@ -140,10 +142,27 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { if (state.isStakingBlockShown) { item( - key = StakingBlockState::class.java, - contentType = StakingBlockState::class.java, - content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) }, + key = StakingAvailable::class.java, + contentType = StakingAvailable::class.java, + content = { + TokenStakingBlock( + modifier = itemModifier, + state = state.stakingBlocksState.stakingAvailable, + ) + }, ) + if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) { + item( + key = StakingBalance::class.java, + contentType = StakingBalance::class.java, + content = { + StakingBalanceBlock( + state = state.stakingBlocksState.stakingBalance, + modifier = itemModifier, + ) + }, + ) + } } swapTransactionsItems( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 06d871b579..081fab085e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -13,6 +13,8 @@ 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.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -35,20 +37,23 @@ internal fun TokenDetailsBalanceBlock( color = TangemTheme.colors.background.primary, ) { Column { - Box( + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12) .padding(horizontal = TangemTheme.dimens.spacing12) .fillMaxWidth() .heightIn(min = TangemTheme.dimens.spacing24), - contentAlignment = Alignment.CenterStart, ) { Text( text = stringResource(id = R.string.common_balance_title), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, maxLines = 1, + modifier = Modifier + .weight(1f) + .padding(top = TangemTheme.dimens.spacing12), ) + BalanceButtons(state) } FiatBalance( state = state, @@ -130,6 +135,35 @@ private fun CryptoBalance( } } +@Composable +private fun BalanceButtons(state: TokenDetailsBalanceBlockState) { + if (state !is TokenDetailsBalanceBlockState.Content) return + + SegmentedButtons( + config = state.balanceSegmentedButtonConfig, + onClick = state.onBalanceSelect, + showIndication = false, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing11) + .width(IntrinsicSize.Min), + ) { config -> + Text( + text = config.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.caption1, + maxLines = 1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing5, + end = TangemTheme.dimens.spacing5, + top = TangemTheme.dimens.spacing3, + bottom = TangemTheme.dimens.spacing3, + ) + .align(Alignment.Center), + ) + } +} + @Preview(widthDp = 328, heightDp = 152) @Preview(widthDp = 328, heightDp = 152, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt new file mode 100644 index 0000000000..4a2929a40d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -0,0 +1,98 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.Strings + +@Composable +fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.staking_native), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing2), + ) + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + Text( + text = state.fiatAmount.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = Strings.DOT, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.cryptoAmount.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + Text( + text = state.rewardAmount.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingBalanceBlock_Preview( + @PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBalance, +) { + TangemThemePreview { + StakingBalanceBlock( + state = data as StakingBalance.Content, + modifier = Modifier.padding(TangemTheme.dimens.spacing16), + ) + } +} + +private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TokenDetailsPreviewData.tokenDetailsState_1.stakingBlocksState.stakingBalance, + TokenDetailsPreviewData.tokenDetailsState_2.stakingBlocksState.stakingBalance, + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt similarity index 86% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index d888903a28..e9f66e62ab 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -1,11 +1,12 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -14,14 +15,17 @@ 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.* -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA import com.tangem.core.ui.utils.GrayscaleColorFilter import com.tangem.core.ui.utils.NORMAL_ALPHA import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon import com.tangem.features.tokendetails.impl.R /** @@ -31,7 +35,7 @@ import com.tangem.features.tokendetails.impl.R * @param modifier modifier */ @Composable -internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) { +internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Modifier) { Column( modifier = modifier .background( @@ -49,7 +53,7 @@ internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Mo } @Composable -private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) { +private fun Content(state: StakingAvailable, modifier: Modifier = Modifier) { AnimatedContent( modifier = modifier.heightIn(min = TangemTheme.dimens.size60), targetState = state, @@ -57,24 +61,24 @@ private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) { label = "Update the content", ) { stakingBlockState -> when (stakingBlockState) { - is StakingBlockState.Content -> { + is StakingAvailable.Content -> { StakingContent( stakingBlockState = stakingBlockState, iconState = stakingBlockState.iconState, ) } - is StakingBlockState.Loading -> { + is StakingAvailable.Loading -> { StakingLoading( iconState = stakingBlockState.iconState, ) } - is StakingBlockState.Error -> Row {} // TODO staking + is StakingAvailable.Error -> Row {} // TODO staking } } } @Composable -private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) { +private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconState: IconState) { Column { Row { val (alpha, colorFilter) = remember(iconState.isGrayscale) { @@ -177,24 +181,24 @@ private fun StakingLoading(iconState: IconState) { @Composable private fun Preview_TokenStakingBlock( @PreviewParameter(StakingBlockStateProvider::class) - state: StakingBlockState, + state: StakingAvailable, ) { TangemThemePreview { TokenStakingBlock(state = state) } } -private class StakingBlockStateProvider : CollectionPreviewParameterProvider( +private class StakingBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - StakingBlockState.Content( + StakingAvailable.Content( iconState = iconState, interestRate = "10", periodInDays = 4, tokenSymbol = "SOL", onStakeClicked = {}, ), - StakingBlockState.Loading(iconState = iconState), - StakingBlockState.Error(iconState = iconState), + StakingAvailable.Loading(iconState = iconState), + StakingAvailable.Error(iconState = iconState), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 51b5adda0c..9332738ab0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig @Suppress("TooManyFunctions") interface TokenDetailsClickIntents { @@ -59,4 +60,6 @@ interface TokenDetailsClickIntents { fun onAssociateClick() fun onStakeBannerClick() + + fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) } \ 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 09a908e65f..333fdc48e6 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 @@ -63,6 +63,7 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig @@ -125,12 +126,12 @@ internal class TokenDetailsViewModel @Inject constructor( ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY) - ?.let { it.unbundle(UserWalletId.serializer()) } + ?.unbundle(UserWalletId.serializer()) ?: error("This screen can't open without `UserWalletId`") private val cryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) - ?.let { it.unbundle(CryptoCurrency.serializer()) } + ?.unbundle(CryptoCurrency.serializer()) ?: error("This screen can't open without `CryptoCurrency`") private val userWallet: UserWallet @@ -154,6 +155,7 @@ internal class TokenDetailsViewModel @Inject constructor( symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, featureToggles = tokenDetailsFeatureToggles, + isStakingEnabled = stakingFeatureToggles.isStakingEnabled, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -787,6 +789,10 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) { + uiState = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) + } + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 5c1b0a20c5..819d0d20d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -9,6 +9,7 @@ import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.VisaWalletIntents +import com.tangem.utils.Strings import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -20,7 +21,7 @@ internal class VisaTxHistoryItemStateConverter( override fun convert(value: VisaTxHistoryItem): TransactionState { val localDate = value.date.withZone(DateTimeZone.getDefault()) val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter) - val subtitle = "$time • ${value.status.capitalize()}" + val subtitle = "$time ${Strings.DOT} ${value.status.capitalize()}" return TransactionState.Content( txHash = value.id, From 36871cc5c9664ba3f06f0557009a2e408bf4471e Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 23 Jun 2024 23:36:32 +0400 Subject: [PATCH 074/223] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../java/com/tangem/tap/DecomposeFragment.kt | 20 +++++-- .../routing/impl/DefaultRoutingComponent.kt | 5 ++ .../tangem/tap/routing/utils/ChildFactory.kt | 4 ++ .../tangem/tap/routing/utils/RouteMappers.kt | 3 +- .../ComposableContentComponent.kt | 2 +- .../ui/decompose/ComposableDialogComponent.kt | 14 +++++ .../details/component/DetailsComponent.kt | 2 +- .../component/UserWalletListComponent.kt | 2 +- .../features/details/ui/DetailsScreen.kt | 2 +- features/wallet-settings/api/.gitignore | 1 + features/wallet-settings/api/build.gradle.kts | 19 +++++++ .../component/RenameWalletComponent.kt | 16 ++++++ .../component/WalletSettingsComponent.kt | 14 +++++ features/wallet-settings/impl/.gitignore | 1 + .../wallet-settings/impl/build.gradle.kts | 52 +++++++++++++++++++ settings.gradle.kts | 3 ++ 17 files changed, 152 insertions(+), 10 deletions(-) rename core/ui/src/main/java/com/tangem/core/ui/{ => decompose}/ComposableContentComponent.kt (88%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt create mode 100644 features/wallet-settings/api/.gitignore create mode 100644 features/wallet-settings/api/build.gradle.kts create mode 100644 features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt create mode 100644 features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt create mode 100644 features/wallet-settings/impl/.gitignore create mode 100644 features/wallet-settings/impl/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fa383da707..c6f66b3c04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -140,6 +140,8 @@ dependencies { implementation(projects.features.disclaimer.impl) implementation(projects.features.pushNotifications.api) implementation(projects.features.pushNotifications.impl) + implementation(projects.features.walletSettings.api) + implementation(projects.features.walletSettings.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt index 0a78c5558c..296704a5e1 100644 --- a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt +++ b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt @@ -2,15 +2,17 @@ package com.tangem.tap import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.ComposableContentComponent import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.message.EventMessageEffect import com.tangem.core.ui.screen.ComposeFragment import com.tangem.utils.Provider import dagger.hilt.android.AndroidEntryPoint +import java.util.WeakHashMap import javax.inject.Inject @AndroidEntryPoint @@ -20,7 +22,10 @@ internal class DecomposeFragment : ComposeFragment() { override lateinit var uiDependencies: UiDependencies private val component by lazy(mode = LazyThreadSafetyMode.NONE) { - requireNotNull(componentBuilder?.build()) { + val tag = requireArguments().getString(TAG_KEY) + val builder = componentsBuilders[tag] + + requireNotNull(builder?.build()) { "Component builder is not set, call newInstance() for DecomposeFragment creation first." } } @@ -46,16 +51,21 @@ internal class DecomposeFragment : ComposeFragment() { companion object { - private var componentBuilder: ComponentBuilder<*, *, *>? = null + private const val TAG_KEY = "tag" + + private val componentsBuilders = WeakHashMap>() fun > newInstance( + tag: String, contextProvider: Provider, params: P, componentFactory: F, ): Fragment { - this@Companion.componentBuilder = ComponentBuilder(contextProvider, params, componentFactory) + this@Companion.componentsBuilders[tag] = ComponentBuilder(contextProvider, params, componentFactory) - return DecomposeFragment() + return DecomposeFragment().apply { + this.arguments = bundleOf(TAG_KEY to tag) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt index ece22bfeb5..e766371cb7 100644 --- a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt @@ -5,6 +5,7 @@ import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.lifecycle.doOnDestroy import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext @@ -34,6 +35,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor( init { backHandler.register(backCallback) + + lifecycle.doOnDestroy { + childFactory.doOnDestroy() + } } private fun getInitialRoute(): AppRoute = AppRoute.Initial diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 3a6bcba601..dc76ad9db0 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -163,6 +163,10 @@ internal class ChildFactory @Inject constructor( } } + fun doOnDestroy() { + componentContexts.clear() + } + private fun contextProvider( appRoute: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext, diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt index 201f65ecad..bdade79e4e 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.ComposableContentComponent +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.tap.DecomposeFragment import com.tangem.tap.routing.RoutingComponent.Child import com.tangem.utils.Provider @@ -29,6 +29,7 @@ internal fun Unit + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun Dialog() +} \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt index 8634199b45..f2546ffc26 100644 --- a/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -1,7 +1,7 @@ package com.tangem.features.details.component import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.ComposableContentComponent +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.wallets.models.UserWalletId interface DetailsComponent : ComposableContentComponent { diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt index 37791d71d0..7908aa5ede 100644 --- a/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt @@ -1,7 +1,7 @@ package com.tangem.features.details.component import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.ComposableContentComponent +import com.tangem.core.ui.decompose.ComposableContentComponent interface UserWalletListComponent : ComposableContentComponent { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 7146703e70..c1db3e35bd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -16,10 +16,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.ComposableContentComponent import com.tangem.core.ui.components.appbar.models.TopAppBarMedium import com.tangem.core.ui.components.block.BlockItem import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme diff --git a/features/wallet-settings/api/.gitignore b/features/wallet-settings/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/wallet-settings/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/wallet-settings/api/build.gradle.kts b/features/wallet-settings/api/build.gradle.kts new file mode 100644 index 0000000000..7c48ae5a59 --- /dev/null +++ b/features/wallet-settings/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.walletsettings.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt new file mode 100644 index 0000000000..4d3b21b9f1 --- /dev/null +++ b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.walletsettings.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface RenameWalletComponent : ComposableDialogComponent { + + data class Params( + val userWalletId: UserWalletId, + val currentName: String, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt new file mode 100644 index 0000000000..2c8cdd889a --- /dev/null +++ b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.walletsettings.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface WalletSettingsComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/wallet-settings/impl/.gitignore b/features/wallet-settings/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/wallet-settings/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts new file mode 100644 index 0000000000..a94b469537 --- /dev/null +++ b/features/wallet-settings/impl/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.walletsettings.impl" +} + +dependencies { + + /* Project - API */ + implementation(projects.features.walletSettings.api) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.analytics.models) + implementation(projects.common.routing) + + /* Project - Domain */ + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + + /* AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /* Compose */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) + implementation(deps.decompose.ext.compose) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* Other */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index acbedb8b55..14ba5ff49e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -183,6 +183,9 @@ include(":features:disclaimer:impl") include(":features:push-notifications:api") include(":features:push-notifications:impl") + +include(":features:wallet-settings:api") +include(":features:wallet-settings:impl") // endregion Feature modules // region Domain modules From 3acfadfb08d9ea9d8426b7c245d4965b6c28a74b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jun 2024 15:50:00 +0500 Subject: [PATCH 075/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 10 ++++ core/res/src/main/res/values/strings.xml | 5 ++ .../core/ui/components/appbar/AppBar.kt | 55 +++++++++++++++++++ .../ui/components/rows/RoundableCornersRow.kt | 18 ++++-- .../state/StakingStateController.kt | 1 + .../impl/presentation/state/StakingUiState.kt | 4 ++ .../StakingInfoBottomSheetConfig.kt | 9 +++ .../state/stub/StakingClickIntentsStub.kt | 3 + .../DismissBottomSheetStateTransformer.kt | 10 ++++ .../SetInitialDataStateTransformer.kt | 2 +- .../ShowInfoBottomSheetStateTransformer.kt | 53 ++++++++++++++++++ .../ui/StakingInitialInfoContent.kt | 17 +++++- .../impl/presentation/ui/StakingScreen.kt | 12 ++++ .../ui/bottomsheet/StakingInfoBottomSheet.kt | 38 +++++++++++++ .../viewmodel/StakingClickIntents.kt | 3 + .../viewmodel/StakingViewModel.kt | 11 +++- 16 files changed, 242 insertions(+), 9 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8c1d366c83..a0be0aea2a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -548,8 +548,18 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + APY + Годовой процентный доход, который вы можете получить от участия в стейкинге. Доступно + Способ возраграждения + Способ получения вознаграждений за стейкинг. Он может быть автоматическим или ручным. + Период возрагражения + Это период, определяющий, когда участники стейкинга получат свои вознаграждения. Стейкинг %s + Период вывода + Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. + Период прогрева + Время, необходимое для начала процесса стейкинга. Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 38c7f41345..497372f66e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -554,6 +554,7 @@ Name APR APY + The annual percentage return you can earn from participating in staking. Available Average Reward Rate %s est. profit @@ -563,11 +564,15 @@ No rewards to claim On stake Reward claiming + A way to receive staking rewards. It can be claimed automatically or manually. Reward schedule + This is a schedule that determines when participants in staking receive their rewards. Rewards to claim: %s Staking %s Unbonding Period + The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period + The allocated time for activating participation in staking. Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt new file mode 100644 index 0000000000..80afe74fd7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt @@ -0,0 +1,55 @@ +package com.tangem.core.ui.components.appbar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * App bar without buttons + * + * @param text title + * @param modifier modifier + * + * @see Figma component + */ +@Composable +fun AppBar(text: TextReference, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth()) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.subtitle1, + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = TangemTheme.dimens.spacing10), + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AppBar_Preview() { + TangemThemePreview { + AppBar( + text = stringReference("Tangem"), + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index e83444c298..c2b1e97286 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -2,12 +2,16 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon import androidx.compose.material3.Surface import androidx.compose.material3.Text 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 @@ -17,9 +21,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.R @Suppress("LongParameterList") @Composable @@ -32,6 +36,7 @@ fun RoundableCornersRow( endTextStyle: TextStyle, cornersToRound: CornersToRound, iconResId: Int? = null, + iconClick: (() -> Unit)? = null, ) { Surface( shape = cornersToRound.getShape(), @@ -54,11 +59,16 @@ fun RoundableCornersRow( maxLines = 1, style = startTextStyle, ) - if (iconResId != null) { + if (iconResId != null && iconClick != null) { Icon( modifier = Modifier .padding(TangemTheme.dimens.spacing4) - .size(TangemTheme.dimens.size16), + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.radius10), + onClick = iconClick, + ), painter = painterResource(id = R.drawable.ic_alert_24), contentDescription = null, tint = TangemTheme.colors.text.tertiary, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 8a61d12bad..bef915818c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -42,6 +42,7 @@ internal class StakingStateController @Inject constructor() { confirmStakingState = StakingStates.ConfirmStakingState.Empty(), isBalanceHidden = false, event = consumedEvent(), + bottomSheetConfig = null, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 1f6e81e3b3..5d20156911 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -3,9 +3,11 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -22,6 +24,7 @@ internal data class StakingUiState( val amountState: AmountState, val confirmStakingState: StakingStates.ConfirmStakingState, val isBalanceHidden: Boolean, + val bottomSheetConfig: TangemBottomSheetConfig?, val event: StateEvent, ) { @@ -52,6 +55,7 @@ internal sealed class StakingStates { val rewardClaiming: String, val warmupPeriod: String, val rewardSchedule: String, + val onInfoClick: (InfoType) -> Unit, ) : InitialInfoState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt new file mode 100644 index 0000000000..f8cc06e495 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +data class StakingInfoBottomSheetConfig( + val title: TextReference, + val text: TextReference, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index f59a64f975..8a120d3f9a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.stub import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents object StakingClickIntentsStub : StakingClickIntents { @@ -11,6 +12,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onPrevClick() {} + override fun onInfoClick(infoType: InfoType) {} + override fun onAmountValueChange(value: String) {} override fun onAmountPasteTriggerDismiss() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt new file mode 100644 index 0000000000..251b237e9d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class DismissBottomSheetStateTransformer : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy(bottomSheetConfig = null) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 7a0d619a46..6282c12227 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -70,7 +70,7 @@ internal class SetInitialDataStateTransformer( rewardClaiming = yield.metadata.rewardClaiming, warmupPeriod = yield.metadata.warmupPeriod.days.toString(), rewardSchedule = yield.metadata.rewardSchedule, - + onInfoClick = clickIntents::onInfoClick, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt new file mode 100644 index 0000000000..ff1701bc9b --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig +import com.tangem.utils.transformer.Transformer + +internal class ShowInfoBottomSheetStateTransformer( + private val infoType: InfoType, + private val onDismiss: () -> Unit, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + onDismissRequest = onDismiss, + isShow = true, + content = when (infoType) { + InfoType.APY -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_apy), + text = resourceReference(R.string.staking_details_apy_info), + ) + InfoType.UNBOUNDING_PERIOD -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_unbonding_period), + text = resourceReference(R.string.staking_details_unbonding_period_info), + ) + InfoType.REWARD_CLAIMING -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_reward_claiming), + text = resourceReference(R.string.staking_details_reward_claiming_info), + ) + InfoType.WARMUP_PERIOD -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_warmup_period), + text = resourceReference(R.string.staking_details_warmup_period_info), + ) + InfoType.REWARD_SCHEDULE -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_reward_schedule), + text = resourceReference(R.string.staking_details_reward_schedule_info), + ) + }, + ), + ) + } +} + +enum class InfoType { + APY, + UNBOUNDING_PERIOD, + REWARD_CLAIMING, + WARMUP_PERIOD, + REWARD_SCHEDULE, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 97491c4a36..558dc71308 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType @Composable internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { @@ -121,11 +122,13 @@ internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { startText = stringResource(id = R.string.staking_details_apy), endText = state.aprRange.resolveReference(), cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.APY) }, ) InitialInfoContentRow( startText = stringResource(id = R.string.staking_details_unbonding_period), endText = state.unbondingPeriod, cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, ) InitialInfoContentRow( startText = stringResource(id = R.string.staking_details_minimum_requirement), @@ -136,21 +139,29 @@ internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { startText = stringResource(id = R.string.staking_details_reward_claiming), endText = state.rewardClaiming, cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) }, ) InitialInfoContentRow( startText = stringResource(id = R.string.staking_details_warmup_period), endText = state.warmupPeriod, cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) }, ) InitialInfoContentRow( startText = stringResource(id = R.string.staking_details_reward_schedule), endText = state.rewardSchedule, cornersToRound = CornersToRound.BOTTOM_2, + iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) }, ) } @Composable -private fun InitialInfoContentRow(startText: String, endText: String, cornersToRound: CornersToRound) { +private fun InitialInfoContentRow( + startText: String, + endText: String, + cornersToRound: CornersToRound, + iconClick: (() -> Unit)? = null, +) { RoundableCornersRow( startText = startText, startTextColor = TangemTheme.colors.text.primary1, @@ -159,7 +170,8 @@ private fun InitialInfoContentRow(startText: String, endText: String, cornersToR endTextColor = TangemTheme.colors.text.tertiary, endTextStyle = TangemTheme.typography.body2, cornersToRound = cornersToRound, - iconResId = null, // TODO staking add bottom sheets when text will be available + iconResId = R.drawable.ic_information_24, + iconClick = iconClick, ) } @@ -189,6 +201,7 @@ private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvide rewardClaiming = "Auto", warmupPeriod = "Days", rewardSchedule = "Block", + onInfoClick = {}, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 4a41fba77f..d53e68d30a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -14,11 +14,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig +import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -45,6 +48,15 @@ internal fun StakingScreen(uiState: StakingUiState) { StakingNavigationButtons( uiState = uiState, ) + StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) + } +} + +@Composable +fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { + if (bottomSheetConfig == null) return + when (bottomSheetConfig.content) { + is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt new file mode 100644 index 0000000000..0b1104c35b --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt @@ -0,0 +1,38 @@ +package com.tangem.features.staking.impl.presentation.ui.bottomsheet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.appbar.AppBar +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig + +@Composable +fun StakingInfoBottomSheet(config: TangemBottomSheetConfig) { + val scrollState = rememberScrollState() + TangemBottomSheet( + config = config, + ) { content: StakingInfoBottomSheetConfig -> + Column( + modifier = Modifier.verticalScroll(scrollState), + ) { + AppBar(text = content.title) + Text( + text = content.text.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing28, + vertical = TangemTheme.dimens.spacing16, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 235659fa3e..d89142ec38 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType internal interface StakingClickIntents : AmountScreenClickIntents { @@ -11,6 +12,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onPrevClick() + fun onInfoClick(infoType: InfoType) + override fun onAmountNext() = onNextClick() fun openValidators() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index ee086db4f2..e898de40d8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -22,8 +22,7 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.transformers.HideBalanceStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer @@ -92,6 +91,14 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } + override fun onInfoClick(infoType: InfoType) { + stateController.update( + ShowInfoBottomSheetStateTransformer(infoType) { + stateController.update(DismissBottomSheetStateTransformer()) + }, + ) + } + override fun onAmountValueChange(value: String) { stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value)) } From 164030dc9c501a9d70cf0b833ca7e7c45d8b45d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Jun 2024 10:25:11 +0400 Subject: [PATCH 076/223] Updated on 2026-08-14 --- .../block/information/InformationBlock.kt | 193 ++++++++++++++++++ .../information/InformationBlockContent.kt | 77 +++++++ .../core/ui/components/buttons/SmallButton.kt | 98 +++++++-- .../buttons/common/TangemButtonColors.kt | 2 +- .../core/ui/components/text/TooltipText.kt | 91 +++++++++ 5 files changed, 442 insertions(+), 19 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt new file mode 100644 index 0000000000..110b0048aa --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -0,0 +1,193 @@ +package com.tangem.core.ui.components.block.information + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +@Immutable +class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope + +@Composable +fun InformationBlock( + title: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + action: (@Composable BoxScope.() -> Unit)? = null, + content: (@Composable InformationBlockContentScope.() -> Unit)? = null, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.action), + horizontalAlignment = Alignment.Start, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size40) + .padding( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing6, + ) + .padding(horizontal = TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .weight(weight = 1f) + .heightIn(min = TangemTheme.dimens.size20), + contentAlignment = Alignment.CenterStart, + content = title, + ) + if (action != null) { + Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing8)) + Box( + modifier = Modifier + .weight(weight = 1f) + .heightIn(min = TangemTheme.dimens.size24), + contentAlignment = Alignment.CenterEnd, + content = action, + ) + } + } + + if (content != null) { + Box( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) { + val scope = InformationBlockContentScope(scope = this) + content(scope) + } + } + } +} + +// region Previews +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_Grid() { + TangemThemePreview { + InformationBlock( + title = { + TooltipText( + text = stringReference("Grid title"), + onInfoClick = { }, + ) + }, + content = { + GridItems( + items = persistentListOf( + stringReference("Fist item"), + stringReference("Second item"), + ), + itemContent = { + PreviewItem(text = it) + }, + ) + }, + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_List() { + TangemThemePreview { + InformationBlock( + title = { + Text( + text = "List", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + action = { + SecondarySmallButton( + config = SmallButtonConfig( + text = stringReference("Add token"), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = {}, + ), + ) + }, + content = { + ListItems( + items = persistentListOf( + stringReference("Fist item"), + stringReference("Second item"), + ), + itemContent = { + PreviewItem(it) + }, + ) + }, + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_Plain() { + TangemThemePreview { + InformationBlock( + title = { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TooltipText( + text = stringReference("Title"), + onInfoClick = { /* [REDACTED_TODO_COMMENT]*/ }, + ) + + Text( + text = "Subtitle", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + }, + action = { + PreviewItem(text = stringReference("Action")) + }, + ) + } +} + +@Composable +private fun PreviewItem(text: TextReference) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.secondary) + .padding(all = TangemTheme.dimens.spacing12), + ) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.primary1, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt new file mode 100644 index 0000000000..02b783e839 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt @@ -0,0 +1,77 @@ +package com.tangem.core.ui.components.block.information + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun InformationBlockContentScope.ListItems( + items: ImmutableList, + itemContent: @Composable BoxScope.(T) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8, Alignment.Top), + ) { + items.fastForEach { item -> + Box( + modifier = Modifier.fillMaxWidth(), + ) { + itemContent(item) + } + } + } +} + +@Composable +fun InformationBlockContentScope.GridItems( + items: ImmutableList, + itemContent: @Composable BoxScope.(T) -> Unit, + modifier: Modifier = Modifier, +) { + val rowItems by remember(items) { + derivedStateOf { + items.asSequence() + .windowed(size = 2, step = 2, partialWindows = true) + .map { it.toImmutableList() } + .toImmutableList() + } + } + + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + rowItems.fastForEach { row -> + Row( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start), + ) { + row.fastForEach { item -> + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.Center, + ) { + itemContent(item) + } + } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index 15f640bdbf..e02ae97109 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -6,17 +6,21 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue 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.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Small button config @@ -28,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme data class SmallButtonConfig( val text: TextReference, val onClick: () -> Unit, + val icon: TangemButtonIconPosition = TangemButtonIconPosition.None, ) /** @@ -61,13 +66,12 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: label = "Update background color", ) - val textColor by animateColorAsState( - targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, - label = "Update text color", - ) - Box( + Row( modifier = modifier - .defaultMinSize(minWidth = TangemTheme.dimens.size46, minHeight = TangemTheme.dimens.size24) + .defaultMinSize( + minWidth = TangemTheme.dimens.size46, + minHeight = TangemTheme.dimens.size24, + ) .clip(shape) .background( color = backgroundColor, @@ -75,22 +79,68 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: ) .clickable(enabled = true, onClick = config.onClick) .padding( - vertical = TangemTheme.dimens.spacing2, + paddingValues = when (config.icon) { + is TangemButtonIconPosition.None -> PaddingValues( + horizontal = TangemTheme.dimens.spacing12, + ) + is TangemButtonIconPosition.End -> PaddingValues( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing8, + ) + is TangemButtonIconPosition.Start -> PaddingValues( + start = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing12, + ) + }, ), - contentAlignment = Alignment.Center, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, ) { - Text( - modifier = Modifier.padding( - horizontal = TangemTheme.dimens.spacing10, - ), - text = config.text.resolveReference(), - color = textColor, - maxLines = 1, - style = TangemTheme.typography.button, + ContentContainer( + iconPosition = config.icon, + text = { + val textColor by animateColorAsState( + targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, + label = "Update text color", + ) + + Text( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4), + text = config.text.resolveReference(), + color = textColor, + maxLines = 1, + style = TangemTheme.typography.button, + ) + }, + icon = { iconResId -> + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = iconResId), + tint = TangemTheme.colors.icon.secondary, + contentDescription = null, + ) + }, ) } } +@Composable +private fun RowScope.ContentContainer( + iconPosition: TangemButtonIconPosition, + text: @Composable RowScope.() -> Unit, + icon: @Composable RowScope.(Int) -> Unit, +) { + if (iconPosition is TangemButtonIconPosition.Start) { + icon(iconPosition.iconResId) + Spacer(modifier = Modifier.requiredWidth(TangemTheme.dimens.spacing4)) + } + text() + if (iconPosition is TangemButtonIconPosition.End) { + Spacer(modifier = Modifier.requiredWidth(TangemTheme.dimens.spacing4)) + icon(iconPosition.iconResId) + } +} + @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -112,5 +162,17 @@ private fun ButtonsSample() { ) PrimarySmallButton(config = config) SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"))) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Rating"), + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), + ), + ) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Add token"), + icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), + ), + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt index 344811dac2..f4235c7619 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.graphics.Color -class TangemButtonColors( +data class TangemButtonColors( private val backgroundColor: Color, private val contentColor: Color, private val disabledBackgroundColor: Color, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt new file mode 100644 index 0000000000..110080210d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt @@ -0,0 +1,91 @@ +package com.tangem.core.ui.components.text + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun TooltipText( + text: TextReference, + onInfoClick: () -> Unit, + modifier: Modifier = Modifier, + useSmallerText: Boolean = false, +) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onInfoClick, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = text.resolveReference(), + style = if (useSmallerText) { + TangemTheme.typography.caption2 + } else { + TangemTheme.typography.subtitle2 + }, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + IconButton( + modifier = Modifier.requiredSize(TangemTheme.dimens.size24), + interactionSource = interactionSource, + onClick = onInfoClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TooltipText() { + TangemThemePreview { + Box( + modifier = Modifier + .width(width = 90.dp) + .background(color = TangemTheme.colors.background.primary), + ) { + TooltipText( + text = stringReference("Text"), + onInfoClick = { /* [REDACTED_TODO_COMMENT]*/ }, + ) + } + } +} +// endregion Preview \ No newline at end of file From 04761c45e407fd06f028dba721f738c8cdb8df16 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 24 Jun 2024 17:51:56 +0400 Subject: [PATCH 077/223] Updated on 2026-08-14 --- .../details/ui/details/DetailsScreenState.kt | 4 +- .../main/res/drawable/ic_add_friends_24.xml | 0 .../main/res/drawable/ic_card_foget_24.xml | 10 + .../main/res/drawable/ic_card_settings_24.xml | 0 .../preview/PreviewRenameWalletComponent.kt | 24 +++ .../preview/PreviewWalletSettingsComponent.kt | 32 ++++ .../walletsettings/entity/DialogConfig.kt | 14 ++ .../walletsettings/entity/RenameWalletUM.kt | 12 ++ .../entity/WalletSettingsItemUM.kt | 25 +++ .../walletsettings/entity/WalletSettingsUM.kt | 10 + .../walletsettings/ui/RenameWalletDialog.kt | 53 ++++++ .../walletsettings/ui/WalletSettingsScreen.kt | 171 ++++++++++++++++++ .../walletsettings/utils/ItemsBuilder.kt | 65 +++++++ 13 files changed, 418 insertions(+), 2 deletions(-) rename app/src/main/res/drawable/ic_add_friends.xml => core/ui/src/main/res/drawable/ic_add_friends_24.xml (100%) create mode 100644 core/ui/src/main/res/drawable/ic_card_foget_24.xml rename app/src/main/res/drawable/ic_card_settings.xml => core/ui/src/main/res/drawable/ic_card_settings_24.xml (100%) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index b8d8025916..b35c17a5fd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -67,7 +67,7 @@ internal sealed class SettingsItem( data class CardSettings( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_card_settings, + iconResId = R.drawable.ic_card_settings_24, title = resourceReference(R.string.card_settings_title), ) @@ -95,7 +95,7 @@ internal sealed class SettingsItem( data class ReferralProgram( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_add_friends, + iconResId = R.drawable.ic_add_friends_24, title = resourceReference(R.string.details_referral_title), ) diff --git a/app/src/main/res/drawable/ic_add_friends.xml b/core/ui/src/main/res/drawable/ic_add_friends_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_add_friends.xml rename to core/ui/src/main/res/drawable/ic_add_friends_24.xml diff --git a/core/ui/src/main/res/drawable/ic_card_foget_24.xml b/core/ui/src/main/res/drawable/ic_card_foget_24.xml new file mode 100644 index 0000000000..77d2117ba5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_card_foget_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_card_settings.xml b/core/ui/src/main/res/drawable/ic_card_settings_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_card_settings.xml rename to core/ui/src/main/res/drawable/ic_card_settings_24.xml diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt new file mode 100644 index 0000000000..4c7837a830 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.walletsettings.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.ui.RenameWalletDialog + +internal class PreviewRenameWalletComponent : RenameWalletComponent { + + private val previewState = RenameWalletUM( + walletNameValue = TextFieldValue(text = "My Wallet"), + isNameCorrect = false, + updateValue = {}, + onConfirm = {}, + ) + + override val doOnDismiss: () -> Unit = {} + + @Composable + override fun Dialog() { + RenameWalletDialog(model = previewState, onDismiss = {}) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt new file mode 100644 index 0000000000..1ba3218c38 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.walletsettings.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import com.tangem.feature.walletsettings.utils.ItemsBuilder + +internal class PreviewWalletSettingsComponent : WalletSettingsComponent { + + private val previewState = WalletSettingsUM( + popBack = {}, + items = ItemsBuilder( + router = DummyRouter(), + ).buildItems( + walletName = "My wallet", + renameWallet = {}, + forgetWallet = {}, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + WalletSettingsScreen( + modifier = modifier, + state = previewState, + dialog = {}, + ) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt new file mode 100644 index 0000000000..b338e5d3eb --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.walletsettings.entity + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface DialogConfig { + + @Serializable + data class RenameWallet( + val userWalletId: UserWalletId, + val currentName: String, + ) : DialogConfig +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt new file mode 100644 index 0000000000..4dd5b8c7a7 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.walletsettings.entity + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.input.TextFieldValue + +@Immutable +internal data class RenameWalletUM( + val walletNameValue: TextFieldValue, + val isNameCorrect: Boolean, + val updateValue: (value: TextFieldValue) -> Unit, + val onConfirm: () -> Unit, +) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt new file mode 100644 index 0000000000..9596f1fa02 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.walletsettings.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class WalletSettingsItemUM { + + abstract val id: String + + data class WithItems( + override val id: String, + val description: TextReference, + val blocks: ImmutableList, + ) : WalletSettingsItemUM() + + data class WithText( + override val id: String, + val title: TextReference, + val text: TextReference, + val onClick: () -> Unit, + ) : WalletSettingsItemUM() +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt new file mode 100644 index 0000000000..749a2ce005 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.walletsettings.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class WalletSettingsUM( + val popBack: () -> Unit, + val items: PersistentList, +) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt new file mode 100644 index 0000000000..62a9f39c72 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.walletsettings.ui + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.impl.R + +@Composable +internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { + val value by rememberUpdatedState(newValue = model.walletNameValue) + + TextInputDialog( + title = stringResource(id = R.string.user_wallet_list_rename_popup_title), + fieldValue = value, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + enabled = model.isNameCorrect, + onClick = { + model.onConfirm() + onDismiss() + }, + ), + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + onValueChange = model.updateValue, + textFieldParams = AdditionalTextInputDialogParams( + label = stringResource(id = R.string.user_wallet_list_rename_popup_placeholder), + ), + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_RenameWalletDialog() { + TangemThemePreview { + PreviewRenameWalletComponent().Dialog() + } +} +// endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt new file mode 100644 index 0000000000..9dcfd53ddb --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -0,0 +1,171 @@ +package com.tangem.feature.walletsettings.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.appbar.models.TopAppBarMedium +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.BlockItem +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletSettingsScreen( + state: WalletSettingsUM, + dialog: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + val backgroundColor = TangemTheme.colors.background.secondary + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + BackHandler(onBack = state.popBack) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = backgroundColor, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + hostState = LocalSnackbarHostState.current, + ) + }, + topBar = { + TopAppBarMedium( + title = resourceReference(R.string.wallet_settings_title), + scrollBehavior = scrollBehavior, + onBackClick = state.popBack, + ) + }, + content = { paddingValues -> + Content( + modifier = Modifier.padding(paddingValues), + state = state, + ) + + dialog() + }, + ) +} + +@Composable +private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + items( + items = state.items, + key = WalletSettingsItemUM::id, + ) { item -> + val itemModifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16) + + when (item) { + is WalletSettingsItemUM.WithItems -> ItemsBlock( + modifier = itemModifier, + model = item, + ) + is WalletSettingsItemUM.WithText -> TextBlock( + modifier = itemModifier, + model = item, + ) + } + } + } +} + +@Composable +private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + ), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + ) { + model.blocks.forEach { block -> + BlockItem(model = block) + } + } + + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = model.description.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} + +@Composable +private fun TextBlock(model: WalletSettingsItemUM.WithText, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier.fillMaxWidth(), + onClick = model.onClick, + ) { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = model.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = model.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_WalletSettingsScreen() { + TangemThemePreview { + PreviewWalletSettingsComponent().Content(modifier = Modifier.fillMaxSize()) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt new file mode 100644 index 0000000000..52f95c08b8 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -0,0 +1,65 @@ +package com.tangem.feature.walletsettings.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.impl.R +import kotlinx.collections.immutable.persistentListOf +import javax.inject.Inject + +@ComponentScoped +internal class ItemsBuilder @Inject constructor( + private val router: Router, +) { + + fun buildItems(walletName: String, forgetWallet: () -> Unit, renameWallet: () -> Unit) = persistentListOf( + buildNameItem(walletName, renameWallet), + buildCardItem(), + buildForgetItem(forgetWallet), + ) + + private fun buildNameItem(walletName: String, renameWallet: () -> Unit) = WalletSettingsItemUM.WithText( + id = "wallet_name", + title = resourceReference(id = R.string.settings_wallet_name_title), + text = stringReference(walletName), + onClick = renameWallet, + ) + + private fun buildCardItem() = WalletSettingsItemUM.WithItems( + id = "card", + blocks = buildCardBlocks(), + description = resourceReference(R.string.settings_card_settings_footer), + ) + + private fun buildCardBlocks() = persistentListOf( + BlockUM( + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings) }, + ), + BlockUM( + text = resourceReference(R.string.referral_title), + iconRes = R.drawable.ic_add_friends_24, + onClick = { router.push(AppRoute.ReferralProgram) }, + ), + ) + + private fun buildForgetItem(forgetWallet: () -> Unit) = WalletSettingsItemUM.WithItems( + id = "forget", + blocks = buildForgetBlocks(forgetWallet), + description = resourceReference(R.string.settings_forget_wallet_footer), + ) + + private fun buildForgetBlocks(forgetWallet: () -> Unit) = persistentListOf( + BlockUM( + text = resourceReference(R.string.settings_forget_wallet), + iconRes = R.drawable.ic_card_foget_24, + onClick = forgetWallet, + accentType = BlockUM.AccentType.WARNING, + ), + ) +} \ No newline at end of file From fe1d2342f45107ed23ab1b971bb7c38acdbd5956 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 18:11:25 +0800 Subject: [PATCH 078/223] Updated on 2026-08-14 --- .../tap/data/RuntimeUserWalletsStore.kt | 4 ++ .../tangem/tap/features/main/MainViewModel.kt | 1 - .../datasource/di/UserTokensStoreModule.kt | 24 ++++--- .../local/preferences/PreferencesKeys.kt | 2 + .../token/AppPreferencesUserTokensStore.kt | 63 +++++++++++++++++++ .../datasource/local/token/UserTokensStore.kt | 32 ++++++++++ .../local/token/UserTokensStoreMigration.kt | 56 +++++++++++++++++ .../token/UserTokensStoreMigrationRunner.kt | 50 +++++++++++++++ .../local/userwallet/UserWalletsStore.kt | 3 + 9 files changed, 226 insertions(+), 9 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.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 index bd50b28faf..58d13616e4 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented @@ -15,6 +16,9 @@ internal class RuntimeUserWalletsStore( override val selectedUserWalletOrNull: UserWallet? get() = userWalletsListManager.selectedUserWalletSync + override val userWallets: Flow> + get() = userWalletsListManager.userWallets + override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { return userWalletsListManager .userWallets diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 8d567fc5a5..b127b60b18 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -7,7 +7,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic - import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase 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 757c01cef5..b2997661fb 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,11 +1,11 @@ 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.datastore.FileDataStore -import com.tangem.datasource.local.token.DefaultUserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.AppPreferencesUserTokensStore import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,9 +18,17 @@ internal object UserTokensStoreModule { @Provides @Singleton - fun provideUserTokensStore(fileReader: FileReader, @NetworkMoshi moshi: Moshi): UserTokensStore { - return DefaultUserTokensStore( - dataStore = FileDataStore(fileReader, moshi.adapter(UserTokensResponse::class.java)), + fun provideUserTokensStore( + appPreferencesStore: AppPreferencesStore, + userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, + userWalletsStore: UserWalletsStore, + dispatchers: CoroutineDispatcherProvider, + ): UserTokensStore { + return AppPreferencesUserTokensStore( + appPreferencesStore = appPreferencesStore, + userTokensStoreMigrationRunner = userTokensStoreMigrationRunner, + userWalletsStore = userWalletsStore, + dispatchers = dispatchers, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 42df77c07d..47ad806d82 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -108,6 +108,8 @@ object PreferencesKeys { fun getPermissionDaysCount(permission: String) = longPreferencesKey("pushPermissionDaysCount_$permission") // endregion + + fun getUserTokensKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId") } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt new file mode 100644 index 0000000000..80d70825bf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt @@ -0,0 +1,63 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObject +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +/** + * Implementation of [UserTokensStore] that based on [appPreferencesStore] + * + * @property appPreferencesStore application preference store + * +[REDACTED_AUTHOR] + */ +internal class AppPreferencesUserTokensStore( + private val appPreferencesStore: AppPreferencesStore, + private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, + private val userWalletsStore: UserWalletsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : UserTokensStore { + + init { + runUserTokensMigrations() + } + + override fun get(key: UserWalletId): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) + .filterNotNull() + } + + override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), + ) + } + + override suspend fun store(key: UserWalletId, value: UserTokensResponse) { + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), + value = value, + ) + } + + // TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA] + private fun runUserTokensMigrations() { + userWalletsStore.userWallets + .filter { it.isNotEmpty() } + .take(1) + .onEach { userWallets -> + userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue }) + } + .flowOn(dispatchers.io) + .launchIn(CoroutineScope(dispatchers.io)) + } +} \ 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 08fcb31438..f3f12f026b 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 @@ -4,11 +4,43 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +@Deprecated( + message = "Use AppPreferencesStore", + replaceWith = ReplaceWith( + expression = "AppPreferencesStore", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, +) interface UserTokensStore { + @Deprecated( + message = "Use getObject", + replaceWith = ReplaceWith( + expression = "appPreferencesStore.getObject(userWalletId)", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, + ) fun get(key: UserWalletId): Flow + @Deprecated( + message = "Use getObjectSyncOrNull", + replaceWith = ReplaceWith( + expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, + ) suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? + @Deprecated( + message = "Use storeObject", + replaceWith = ReplaceWith( + expression = "appPreferencesStore.storeObject(userWalletId, response)", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, + ) suspend fun store(key: UserWalletId, value: UserTokensResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt new file mode 100644 index 0000000000..dae09c0616 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt @@ -0,0 +1,56 @@ +package com.tangem.datasource.local.token + +import androidx.datastore.core.DataMigration +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject + +/** + * Migration of saving [UserTokensResponse] from file to [AppPreferencesStore] + * + * @param userWalletId user wallet id + * @param moshi moshi + * @property fileReader file reader + * +[REDACTED_AUTHOR] + */ +internal class UserTokensStoreMigration( + userWalletId: String, + moshi: Moshi, + private val fileReader: FileReader, +) : DataMigration { + + private val legacyFileName = "user_tokens_$userWalletId" + private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId) + + @OptIn(ExperimentalStdlibApi::class) + private val adapter = moshi.adapter() + + override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true + + override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore { + val currentKey = currentData.getObjectSyncOrNull(key = keyName) + + if (currentKey != null) return currentData + + val value = runCatching { + val json = fileReader.readFile(legacyFileName) + adapter.fromJson(json) + }.getOrNull() + + if (value != null) { + currentData.storeObject(key = keyName, value = value) + } + + return currentData + } + + override suspend fun cleanUp() { + fileReader.removeFile(legacyFileName) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt new file mode 100644 index 0000000000..eafb84aa79 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt @@ -0,0 +1,50 @@ +package com.tangem.datasource.local.token + +import com.squareup.moshi.Moshi +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Runner that launch migrations of saving user tokens store + * + * @property appPreferencesStore application preference store + * @property fileReader file reader + * @property moshi moshi + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +class UserTokensStoreMigrationRunner @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, + private val fileReader: FileReader, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend fun run(ids: List) { + ids.forEach { id -> + coroutineScope { run(id) } + } + } + + private suspend fun run(id: String) { + withContext(dispatchers.io) { + val migration = UserTokensStoreMigration( + userWalletId = id, + moshi = moshi, + fileReader = fileReader, + ) + + migration.migrate(appPreferencesStore) + + migration.cleanUp() + } + } +} \ 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 bfee20e460..6aa2933ba2 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 @@ -2,11 +2,14 @@ package com.tangem.datasource.local.userwallet import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface UserWalletsStore { val selectedUserWalletOrNull: UserWallet? + val userWallets: Flow> + suspend fun getSyncOrNull(key: UserWalletId): UserWallet? suspend fun getAllSyncOrNull(): List? From 6e69e043c36459ec898fc4a103dd3805ea5d024c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 14:15:40 +0300 Subject: [PATCH 079/223] Updated on 2026-08-14 --- .../common/ui/charts/MarketChartMini.kt | 111 ++++++++++++++++++ .../charts/state/MarketChartDataProducer.kt | 22 ++-- .../ui/charts/state/MarketChartRawData.kt | 9 ++ .../ui/charts/state/MarketChartState.kt | 24 +--- .../ui/charts/state/PointValuesConverter.kt | 69 +++++++++++ .../core/ui/haptic/MockHapticManager.kt | 22 +++- .../tangem/core/ui/res/TangemThemePreview.kt | 3 + 7 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt create mode 100644 common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt new file mode 100644 index 0000000000..d9902450d3 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt @@ -0,0 +1,111 @@ +package com.tangem.common.ui.charts + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec +import com.patrykandpatrick.vico.compose.common.shader.BrushShader +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.shader.ColorShader +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlin.random.Random + +@Composable +fun MarketChartMini( + rawData: MarketChartRawData, + modifier: Modifier = Modifier, + type: MarketChartLook.Type = MarketChartLook.Type.Growing, + growingColor: Color = TangemTheme.colors.icon.accent, + fallingColor: Color = TangemTheme.colors.icon.warning, +) { + val model = remember(rawData) { + CartesianChartModel(LineCartesianLayerModel.build { series(rawData.y) }) + } + + val lineColor = when (type) { + MarketChartLook.Type.Growing -> growingColor + MarketChartLook.Type.Falling -> fallingColor + } + + val lineSpec = rememberLineSpec( + shader = ColorShader(lineColor.toArgb()), + thickness = 1.dp, + backgroundShader = BrushShader( + brush = Brush.verticalGradient( + colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent), + ), + ), + ) + + val layer = rememberLineCartesianLayer(listOf(lineSpec)) + val chart = rememberCartesianChart(layer) + + CartesianChartHost( + modifier = modifier, + chart = chart, + model = model, + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + scrollState = rememberVicoScrollState(scrollEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + ) +} + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + val data = MarketChartRawData( + x = List(20) { Random.nextFloat() }, + y = List(20) { Random.nextFloat() }, + ) + + TangemThemePreview { + Column { + MarketChartMini(rawData = data, type = MarketChartLook.Type.Growing) + SpacerH16() + MarketChartMini(rawData = data, type = MarketChartLook.Type.Falling) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewColumn() { + val data = MarketChartRawData( + x = List(20) { Random.nextFloat() }, + y = List(20) { Random.nextFloat() }, + ) + + TangemThemePreview { + LazyColumn { + items(100) { + MarketChartMini( + rawData = data, + type = if (it % 3 == 0) MarketChartLook.Type.Growing else MarketChartLook.Type.Falling, + ) + } + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt index da62140e59..bcad7d3ceb 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -75,6 +75,7 @@ class TransactionSuspend( class MarketChartDataProducer private constructor( initialData: MarketChartData, initialLook: MarketChartLook, + val pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, private val dispatcher: CoroutineDispatcher = Dispatchers.Default, ) { internal val startDrawingAnimation = MutableSharedFlow() @@ -110,13 +111,11 @@ class MarketChartDataProducer private constructor( startDrawingAnimation.emit(Unit) } withContext(dispatcher) { - val minX = chartData.x.min() - val minY = chartData.y.min() + val rawData = pointsValuesConverter.convert(chartData) - val normY = chartData.y.map { normalize(it, minY) } - val normX = chartData.x.map { normalize(it, minX) } + val entriesLocal = + rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) } - val entriesLocal = normX.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, normY[index]) } entries.value = entriesLocal modelProducer.runTransaction { @@ -160,6 +159,7 @@ class MarketChartDataProducer private constructor( * @return A MarketChartDataProducer. */ suspend fun buildSuspend( + pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, dispatcher: CoroutineDispatcher = Dispatchers.Default, block: TransactionSuspend.() -> Unit, ): MarketChartDataProducer { @@ -169,6 +169,7 @@ class MarketChartDataProducer private constructor( initialData = initialData, initialLook = initialLook, dispatcher = dispatcher, + pointsValuesConverter = pointsValuesConverter, ).apply { handleTransactionSuspend(transaction) } @@ -183,6 +184,7 @@ class MarketChartDataProducer private constructor( * @return A MarketChartDataProducer. */ fun build( + pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, dispatcher: CoroutineDispatcher = Dispatchers.Default, block: Transaction.() -> Unit, ): MarketChartDataProducer { @@ -192,16 +194,8 @@ class MarketChartDataProducer private constructor( initialData = transaction.chartData ?: initialData, initialLook = transaction.chartLook ?: initialLook, dispatcher = dispatcher, + pointsValuesConverter = pointsValuesConverter, ) } } -} - -private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float { - val n = value - min - return if (scale > 2) { - n.movePointRight(scale - 2).toFloat() - } else { - n.toFloat() - } } \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt new file mode 100644 index 0000000000..a1b7e91f2c --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt @@ -0,0 +1,9 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Immutable + +@Immutable +data class MarketChartRawData( + val y: List, + val x: List = List(y.size) { 1f }, +) \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt index 110161db69..92a0379f5d 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -75,15 +75,9 @@ class MarketChartState internal constructor( val state = dataProducer.dataState.value as? MarketChartData.Data ?: return@CartesianValueFormatter value.toString() - val sMin = state.x.min() - val scale = sMin.scale() - val bVal = if (scale > 2) { - value.toBigDecimal().movePointLeft(scale - 2) + sMin - } else { - value.toBigDecimal() + sMin - } - - lookState.value.xAxisFormatter.format(bVal) + lookState.value.xAxisFormatter.format( + value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state), + ) } } @@ -92,15 +86,9 @@ class MarketChartState internal constructor( val state = dataProducer.dataState.value as? MarketChartData.Data ?: return@CartesianValueFormatter value.toString() - val sMin = state.y.min() - val scale = sMin.scale() - val bVal = if (scale > 2) { - value.toBigDecimal().movePointLeft(scale - 2) + sMin - } else { - value.toBigDecimal() + sMin - } - - lookState.value.yAxisFormatter.format(bVal) + lookState.value.yAxisFormatter.format( + value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state), + ) } } diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt new file mode 100644 index 0000000000..1dab1cd0cc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt @@ -0,0 +1,69 @@ +package com.tangem.common.ui.charts.state + +import java.math.BigDecimal + +/** + * Interface to convert chart data values to Floats and backwards. + * + * We need to convert the values on the graph to floating point values in order to display them correctly on the canvas. + * We also need to determine exactly which floating point value on the graph corresponds to the decimal point, + * so that we can format the actual value and display on the x/y axis. + */ +interface PointValuesConverter { + + fun convert(data: MarketChartData.Data): MarketChartRawData + + fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal + + fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal +} + +object DefaultPointValuesConverter : PointValuesConverter { + + override fun convert(data: MarketChartData.Data): MarketChartRawData { + val minX = data.x.min() + val minY = data.y.min() + + val normY = data.y.map { normalize(it, minY) } + val normX = data.x.map { normalize(it, minX) } + + return MarketChartRawData( + x = normX, + y = normY, + ) + } + + override fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal { + val dataMin = data.x.min() + val scale = dataMin.scale() + val bVal = if (scale > 2) { + rawX.toBigDecimal().movePointLeft(scale - 2) + dataMin + } else { + rawX.toBigDecimal() + dataMin + } + + return bVal + } + + override fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal { + val dataMin = data.y.min() + val scale = dataMin.scale() + val bVal = if (scale > 2) { + rawY.toBigDecimal().movePointLeft(scale - 2) + dataMin + } else { + rawY.toBigDecimal() + dataMin + } + + return bVal + } + + // TODO enhance algorithm for values with big difference between min and max, which cannot fit in Float + private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float { + val n = value - min + return if (scale > 2) { + n.movePointRight(scale - 2).toFloat() + } else { + n.toFloat() + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt index 876ee7a05d..ad74ee1bd1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt @@ -1,16 +1,30 @@ package com.tangem.core.ui.haptic -object MockHapticManager : HapticManager { +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType + +@Suppress("FunctionName") +fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager = + if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback) + +val MockHapticManager: HapticManager = MockHapticManagerImpl() + +private class MockHapticManagerImpl( + private val mockHapticFeedback: HapticFeedback? = null, +) : HapticManager { override fun vibrateShort() { - /** Intentionnaly do nothing */ + mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove) + /** Intentionally do nothing */ } override fun vibrateMeduim() { - /** Intentionnaly do nothing */ + mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove) + /** Intentionally do nothing */ } override fun vibrateLong() { - /** Intentionnaly do nothing */ + mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress) + /** Intentionally do nothing */ } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 48e3237b47..2135bbdff8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -3,6 +3,8 @@ package com.tangem.core.ui.res import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalHapticFeedback +import com.tangem.core.ui.haptic.MockHapticManager import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable @@ -20,6 +22,7 @@ fun TangemThemePreview( typography = typography, dimens = dimens, windowSize = rememberWindowSizePreview(maxWidth, maxHeight), + hapticManager = MockHapticManager(LocalHapticFeedback.current), content = content, ) } From 24848624e935f92247642d2d6a0535347fd5180d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 20:58:35 +0800 Subject: [PATCH 080/223] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 + .../java/com/tangem/tap/TangemApplication.kt | 7 ++ .../common/feedback/LegacyFeedbackManager.kt | 43 +++++++++--- .../common/feedback/ProxyFeedbackManager.kt | 16 ++--- .../tap/common/redux/global/GlobalAction.kt | 2 +- .../common/redux/global/GlobalMiddleware.kt | 5 +- .../common/redux/legacy/LegacyMiddleware.kt | 33 +++++++++- .../tangem/tap/common/ui/ScanFailsDialog.kt | 2 +- .../java/com/tangem/tap/di/UtilsModule.kt | 2 +- .../tap/di/domain/FeedbackDomainModule.kt | 7 ++ .../details/ui/details/DetailsViewModel.kt | 8 ++- .../onboarding/OnboardingMenuProvider.kt | 14 +++- .../products/BaseOnboardingFragment.kt | 10 ++- .../products/twins/redux/TwinCardsReducer.kt | 7 +- .../products/twins/redux/TwinCardsState.kt | 3 +- .../twins/ui/OnboardingTwinsFragment.kt | 12 ++++ .../wallet/ui/OnboardingWalletFragment.kt | 16 ++++- .../ui/dialogs/WalletActivationErrorDialog.kt | 8 ++- .../features/send/redux/SendScreenAction.kt | 9 ++- .../redux/middlewares/RequestFeeMiddleware.kt | 6 +- .../send/redux/middlewares/SendMiddleware.kt | 27 +++++++- .../send/ui/dialogs/RequestFeeErrorDialog.kt | 7 +- .../ui/dialogs/SendTransactionFailsDialog.kt | 9 +-- .../tap/proxy/redux/DaggerGraphState.kt | 4 ++ .../feedback/DummyFeedbackManager.kt | 8 --- .../navigation/feedback/FeedbackManager.kt | 6 -- .../core/navigation/feedback/FeedbackType.kt | 13 ---- data/feedback/build.gradle.kts | 4 +- .../feedback/DefaultFeedbackRepository.kt | 65 ++++++++++--------- .../feedback/converters/CardInfoConverter.kt | 22 +++++-- .../feedback/di/FeedbackRepositoryModule.kt | 14 +++- domain/feedback/build.gradle.kts | 1 + .../tangem/domain/feedback/FeedbackManager.kt | 8 +++ .../domain/feedback/GetCardInfoUseCase.kt | 23 +++++++ .../feedback/GetFeedbackEmailUseCase.kt | 19 +++--- .../tangem/domain/feedback/models/CardInfo.kt | 3 + .../feedback/models/FeedbackEmailType.kt | 12 ++-- .../feedback/repository/FeedbackRepository.kt | 18 +++-- .../utils/EmailMessageBodyResolver.kt | 30 ++++++--- .../utils/EmailMessageTitleResolver.kt | 8 +-- .../feedback/utils/EmailSubjectResolver.kt | 15 ++--- .../com/tangem/domain/redux/LegacyAction.kt | 3 +- features/details/impl/build.gradle.kts | 1 + .../preview/PreviewDetailsComponent.kt | 4 +- .../features/details/model/DetailsModel.kt | 24 ++++++- .../features/details/utils/ItemsBuilder.kt | 24 ++++--- features/send/impl/build.gradle.kts | 1 + .../presentation/viewmodel/SendViewModel.kt | 1 + .../intents/WalletWarningsClickIntents.kt | 9 ++- 49 files changed, 414 insertions(+), 182 deletions(-) delete mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt delete mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt delete mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt create mode 100644 domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 038f6f9a6a..aec8799cdd 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -18,6 +18,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -112,6 +113,8 @@ interface ApplicationEntryPoint { fun getDetailsFeatureToggles(): DetailsFeatureToggles + fun getGetCardInfoUseCase(): GetCardInfoUseCase + fun getUrlOpener(): UrlOpener fun getShareManager(): ShareManager diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 2b87ffb1f0..d92288e204 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -33,6 +33,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -183,6 +184,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase get() = entryPoint.getSaveBlockchainErrorUseCase() + private val getCardInfoUseCase: GetCardInfoUseCase + get() = entryPoint.getGetCardInfoUseCase() + private val detailsFeatureToggles: DetailsFeatureToggles get() = entryPoint.getDetailsFeatureToggles() @@ -284,6 +288,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, + getFeedbackEmailUseCase = getFeedbackEmailUseCase, + getCardInfoUseCase = getCardInfoUseCase, + assetLoader = assetLoader, detailsFeatureToggles = detailsFeatureToggles, urlOpener = urlOpener, shareManager = shareManager, diff --git a/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt index ed99833b04..9f2f6bb335 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt @@ -7,13 +7,14 @@ import com.tangem.domain.common.TapWorkarounds import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.foregroundActivityObserver -import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.withForegroundActivity import kotlinx.coroutines.launch @@ -36,16 +37,24 @@ class LegacyFeedbackManager( private var sessionFeedbackFile: File? = null private var sessionLogsFile: File? = null - fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { + fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) { if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { - mainScope.launch { + scope.launch { + val getCardInfo = suspend { + scanResponse ?: error("ScanResponse must be not null") + store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull() + ?: error("CardInfo must be not null") + } + val email = getFeedbackEmailUseCase( - when (feedbackData) { - is FeedbackEmail -> FeedbackEmailType.DirectUserRequest - is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter + type = when (feedbackData) { + is FeedbackEmail -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo()) + is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter(cardInfo = getCardInfo()) is ScanFailsEmail -> FeedbackEmailType.ScanningProblem - is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem - else -> FeedbackEmailType.DirectUserRequest + is SendTransactionFailedEmail -> { + FeedbackEmailType.TransactionSendingProblem(cardInfo = getCardInfo()) + } + else -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo()) }, ) @@ -66,12 +75,28 @@ class LegacyFeedbackManager( subject = activity.getString(feedbackData.subjectResId), message = feedbackData.joinTogether(activity, infoHolder), file = getLogFile(activity), - onFail = onFail, ) } } } + fun sendEmail(type: FeedbackEmailType) { + if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) error("LOCAL_LOGS feature toggle must be enabled") + + scope.launch { + val email = getFeedbackEmailUseCase(type = type) + + store.inject(DaggerGraphState::emailSender).send( + email = EmailSender.Email( + address = email.address, + subject = email.subject, + message = email.message, + attachment = email.file, + ), + ) + } + } + fun openChat(config: ChatConfig, feedbackData: FeedbackData) { chatManager.open( config = config, diff --git a/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt index 680468bef1..d013937813 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt @@ -1,13 +1,13 @@ package com.tangem.tap.common.feedback -import com.tangem.core.navigation.feedback.FeedbackManager -import com.tangem.core.navigation.feedback.FeedbackType +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.tap.store import timber.log.Timber internal class ProxyFeedbackManager : FeedbackManager { - override fun sendEmail(type: FeedbackType) { + override fun sendEmail(type: FeedbackEmailType) { val manager = store.state.globalState.feedbackManager if (manager == null) { @@ -15,14 +15,6 @@ internal class ProxyFeedbackManager : FeedbackManager { return } - val data = when (type) { - is FeedbackType.Feedback -> FeedbackEmail() - is FeedbackType.RateCanBeBetter -> RateCanBeBetterEmail() - is FeedbackType.ScanFails -> ScanFailsEmail() - is FeedbackType.SendTransactionFailed -> SendTransactionFailedEmail(type.error) - is FeedbackType.Support -> FeedbackEmail() - } - - manager.sendEmail(data) + manager.sendEmail(type) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 4aa4c0b58b..ba7a0ac095 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -81,7 +81,7 @@ sealed class GlobalAction : Action { data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() - data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() + data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index f1d80acbf8..957b756803 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -69,7 +69,10 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } } is GlobalAction.SendEmail -> { - store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) + store.state.globalState.feedbackManager?.sendEmail( + feedbackData = action.feedbackData, + scanResponse = action.scanResponse, + ) } is GlobalAction.OpenChat -> { val globalState = store.state.globalState diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 7cf0e08bf1..0b0af1a01b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -1,8 +1,11 @@ package com.tangem.tap.common.redux.legacy +import com.tangem.blockchain.common.AmountType +import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.redux.LegacyAction import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.feedback.RateCanBeBetterEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState @@ -19,7 +22,10 @@ internal object LegacyMiddleware { { action -> when (action) { is LegacyAction.SendEmailRateCanBeBetter -> { - store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail()) + store.state.globalState.feedbackManager?.sendEmail( + feedbackData = RateCanBeBetterEmail(), + scanResponse = action.scanResponse, + ) } is LegacyAction.StartOnboardingProcess -> { store.dispatch( @@ -28,8 +34,28 @@ internal object LegacyMiddleware { } is LegacyAction.SendEmailTransactionFailed -> { if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { + + val amount = action.amount?.convertToAmount(action.cryptoCurrency) + store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( + error = BlockchainErrorInfo( + errorMessage = action.errorMessage, + blockchainId = action.cryptoCurrency.network.id.value, + derivationPath = action.cryptoCurrency.network.derivationPath.value, + destinationAddress = action.destinationAddress.orEmpty(), + tokenSymbol = if (amount?.type is AmountType.Token) { + amount.currencySymbol + } else { + "" + }, + amount = amount?.value?.stripZeroPlainString() ?: "unknown", + fee = action.fee?.convertToAmount(action.cryptoCurrency) + ?.value?.stripZeroPlainString() ?: "unknown", + ), + ) + store.state.globalState.feedbackManager?.sendEmail( - SendTransactionFailedEmail(action.errorMessage), + feedbackData = SendTransactionFailedEmail(action.errorMessage), + scanResponse = action.scanResponse, ) } else { scope.launch { @@ -46,7 +72,8 @@ internal object LegacyMiddleware { ) } store.state.globalState.feedbackManager?.sendEmail( - SendTransactionFailedEmail(action.errorMessage), + feedbackData = SendTransactionFailedEmail(action.errorMessage), + scanResponse = null, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index ed7c3c4c90..410f81e1c6 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -65,7 +65,7 @@ internal object ScanFailsDialog { } customView.findViewById(R.id.request_support_button)?.setOnClickListener { Analytics.send(Basic.ButtonSupport(sourceAnalytics)) - store.dispatch(GlobalAction.SendEmail(ScanFailsEmail())) + store.dispatch(GlobalAction.SendEmail(feedbackData = ScanFailsEmail(), scanResponse = null)) } customView.findViewById(R.id.cancel_button)?.setOnClickListener { store.dispatchDialogHide() diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index 2db04791cf..9c29c8598b 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -1,11 +1,11 @@ package com.tangem.tap.di import android.content.Context -import com.tangem.core.navigation.feedback.FeedbackManager import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.feedback.FeedbackManager import com.tangem.tap.common.feedback.ProxyFeedbackManager import com.tangem.tap.common.finisher.AndroidAppFinisher import com.tangem.tap.common.settings.IntentSettingsManager diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt index 033e322536..04f4f03fb8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import android.content.Context +import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.repository.FeedbackRepository @@ -15,6 +16,12 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object FeedbackDomainModule { + @Provides + @Singleton + fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase { + return GetCardInfoUseCase(feedbackRepository = feedbackRepository) + } + @Provides @Singleton fun provideGetFeedbackEmailUseCase( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index e0122f240b..4cc6783772 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -142,7 +142,13 @@ internal class DetailsViewModel( private fun sendFeedback() { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings)) - store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) + store.dispatchOnMain( + GlobalAction.SendEmail( + feedbackData = FeedbackEmail(), + scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } private fun navigateToAppSettings() { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt index 5e0028b00d..89ffc2161c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt @@ -7,15 +7,20 @@ import androidx.core.view.MenuProvider import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -class OnboardingMenuProvider : MenuProvider { +class OnboardingMenuProvider( + private val scanResponseProvider: Provider, +) : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.menu_onboarding, menu) } @@ -24,7 +29,12 @@ class OnboardingMenuProvider : MenuProvider { R.id.menu_item_chat_support -> { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) // changed on email support [REDACTED_TASK_KEY] - store.dispatch(GlobalAction.SendEmail(SupportInfo())) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SupportInfo(), + scanResponse = scanResponseProvider(), + ), + ) true } else -> false diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt index 032cce60d3..0922d3f023 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt @@ -9,6 +9,8 @@ import com.tangem.tap.common.extensions.show import com.tangem.tap.common.transitions.HomeToOnboardingTransition import com.tangem.tap.features.BaseStoreFragment import com.tangem.tap.features.onboarding.OnboardingMenuProvider +import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingMainBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding @@ -32,7 +34,13 @@ abstract class BaseOnboardingFragment : BaseStoreFragment(R.layout.fragment_o (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) } - override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() + override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( + scanResponseProvider = Provider { + store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: store.state.detailsState.scanResponse + ?: error("ScanResponse must be not null") + }, + ) protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { lavConfetti.show(show) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt index 9ba9492c54..594dd4e77c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.redux.AppState import org.rekotlin.Action @@ -35,7 +35,10 @@ private fun internalReduce(action: Action, state: AppState): TwinCardsState { ) } is TwinCardsAction.SetStepOfScreen -> { - state = state.copy(currentStep = action.step) + state = state.copy( + currentStep = action.step, + welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse, + ) } is TwinCardsAction.SetUserUnderstand -> { state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index a5608d4371..fcc38388ac 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.common.WalletManager -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance @@ -29,6 +29,7 @@ data class TwinCardsState( val balanceNonCriticalError: TapError? = null, val balanceCriticalError: TapError? = null, val showConfetti: Boolean = false, + val welcomeOnlyScanResponse: ScanResponse? = null, ) : StateType { val steps: List diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index 2cbf5949d1..e1e0a58536 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.animation.OvershootInterpolator import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.transition.TransitionManager import coil.load @@ -25,12 +26,14 @@ import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment 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.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding import dagger.hilt.android.AndroidEntryPoint @@ -61,6 +64,15 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( } } + override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( + scanResponseProvider = Provider { + store.state.twinCardsState.welcomeOnlyScanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: store.state.detailsState.scanResponse + ?: error("ScanResponse must be not null") + }, + ) + @Suppress("MagicNumber") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index b66984164b..e0577994f9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -46,6 +46,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.* import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog import com.tangem.tap.mainScope import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding @@ -115,7 +116,12 @@ class OnboardingWalletFragment : ) } - override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider() + override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( + scanResponseProvider = Provider { + store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: error("ScanResponse must be not null") + }, + ) private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) { val viewBackupCount = flCardsContainer.childCount - 1 @@ -507,7 +513,13 @@ class OnboardingWalletFragment : onOpenChat = { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) // changed on email support [REDACTED_TASK_KEY] - store.dispatch(GlobalAction.SendEmail(SupportInfo())) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SupportInfo(), + scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) }, onOpenUriClick = { uri -> store.dispatchOpenUrl(uri.toString()) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt index ace9edcf19..45c8213362 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt @@ -23,7 +23,13 @@ object WalletActivationErrorDialog { setNegativeButton(R.string.common_support) { _, _ -> // changed on email support [REDACTED_TASK_KEY] Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) - store.dispatch(GlobalAction.SendEmail(SupportInfo())) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SupportInfo(), + scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } setOnDismissListener { store.dispatchDialogHide() } setCancelable(false) 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 5663b7cec7..35ee864987 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 @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.core.TangemSdkError +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered @@ -185,12 +186,16 @@ sealed class SendAction : SendScreenAction { ) : Dialog() sealed class SendTransactionFails : Dialog() { - data class CardSdkError(val error: TangemSdkError) : Dialog() - data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog() + data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog() + data class BlockchainSdkError( + val error: com.tangem.blockchain.common.BlockchainSdkError, + val scanResponse: ScanResponse, + ) : Dialog() } data class RequestFeeError( val error: com.tangem.blockchain.common.BlockchainSdkError, + val scanResponse: ScanResponse, val onRetry: () -> Unit, ) : Dialog() 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 e3096c6a4f..ea4092c252 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 @@ -1,6 +1,9 @@ package com.tangem.tap.features.send.redux.middlewares -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero @@ -92,6 +95,7 @@ class RequestFeeMiddleware { dispatch( SendAction.Dialog.RequestFeeError( error = blockchainSdkError, + scanResponse = scanResponse, onRetry = { dispatch(FeeAction.RequestFee) }, ), ) 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 94378ee16e..ecb0e071d8 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 @@ -288,11 +288,25 @@ private fun sendTransaction( val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext - dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError)) + dispatch( + SendAction.Dialog.SendTransactionFails.CardSdkError( + error = tangemSdkError, + scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) + .selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } is BlockchainSdkError.CreateAccountUnderfunded -> { // from XLM, XRP, Polkadot - dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) + dispatch( + SendAction.Dialog.SendTransactionFails.BlockchainSdkError( + error = error, + scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) + .selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } is BlockchainSdkError.Kaspa.UtxoAmountError -> { dispatch( @@ -331,7 +345,14 @@ private fun sendTransaction( ) } else -> { - dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) + dispatch( + SendAction.Dialog.SendTransactionFails.BlockchainSdkError( + error = error, + scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) + .selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt index cf27394348..bb8a5ea183 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt @@ -23,7 +23,12 @@ object RequestFeeErrorDialog { setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ -> Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) - store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SendTransactionFailedEmail(errorMessage), + scanResponse = dialog.scanResponse, + ), + ) } setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() } setNeutralButton(R.string.common_cancel) { _, _ -> } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index 5687caa590..a768ea033b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -8,6 +8,7 @@ import com.tangem.common.module.ModuleMessageConverter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.domain.models.scan.ScanResponse import com.tangem.sdk.extensions.localizedDescription import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.feedback.SendTransactionFailedEmail @@ -21,21 +22,21 @@ import com.tangem.wallet.R */ object SendTransactionFailsDialog { fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog { - return create(context, dialog.error.localizedDescription(context)) + return create(context, dialog.error.localizedDescription(context), dialog.scanResponse) } fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog { val errorConverter = BlockchainSdkErrorConverter(context) - return create(context, errorConverter.convert(dialog.error)) + return create(context, errorConverter.convert(dialog.error), dialog.scanResponse) } - private fun create(context: Context, errorMessage: String): AlertDialog { + private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): AlertDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.alert_failed_to_send_transaction_title) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ -> Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) - store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) + store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse)) } setPositiveButton(R.string.common_cancel) { _, _ -> } setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } 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 1e0ddd0477..6ca986bc42 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 @@ -16,6 +16,8 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -82,6 +84,8 @@ data class DaggerGraphState( val blockchainSDKFactory: BlockchainSDKFactory? = null, val emailSender: EmailSender? = null, val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, + val getFeedbackEmailUseCase: GetFeedbackEmailUseCase? = null, + val getCardInfoUseCase: GetCardInfoUseCase? = null, val assetLoader: AssetLoader? = null, val detailsFeatureToggles: DetailsFeatureToggles? = null, val stakingRouter: StakingRouter? = null, diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt deleted file mode 100644 index 2f92f40ad9..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/DummyFeedbackManager.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.core.navigation.feedback - -class DummyFeedbackManager : FeedbackManager { - - override fun sendEmail(type: FeedbackType) { - /* no-op */ - } -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt deleted file mode 100644 index a76d2704e6..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackManager.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.core.navigation.feedback - -interface FeedbackManager { - - fun sendEmail(type: FeedbackType) -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt b/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt deleted file mode 100644 index b1470f5e4d..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/feedback/FeedbackType.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.core.navigation.feedback - -sealed class FeedbackType { - data object RateCanBeBetter : FeedbackType() - - data object ScanFails : FeedbackType() - - data class SendTransactionFailed(val error: String) : FeedbackType() - - data object Feedback : FeedbackType() - - data object Support : FeedbackType() -} \ No newline at end of file diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 4847e15ddd..e936310c64 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -37,7 +37,9 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + + implementation(projects.libs.blockchainSdk) } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 4bf2f7eb2a..b5313ac608 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -9,12 +9,14 @@ import com.tangem.data.feedback.converters.CardInfoConverter import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import timber.log.Timber @@ -25,43 +27,47 @@ import java.io.StringWriter /** * Implementation of [FeedbackRepository] * - * @property appPreferencesStore application preferences store - * @property userWalletsStore user wallets store - * @property walletManagersStore wallet managers store - * @property context context for getting app version + * @property appPreferencesStore application preferences store + * @property userWalletsListManager user wallets list manager + * @property walletManagersStore wallet managers store + * @property context context for getting app version + * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultFeedbackRepository( private val appPreferencesStore: AppPreferencesStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListManager: UserWalletsListManager, private val walletManagersStore: WalletManagersStore, private val context: Context, + private val dispatchers: CoroutineDispatcherProvider, ) : FeedbackRepository { private val blockchainsErrors = MutableStateFlow>(emptyMap()) - override suspend fun getUserWalletsInfo(): UserWalletsInfo { + override suspend fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse) + + override suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo { return UserWalletsInfo( - selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, - totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"), + selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated", + totalUserWallets = userWalletsListManager.walletsCount, ) } - override suspend fun getCardInfo(): CardInfo { - return CardInfoConverter.convert(value = getSelectedUserWallet()) - } - - override suspend fun getBlockchainInfoList(): List { + override suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List { return walletManagersStore - .getAllSync(userWalletId = getSelectedUserWallet().walletId) + .getAllSync(userWalletId = userWalletId) .map(BlockchainInfoConverter::convert) } - override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? { + override suspend fun getBlockchainInfo( + userWalletId: UserWalletId, + blockchainId: String, + derivationPath: String?, + ): BlockchainInfo? { return walletManagersStore .getSyncOrNull( - userWalletId = getSelectedUserWallet().walletId, + userWalletId = userWalletId, blockchain = Blockchain.fromId(blockchainId), derivationPath = derivationPath, ) @@ -77,18 +83,18 @@ internal class DefaultFeedbackRepository( } override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { + val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected") + blockchainsErrors.update { it.toMutableMap().apply { - put(getSelectedUserWallet().walletId, error) + put(userWallet.walletId, error) } } } - override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? { - return blockchainsErrors.value[getSelectedUserWallet().walletId].also { - if (it == null) { - Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}") - } + override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { + return blockchainsErrors.value[userWalletId].also { + if (it == null) Timber.e("Blockchain error info is null for $userWalletId") } } @@ -99,7 +105,7 @@ internal class DefaultFeedbackRepository( } override suspend fun createLogFile(logs: String): File? { - return try { + return runCatching(dispatchers.io) { val file = File(context.filesDir, LOGS_FILE) file.delete() file.createNewFile() @@ -113,8 +119,8 @@ internal class DefaultFeedbackRepository( fileWriter.close() file - } catch (ex: Exception) { - Timber.e(ex, "Logs file isn't created") + }.getOrElse { + Timber.e(it, "Logs file isn't created") null } } @@ -130,11 +136,6 @@ internal class DefaultFeedbackRepository( ) } - private fun getSelectedUserWallet(): UserWallet { - return userWalletsStore.selectedUserWalletOrNull - ?: error("UserWallet is not selected") - } - private companion object { const val LOGS_FILE = "logs.txt" } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index 7d16454467..c4aafd2bf2 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -2,28 +2,36 @@ package com.tangem.data.feedback.converters import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.converter.Converter /** - * Converter from [UserWallet] to [CardInfo] + * Converter from [ScanResponse] to [CardInfo] * [REDACTED_AUTHOR] */ -internal object CardInfoConverter : Converter { +internal object CardInfoConverter : Converter { - override fun convert(value: UserWallet): CardInfo { - return with(value.scanResponse) { + override fun convert(value: ScanResponse): CardInfo { + return with(value) { CardInfo( + userWalletId = createUserWalletId(scanResponse = value), cardId = card.cardId, firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) }, - isImported = value.isImported, - isStart2Coin = value.scanResponse.card.isStart2Coin, + isImported = value.card.wallets.any(CardDTO.Wallet::isImported), + isStart2Coin = value.card.isStart2Coin, ) } } + + private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? { + return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build() + } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt index 5a00938959..393a30cf25 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt @@ -3,9 +3,10 @@ package com.tangem.data.feedback.di import android.content.Context import com.tangem.data.feedback.DefaultFeedbackRepository import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,10 +22,17 @@ internal object FeedbackRepositoryModule { @Singleton fun provideFeedbackRepository( appPreferencesStore: AppPreferencesStore, - userWalletsStore: UserWalletsStore, + userWalletsListManager: UserWalletsListManager, walletManagersStore: WalletManagersStore, @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, ): FeedbackRepository { - return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context) + return DefaultFeedbackRepository( + appPreferencesStore = appPreferencesStore, + userWalletsListManager = userWalletsListManager, + walletManagersStore = walletManagersStore, + context = context, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 6a21560035..28fe483e00 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -13,5 +13,6 @@ dependencies { implementation(deps.jodatime) implementation(projects.core.res) + implementation(projects.domain.models) implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt new file mode 100644 index 0000000000..bc18ec66ec --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.FeedbackEmailType + +interface FeedbackManager { + + fun sendEmail(type: FeedbackEmailType) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt new file mode 100644 index 0000000000..9d9fc6995c --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.feedback + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.models.scan.ScanResponse + +/** + * UseCase for creating 'CardInfo' + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +class GetCardInfoUseCase( + private val feedbackRepository: FeedbackRepository, +) { + + suspend operator fun invoke(scanResponse: ScanResponse): Either { + return catch { feedbackRepository.getCardInfo(scanResponse) } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt index 50998a4ed8..acdcc492b7 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt @@ -25,23 +25,21 @@ class GetFeedbackEmailUseCase( private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { - val cardInfo = feedbackRepository.getCardInfo() - val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs()) return FeedbackEmail( - address = getAddress(cardInfo), - subject = emailSubjectResolver.resolve(type, cardInfo), - message = createMessage(type, cardInfo), + address = getAddress(type.cardInfo), + subject = emailSubjectResolver.resolve(type), + message = createMessage(type), file = feedbackRepository.createLogFile(logs = formattedLogs), ) } - private fun getAddress(cardInfo: CardInfo): String { - return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + private fun getAddress(cardInfo: CardInfo?): String { + return if (cardInfo?.isStart2Coin == true) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL } - private suspend fun createMessage(type: FeedbackEmailType, cardInfo: CardInfo): String { + private suspend fun createMessage(type: FeedbackEmailType): String { return StringBuilder().apply { val title = emailMessageTitleResolver.resolve(type) append(title) @@ -50,9 +48,7 @@ class GetFeedbackEmailUseCase( appendDisclaimerIfNeeded(type) - skipLine() - - val body = emailMessageBodyResolver.resolve(type, cardInfo) + val body = emailMessageBodyResolver.resolve(type) append(body) }.toString() } @@ -62,6 +58,7 @@ class GetFeedbackEmailUseCase( this } else { append(resources.getString(R.string.feedback_data_collection_message)) + skipLine() } } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt index 7ae84ac608..2e7e710cc1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt @@ -1,6 +1,9 @@ package com.tangem.domain.feedback.models +import com.tangem.domain.wallets.models.UserWalletId + data class CardInfo( + val userWalletId: UserWalletId?, val cardId: String, val firmwareVersion: String, val cardBlockchain: String?, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 6bbd5a9670..1e4971e4cf 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -7,15 +7,19 @@ package com.tangem.domain.feedback.models */ sealed interface FeedbackEmailType { + val cardInfo: CardInfo? + /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ - data object DirectUserRequest : FeedbackEmailType + data class DirectUserRequest(override val cardInfo: CardInfo) : FeedbackEmailType /** User rate the app as "can be better" */ - data object RateCanBeBetter : FeedbackEmailType + data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType /** User has problem with scanning */ - data object ScanningProblem : FeedbackEmailType + data object ScanningProblem : FeedbackEmailType { + override val cardInfo: CardInfo? = null + } /** User has problem with sending transaction */ - data object TransactionSendingProblem : FeedbackEmailType + data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index c9d6806eea..1b5de9c70c 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -1,23 +1,29 @@ package com.tangem.domain.feedback.repository import com.tangem.domain.feedback.models.* +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWalletId import java.io.File interface FeedbackRepository { - suspend fun getUserWalletsInfo(): UserWalletsInfo + suspend fun getCardInfo(scanResponse: ScanResponse): CardInfo - suspend fun getCardInfo(): CardInfo + suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo - suspend fun getBlockchainInfoList(): List - - suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? + suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List fun getPhoneInfo(): PhoneInfo + suspend fun getBlockchainInfo( + userWalletId: UserWalletId, + blockchainId: String, + derivationPath: String?, + ): BlockchainInfo? + fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) - suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? + suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? suspend fun getAppLogs(): List diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 8574fb1010..a9bb1a64dd 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -16,25 +16,33 @@ internal class EmailMessageBodyResolver( private val feedbackRepository: FeedbackRepository, ) { - /** Resolve email message body by [type] using [cardInfo] */ - suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) { + /** Resolve email message body by [type] */ + suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) { when (type) { - FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo) - FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo) - FeedbackEmailType.ScanningProblem -> addScanningProblemBody() - FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo) + is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo) + is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) + is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() + is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) } return build() } private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { - addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId)) addDelimiter() addCardInfo(cardInfo) addDelimiter() - addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) - addDelimiter() + + if (cardInfo.userWalletId != null) { + val blockchainInfoList = feedbackRepository.getBlockchainInfoList(cardInfo.userWalletId) + + if (blockchainInfoList.isNotEmpty()) { + addBlockchainInfoList(blockchainInfoList = blockchainInfoList) + addDelimiter() + } + } + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } @@ -46,9 +54,11 @@ internal class EmailMessageBodyResolver( addCardInfo(cardInfo) addDelimiter() - val blockchainError = feedbackRepository.getBlockchainErrorInfo() + val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( + userWalletId = userWalletId, blockchainId = blockchainError.blockchainId, derivationPath = blockchainError.derivationPath, ) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 38c983c617..6ab001b775 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -16,10 +16,10 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { /** Resolve email message title by [type] */ fun resolve(type: FeedbackEmailType): String { return when (type) { - FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support - FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative - FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed - FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support + is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative + is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed + is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed } .let(resources::getString) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index 392fa7d2f4..beb89739bd 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -2,7 +2,6 @@ package com.tangem.domain.feedback.utils import android.content.res.Resources import com.tangem.domain.feedback.R -import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.FeedbackEmailType /** @@ -14,19 +13,19 @@ import com.tangem.domain.feedback.models.FeedbackEmailType */ internal class EmailSubjectResolver(private val resources: Resources) { - /** Resolve email message body by [type] using [cardInfo] */ - fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String { + /** Resolve email message body by [type] */ + fun resolve(type: FeedbackEmailType): String { return when (type) { - FeedbackEmailType.DirectUserRequest -> { - if (cardInfo.isStart2Coin) { + is FeedbackEmailType.DirectUserRequest -> { + if (type.cardInfo.isStart2Coin) { R.string.feedback_subject_support } else { R.string.feedback_subject_support_tangem } } - FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative - FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed - FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative + is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed + is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed } .let(resources::getString) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index 3e0b022afd..68c6988923 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -8,7 +8,7 @@ import java.math.BigDecimal sealed interface LegacyAction : Action { - object SendEmailRateCanBeBetter : LegacyAction + data class SendEmailRateCanBeBetter(val scanResponse: ScanResponse) : LegacyAction /** * Initiate an onboarding process. @@ -34,5 +34,6 @@ sealed interface LegacyAction : Action { val fee: BigDecimal?, val destinationAddress: String?, val errorMessage: String, + val scanResponse: ScanResponse, ) : LegacyAction } \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 1edff72903..c1f2899eff 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.models) + implementation(projects.domain.feedback) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.card) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index ccbc7f2409..5d2d2b67d0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -3,7 +3,6 @@ package com.tangem.features.details.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.decompose.navigation.DummyRouter -import com.tangem.core.navigation.feedback.DummyFeedbackManager import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM @@ -19,8 +18,7 @@ internal class PreviewDetailsComponent : DetailsComponent { ItemsBuilder( router = DummyRouter(), urlOpener = DummyUrlOpener(), - feedbackManager = DummyFeedbackManager(), - ).buldAll(isWalletConnectAvailable = true) + ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}) } private val previewFooter = DetailsFooterUM( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index f1bbd73bdb..8bd54c48e9 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -5,7 +5,11 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsItemUM @@ -33,6 +37,9 @@ internal class DetailsModel @Inject constructor( private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, private val paramsContainer: ParamsContainer, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val feedbackManager: FeedbackManager, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -66,7 +73,22 @@ internal class DetailsModel @Inject constructor( false } - items.value = itemsBuilder.buldAll(isWalletConnectAvailable) + items.value = itemsBuilder.buildAll( + isWalletConnectAvailable = isWalletConnectAvailable, + onSupportClick = ::sendFeedback, + ) + } + + private fun sendFeedback() { + modelScope.launch { + val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse + ?: error("Selected wallet is null") + + val cardInfo = getCardInfoUseCase(scanResponse = scanResponse).getOrNull() + ?: error("CardInfo must be not null") + + feedbackManager.sendEmail(type = FeedbackEmailType.DirectUserRequest(cardInfo)) + } } private suspend fun updateState(items: ImmutableList) { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 6ce516821b..ecb5d93de7 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -3,8 +3,6 @@ package com.tangem.features.details.utils import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.feedback.FeedbackManager -import com.tangem.core.navigation.feedback.FeedbackType import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference @@ -21,18 +19,18 @@ import javax.inject.Inject internal class ItemsBuilder @Inject constructor( private val router: Router, private val urlOpener: UrlOpener, - private val feedbackManager: FeedbackManager, ) { - suspend fun buldAll(isWalletConnectAvailable: Boolean): ImmutableList = buildList { - buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) - buildUserWalletListBlock().let(::add) - buildShopBlock().let(::add) - buildSettingsBlock().let(::add) - buildSupportBlock().let(::add) - }.toImmutableList() + fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList = + buildList { + buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) + buildUserWalletListBlock().let(::add) + buildShopBlock().let(::add) + buildSettingsBlock().let(::add) + buildSupportBlock(onSupportClick).let(::add) + }.toImmutableList() - private suspend fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { return if (isWalletConnectAvailable) { DetailsItemUM.WalletConnect( onClick = { router.push(AppRoute.WalletConnectSessions) }, @@ -83,7 +81,7 @@ internal class ItemsBuilder @Inject constructor( }.toImmutableList(), ) - private fun buildSupportBlock(): DetailsItemUM = DetailsItemUM.Basic( + private fun buildSupportBlock(onClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic( id = "support", items = persistentListOf( DetailsItemUM.Basic.Item( @@ -91,7 +89,7 @@ internal class ItemsBuilder @Inject constructor( block = BlockUM( text = resourceReference(R.string.details_send_feedback), iconRes = R.drawable.ic_comment_24, - onClick = { feedbackManager.sendEmail(FeedbackType.Feedback) }, + onClick = onClick, ), ), DetailsItemUM.Basic.Item( diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 3937749519..9a3de6d8cb 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.feedback) implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning.models) implementation(projects.domain.settings) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index cc5d43f0ec..6fd5854725 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -530,6 +530,7 @@ internal class SendViewModel @Inject constructor( fee = feeValue, destinationAddress = recipient, errorMessage = errorMessage, + scanResponse = userWallet.scanResponse, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 087ec87d35..c27292d883 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -204,7 +204,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( viewModelScope.launch(dispatchers.main) { neverToSuggestRateAppUseCase() - reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter) + reduxStateHolder.dispatch( + LegacyAction.SendEmailRateCanBeBetter( + scanResponse = getSelectedUserWallet()?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } } @@ -250,7 +255,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - private suspend fun getSelectedUserWallet(): UserWallet? { + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { Timber.e( From 0b671b4ac1326308495ba86d0a0f5a8a7025f0bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Jun 2024 21:33:54 +0500 Subject: [PATCH 081/223] Updated on 2026-08-14 --- .../common/adapter/UnknownEnumMoshiAdapter.kt | 10 +- .../datasource/api/stakekit/StakeKitApi.kt | 7 +- .../models/request/EnterActionRequestBody.kt | 4 +- .../models/request/YieldBalanceRequestBody.kt | 1 - .../response/model/YieldBalanceWrapperDTO.kt | 225 +++++++++--------- .../di/StakingBalanceStoreModule.kt | 21 ++ .../local/token/DefaultStakingBalanceStore.kt | 26 ++ .../local/token/StakingBalanceStore.kt | 13 + 8 files changed, 184 insertions(+), 123 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index 2d46248977..2e6adf8fe7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.api.common.adapter -import com.squareup.moshi.* +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi import com.squareup.moshi.adapters.EnumJsonAdapter -import com.tangem.datasource.api.stakekit.models.response.model.* +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO @@ -24,8 +27,7 @@ fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN, StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN, - YieldBalanceWrapperDTO.BalanceDTO.BalanceType::class.java to - YieldBalanceWrapperDTO.BalanceDTO.BalanceType.UNKNOWN, + BalanceDTO.BalanceType::class.java to BalanceDTO.BalanceType.UNKNOWN, StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, StakingActionStatusDTO::class.java to StakingActionStatusDTO.UNKNOWN, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 9818ec8742..17e656903f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO @@ -29,16 +30,16 @@ interface StakeKitApi { @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false, ): ApiResponse - @GET("yields/balances") + @POST("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, ): ApiResponse> - @GET("yields/{integrationId}/balances") + @POST("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( @Path("integrationId") integrationId: String, @Body body: YieldBalanceRequestBody, - ): ApiResponse + ): ApiResponse> @GET("tokens") suspend fun getTokens(): ApiResponse> diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt index 677ff1a33f..6d06f46f5c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO data class EnterActionRequestBody( @Json(name = "integrationId") @@ -27,7 +27,7 @@ data class EnterActionRequestBody( @Json(name = "duration") val duration: String? = null, @Json(name = "nfts") - val nfts: List? = null, + val nfts: List? = null, @Json(name = "ledgerWalletAPICompatible") val ledgerWalletAPICompatible: Boolean? = null, @Json(name = "tronResource") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index bbfe66a6af..3d4947c14a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -5,7 +5,6 @@ import com.squareup.moshi.Json data class YieldBalanceRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") val args: YieldBalanceRequestArgs, - @Json(name = "integrationId") val integrationId: String? = null, ) { data class YieldBalanceRequestArgs( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index 2fc44ede6e..5be46fc967 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -12,130 +12,129 @@ data class YieldBalanceWrapperDTO( val balances: List, @Json(name = "integrationId") val integrationId: String?, +) + +@JsonClass(generateAdapter = true) +data class BalanceDTO( + @Json(name = "groupId") + val groupId: String, + @Json(name = "type") + val type: BalanceType, + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "date") + val date: DateTime?, + @Json(name = "pricePerShare") + val pricePerShare: BigDecimal, + @Json(name = "pendingActions") + val pendingActions: List, + @Json(name = "token") + val tokenDTO: TokenDTO, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @Json(name = "providerId") + val providerId: String?, ) { + enum class BalanceType { + @Json(name = "available") + AVAILABLE, + + @Json(name = "staked") + STAKED, + + @Json(name = "unstaking") + UNSTAKING, + + @Json(name = "unstaked") + UNSTAKED, + + @Json(name = "preparing") + PREPARING, + + @Json(name = "rewards") + REWARDS, + + @Json(name = "locked") + LOCKED, + + @Json(name = "unlocking") + UNLOCKING, + + UNKNOWN, + } + @JsonClass(generateAdapter = true) - data class BalanceDTO( - @Json(name = "groupId") - val groupId: String, + data class PendingAction( @Json(name = "type") - val type: BalanceType, - @Json(name = "amount") - val amount: BigDecimal, - @Json(name = "date") - val date: DateTime?, - @Json(name = "pricePerShare") - val pricePerShare: BigDecimal, - @Json(name = "pendingActions") - val pendingActions: List, - @Json(name = "token") - val tokenDTO: TokenDTO, - @Json(name = "validatorAddress") - val validatorAddress: String?, - @Json(name = "validatorAddresses") - val validatorAddresses: List?, - @Json(name = "providerId") - val providerId: String?, + val type: StakingActionTypeDTO, + @Json(name = "passthrough") + val passthrough: String, + @Json(name = "args") + val args: PendingActionArgs?, ) { - - enum class BalanceType { - @Json(name = "available") - AVAILABLE, - - @Json(name = "staked") - STAKED, - - @Json(name = "unstaking") - UNSTAKING, - - @Json(name = "unstaked") - UNSTAKED, - - @Json(name = "preparing") - PREPARING, - - @Json(name = "rewards") - REWARDS, - - @Json(name = "locked") - LOCKED, - - @Json(name = "unlocking") - UNLOCKING, - - UNKNOWN, - } - @JsonClass(generateAdapter = true) - data class PendingAction( - @Json(name = "type") - val type: StakingActionTypeDTO, - @Json(name = "passthrough") - val passthrough: String, - @Json(name = "args") - val args: PendingActionArgs?, + data class PendingActionArgs( + @Json(name = "amount") + val amount: Amount?, + @Json(name = "duration") + val duration: Duration?, + @Json(name = "validatorAddress") + val validatorAddress: Required?, + @Json(name = "validatorAddresses") + val validatorAddresses: Required?, + @Json(name = "nfts") + val nfts: List?, + @Json(name = "tronResource") + val tronResource: TronResource?, + @Json(name = "signatureVerification") + val signatureVerification: Required?, ) { @JsonClass(generateAdapter = true) - data class PendingActionArgs( - @Json(name = "amount") - val amount: Amount?, - @Json(name = "duration") - val duration: Duration?, - @Json(name = "validatorAddress") - val validatorAddress: Required?, - @Json(name = "validatorAddresses") - val validatorAddresses: Required?, - @Json(name = "nfts") - val nfts: List?, - @Json(name = "tronResource") - val tronResource: TronResource?, - @Json(name = "signatureVerification") - val signatureVerification: Required?, - ) { - @JsonClass(generateAdapter = true) - data class Amount( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: BigDecimal?, - @Json(name = "maximum") - val maximum: BigDecimal?, - ) + data class Amount( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: BigDecimal?, + @Json(name = "maximum") + val maximum: BigDecimal?, + ) - @JsonClass(generateAdapter = true) - data class Duration( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: Int?, - @Json(name = "maximum") - val maximum: Int?, - ) + @JsonClass(generateAdapter = true) + data class Duration( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: Int?, + @Json(name = "maximum") + val maximum: Int?, + ) - @JsonClass(generateAdapter = true) - data class Nft( - @Json(name = "baycId") - val baycId: Required?, - @Json(name = "maycId") - val maycId: Required?, - @Json(name = "bakcId") - val bakcId: Required?, - ) + @JsonClass(generateAdapter = true) + data class Nft( + @Json(name = "baycId") + val baycId: Required?, + @Json(name = "maycId") + val maycId: Required?, + @Json(name = "bakcId") + val bakcId: Required?, + ) - @JsonClass(generateAdapter = true) - data class TronResource( - @Json(name = "required") - val required: Boolean, - @Json(name = "options") - val options: List, - ) - } + @JsonClass(generateAdapter = true) + data class TronResource( + @Json(name = "required") + val required: Boolean, + @Json(name = "options") + val options: List, + ) } - - @JsonClass(generateAdapter = true) - data class Required( - @Json(name = "required") - val required: Boolean, - ) } + + @JsonClass(generateAdapter = true) + data class Required( + @Json(name = "required") + val required: Boolean, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt new file mode 100644 index 0000000000..21d78318c1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultStakingBalanceStore +import com.tangem.datasource.local.token.StakingBalanceStore +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 StakingBalanceStoreModule { + + @Provides + @Singleton + fun provideStakingBalanceStore(): StakingBalanceStore { + return DefaultStakingBalanceStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt new file mode 100644 index 0000000000..c905706748 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import kotlinx.coroutines.flow.Flow + +internal class DefaultStakingBalanceStore( + private val dataStore: StringKeyDataStore>, +) : StakingBalanceStore { + + override fun get(): Flow> { + return dataStore.get(STAKING_BALANCE_KEY) + } + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + } + + override suspend fun store(items: List) { + return dataStore.store(STAKING_BALANCE_KEY, items) + } + + companion object { + private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt new file mode 100644 index 0000000000..b7260b675b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import kotlinx.coroutines.flow.Flow + +interface StakingBalanceStore { + + fun get(): Flow> + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file From 1d75e884c86ffd0fd76a49a3eb14bbf48932dbd9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 21 Jun 2024 21:35:19 +0500 Subject: [PATCH 082/223] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 18 ++- .../models/request/YieldBalanceRequestBody.kt | 1 + .../local/token/DefaultStakingBalanceStore.kt | 33 ++++- .../local/token/StakingBalanceStore.kt | 13 +- data/staking/build.gradle.kts | 7 + .../data/staking/DefaultStakingRepository.kt | 136 ++++++++++++++++-- .../converters/YieldBalanceConverter.kt | 16 +++ .../converters/YieldBalanceListConverter.kt | 18 +++ .../data/staking/di/StakingDataModule.kt | 6 + domain/staking/build.gradle.kts | 1 + .../domain/staking/model/YieldBalance.kt | 21 +++ .../domain/staking/model/YieldBalanceList.kt | 5 + .../FetchStakingYieldBalanceUseCase.kt | 25 ++++ .../staking/GetStakingYieldBalanceUseCase.kt | 30 ++++ .../staking/repositories/StakingRepository.kt | 33 ++++- 15 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 790bb01a7a..2a6fb034b9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,10 +1,6 @@ package com.tangem.tap.di.domain -import com.tangem.domain.settings.* -import com.tangem.domain.staking.GetStakingAvailabilityUseCase -import com.tangem.domain.staking.GetStakingEntryInfoUseCase -import com.tangem.domain.staking.FetchStakingTokensUseCase -import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -47,4 +43,16 @@ internal object StakingDomainModule { stakingRepository = stakingRepository, ) } + + @Provides + @Singleton + fun provideFetchStakingYieldBalanceUseCase(stakingRepository: StakingRepository): FetchStakingYieldBalanceUseCase { + return FetchStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + } + + @Provides + @Singleton + fun provideGetStakingYieldBalanceUseCase(stakingRepository: StakingRepository): GetStakingYieldBalanceUseCase { + return GetStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index 3d4947c14a..bbfe66a6af 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.Json data class YieldBalanceRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") val args: YieldBalanceRequestArgs, + @Json(name = "integrationId") val integrationId: String? = null, ) { data class YieldBalanceRequestArgs( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index c905706748..c88970b238 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -1,25 +1,50 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map internal class DefaultStakingBalanceStore( - private val dataStore: StringKeyDataStore>, + private val dataStore: StringKeyDataStore>, ) : StakingBalanceStore { - override fun get(): Flow> { + override fun get(): Flow> { return dataStore.get(STAKING_BALANCE_KEY) } - override suspend fun getSyncOrNull(): List? { + override suspend fun getSyncOrNull(): List? { return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) } - override suspend fun store(items: List) { + override suspend fun store(items: List) { return dataStore.store(STAKING_BALANCE_KEY, items) } + override fun get(integrationId: String): Flow> { + return dataStore.get(STAKING_BALANCE_KEY) + .map { balances -> + balances.filter { it.integrationId == integrationId } + .flatMap { it.balances } + } + } + + override suspend fun getSyncOrNull(integrationId: String): List? { + return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + ?.firstOrNull { it.integrationId == integrationId }?.balances + } + + override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) { + val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + ?.toMutableList() + ?.addOrReplace(item) { item.integrationId == integrationId } + ?: listOf(item) + + return dataStore.store(STAKING_BALANCE_KEY, balances) + } + companion object { private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index b7260b675b..0a9ea06c9e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -1,13 +1,20 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import kotlinx.coroutines.flow.Flow interface StakingBalanceStore { - fun get(): Flow> + fun get(): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(): List? - suspend fun store(items: List) + suspend fun store(items: List) + + fun get(integrationId: String): Flow> + + suspend fun getSyncOrNull(integrationId: String): List? + + suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 186d53da6e..e0b3458636 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -12,10 +12,17 @@ android { dependencies { + /** Core modules */ implementation(projects.core.datasource) implementation(projects.core.utils) + + /** Common modules */ + implementation(projects.data.common) + + /** Domain modules */ implementation(projects.domain.tokens.models) implementation(projects.domain.staking) + implementation(projects.domain.wallets.models) // region DI diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index f0e5a33941..379877723a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,23 +2,33 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.data.staking.converters.StakingNetworkTypeConverter -import com.tangem.data.staking.converters.TokenConverter -import com.tangem.data.staking.converters.YieldConverter +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.converters.* import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingYieldsStore: StakingYieldsStore, + private val stakingBalanceStore: StakingBalanceStore, + private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : StakingRepository { @@ -31,6 +41,10 @@ internal class DefaultStakingRepository( tokenConverter = tokenConverter, ) + private val yieldBalanceConverter = YieldBalanceConverter() + + private val yieldBalanceListConverter = YieldBalanceListConverter() + override fun isStakingSupported(currencyId: String): Boolean { return integrationIds.contains(currencyId) } @@ -94,9 +108,93 @@ internal class DefaultStakingRepository( } } + override suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + refresh: Boolean, + ) = withContext(dispatchers.io) { + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val result = stakeKitApi.getSingleYieldBalance( + integrationId = integrationId, + body = getBalanceRequestData(networkStatus), + ).getOrThrow() + + stakingBalanceStore.store( + integrationId, + YieldBalanceWrapperDTO( + balances = result, + integrationId = integrationId, + ), + ) + }, + ) + } + + override fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + ): Flow> = channelFlow { + launch(dispatchers.io) { + stakingBalanceStore.get(integrationId) + .collectLatest { + send(yieldBalanceConverter.convertList(it)) + } + } + + withContext(dispatchers.io) { + fetchSingleYieldBalance( + userWalletId, + networkStatus, + integrationId, + ) + } + }.cancellable() + + override suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + refresh: Boolean, + ) = withContext(dispatchers.io) { + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val result = stakeKitApi.getMultipleYieldBalances( + networks.map(::getBalanceRequestData), + ).getOrThrow() + + stakingBalanceStore.store(result) + }, + ) + } + + override fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + ): Flow> = channelFlow { + launch(dispatchers.io) { + stakingBalanceStore.get() + .collectLatest { send(yieldBalanceListConverter.convertList(it)) } + } + + withContext(dispatchers.io) { + fetchMultiYieldBalance( + userWalletId, + networks, + integrationId, + ) + } + }.cancellable() + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { - return yields - .find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } + return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } private suspend fun getEnabledYields(): List? { @@ -104,6 +202,28 @@ internal class DefaultStakingRepository( return yields.map { yieldConverter.convert(it) } } + private fun getBalanceRequestData(networkStatus: NetworkStatus): YieldBalanceRequestBody { + val networkAddress = when (val network = networkStatus.value) { + NetworkStatus.MissedDerivation -> null + is NetworkStatus.NoAccount -> network.address + is NetworkStatus.Unreachable -> network.address + is NetworkStatus.Verified -> network.address + } + val address = networkAddress?.defaultAddress?.value.orEmpty() + return YieldBalanceRequestBody( + addresses = Address( + address = address, + additionalAddresses = null, // todo fill additional addresses metadata if needed + explorerUrl = "", // todo fill exporer url + ), + args = YieldBalanceRequestBody.YieldBalanceRequestArgs( + validatorAddresses = listOf(), // todo add validator addresses + ), + ) + } + + private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" + companion object { private val integrationIds = setOf( Blockchain.Solana.toCoinId(), diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt new file mode 100644 index 0000000000..7a3182cc87 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.utils.converter.Converter + +internal class YieldBalanceConverter : Converter { + override fun convert(value: BalanceDTO): YieldBalance { + return YieldBalance( + type = BalanceType.valueOf(value.type.name), + amount = value.amount, + pricePerShare = value.pricePerShare, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt new file mode 100644 index 0000000000..d0c4eea878 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.utils.converter.Converter + +internal class YieldBalanceListConverter : Converter { + + internal val converter by lazy(LazyThreadSafetyMode.NONE) { + YieldBalanceConverter() + } + + override fun convert(value: YieldBalanceWrapperDTO): YieldBalanceList { + return YieldBalanceList( + balances = converter.convertList(value.balances), + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 0b47b925d1..16c1bed364 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -1,7 +1,9 @@ package com.tangem.data.staking.di +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -20,12 +22,16 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingTokenStore: StakingYieldsStore, + stakingBalanceStore: StakingBalanceStore, dispatchers: CoroutineDispatcherProvider, + cacheRegistry: CacheRegistry, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingYieldsStore = stakingTokenStore, + stakingBalanceStore = stakingBalanceStore, dispatchers = dispatchers, + cacheRegistry = cacheRegistry, ) } } \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index fc6ced152c..38ec70430c 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(deps.kotlin.serialization) implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt new file mode 100644 index 0000000000..817743ac7c --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.staking.model + +import java.math.BigDecimal + +data class YieldBalance( + val type: BalanceType, + val amount: BigDecimal, + val pricePerShare: BigDecimal, +) + +enum class BalanceType { + AVAILABLE, + STAKED, + UNSTAKING, + UNSTAKED, + PREPARING, + REWARDS, + LOCKED, + UNLOCKING, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt new file mode 100644 index 0000000000..931e8fa879 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.staking.model + +data class YieldBalanceList( + val balances: List, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt new file mode 100644 index 0000000000..46f0fdf101 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId + +class FetchStakingYieldBalanceUseCase( + private val stakingRepository: StakingRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + refresh: Boolean = false, + ): Either = Either.catch { + stakingRepository.fetchSingleYieldBalance( + userWalletId = userWalletId, + networkStatus = networkStatus, + integrationId = integrationId, + refresh = refresh, + ) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt new file mode 100644 index 0000000000..8821767a15 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetStakingYieldBalanceUseCase( + private val stakingRepository: StakingRepository, +) { + + operator fun invoke( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + ): EitherFlow> { + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + networkStatus = networkStatus, + integrationId = integrationId, + ).map, Either>> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 2e1fe82f5d..5eb34854c5 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -1,9 +1,10 @@ package com.tangem.domain.staking.repositories -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.* import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface StakingRepository { @@ -19,4 +20,30 @@ interface StakingRepository { cryptoCurrencyId: CryptoCurrency.ID, symbol: String, ): StakingAvailability + + suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + refresh: Boolean = false, + ) + + fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + networkStatus: NetworkStatus, + integrationId: String, + ): Flow> + + suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + refresh: Boolean = false, + ) + + fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + networks: Set, + integrationId: String, + ): Flow> } \ No newline at end of file From e1ef1c4b8a4c666e85c371bbcf9b06ce32ea68e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Jun 2024 13:40:04 +0400 Subject: [PATCH 083/223] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 9 ++ .../com/tangem/common/routing/AppRoute.kt | 5 + domain/wallets/build.gradle.kts | 6 +- .../wallets/usecase/GetUserWalletUseCase.kt | 14 +++ .../details/utils/UserWalletsFetcher.kt | 7 +- .../impl/DefaultRenameWalletComponent.kt | 92 +++++++++++++++ .../impl/DefaultWalletSettingsComponent.kt | 74 ++++++++++++ .../walletsettings/di/ComponentModule.kt | 26 +++++ .../feature/walletsettings/di/ModelModule.kt | 20 ++++ .../model/WalletSettingsModel.kt | 107 ++++++++++++++++++ 10 files changed, 352 insertions(+), 8 deletions(-) create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index dc76ad9db0..48df79739b 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.DisclaimerRouter @@ -44,6 +45,7 @@ import javax.inject.Inject @Suppress("LongParameterList") internal class ChildFactory @Inject constructor( private val detailsComponentFactory: DetailsComponent.Factory, + private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -160,6 +162,13 @@ internal class ChildFactory @Inject constructor( is AppRoute.PushNotification -> { route.asFragmentChild(Provider { pushNotificationRouter.entryFragment() }) } + is AppRoute.WalletSettings -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = WalletSettingsComponent.Params(route.userWalletId), + componentFactory = walletSettingsComponentFactory, + ) + } } } diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 2b6831a967..8bd5b8a2c1 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -200,4 +200,9 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object PushNotification : AppRoute(path = "/push_notification") + + @Serializable + data class WalletSettings( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index bd175a01fa..9972fdc9c7 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { // endregion // region Domain modules + api(projects.domain.core) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) @@ -28,9 +29,4 @@ dependencies { implementation(deps.tangem.blockchain) // android-library implementation(deps.tangem.card.core) // 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/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index 9323fd5423..a8b14fe269 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -1,12 +1,17 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either +import arrow.core.left import arrow.core.raise.either import arrow.core.raise.ensureNotNull +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { @@ -17,4 +22,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa raise(GetUserWalletError.UserWalletNotFound) } } + + @OptIn(ExperimentalCoroutinesApi::class) + fun invokeFlow(userWalletId: UserWalletId): EitherFlow { + return userWalletsListManager.userWallets.transformLatest { userWallets -> + userWallets.firstOrNull { it.walletId == userWalletId } + ?.let { emit(it.right()) } + ?: emit(GetUserWalletError.UserWalletNotFound.left()) + } + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index 73b54461fb..e606030df5 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -1,10 +1,11 @@ package com.tangem.features.details.utils import arrow.core.Either +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError @@ -30,6 +31,7 @@ internal class UserWalletsFetcher @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val router: Router, private val messageSender: UiMessageSender, ) { @@ -82,8 +84,7 @@ internal class UserWalletsFetcher @Inject constructor( } private fun navigateToWalletSettings(userWalletId: UserWalletId) { - val message = stringReference("Wallet settings have not yet been implemented: $userWalletId") - messageSender.send(SnackbarMessage(message)) + router.push(AppRoute.WalletSettings(userWalletId)) } sealed class Error { diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt new file mode 100644 index 0000000000..a4c28c649c --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.walletsettings.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.RenameWalletUseCase +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.ui.RenameWalletDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DefaultRenameWalletComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: RenameWalletComponent.Params, + private val renameWalletUseCase: RenameWalletUseCase, +) : RenameWalletComponent, AppComponentContext by context { + + private val userWalletId = params.userWalletId + private val currentWalletName = params.currentName + + override val doOnDismiss: () -> Unit = params.onDismiss + + private val stateFlow: MutableStateFlow = MutableStateFlow( + value = RenameWalletUM( + walletNameValue = TextFieldValue(text = params.currentName), + isNameCorrect = false, + updateValue = ::updateValue, + onConfirm = { renameWallet(params.userWalletId) }, + ), + ) + + @Composable + override fun Dialog() { + val model by stateFlow.collectAsStateWithLifecycle() + + RenameWalletDialog( + model = model, + onDismiss = doOnDismiss, + ) + } + + private fun updateValue(value: TextFieldValue) { + stateFlow.update { + it.copy( + walletNameValue = value, + isNameCorrect = value.text.isNotBlank() && value.text != currentWalletName, + ) + } + } + + private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { + val newName = stateFlow.value.walletNameValue + val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() + + if (maybeError != null) { + val message = when (maybeError) { + UpdateWalletError.DataError -> resourceReference( + id = R.string.common_unknown_error, + ) + UpdateWalletError.NameAlreadyExists -> resourceReference( + id = R.string.user_wallet_list_rename_popup_error_already_exists, + formatArgs = wrappedList(newName), + ) + } + + messageSender.send(message = SnackbarMessage(message)) + } + + doOnDismiss() + } + + @AssistedFactory + interface Factory : RenameWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: RenameWalletComponent.Params, + ): DefaultRenameWalletComponent + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt new file mode 100644 index 0000000000..39ed1d65a5 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -0,0 +1,74 @@ +package com.tangem.feature.walletsettings.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.model.WalletSettingsModel +import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultWalletSettingsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: WalletSettingsComponent.Params, + private val renameWalletComponentFactory: RenameWalletComponent.Factory, +) : WalletSettingsComponent, AppComponentContext by context { + + private val model: WalletSettingsModel = getOrCreateModel(params) + + private val dialog = childSlot( + source = model.dialogNavigation, + serializer = DialogConfig.serializer(), + handleBackButton = true, + childFactory = ::dialogChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + val dialog by dialog.subscribeAsState() + + WalletSettingsScreen( + modifier = modifier, + state = state, + dialog = { dialog.child?.instance?.Dialog() }, + ) + } + + private fun dialogChild( + dialogConfig: DialogConfig, + componentContext: ComponentContext, + ): ComposableDialogComponent = when (dialogConfig) { + is DialogConfig.RenameWallet -> { + renameWalletComponentFactory.create( + context = childByContext(componentContext), + params = RenameWalletComponent.Params( + userWalletId = dialogConfig.userWalletId, + currentName = dialogConfig.currentName, + onDismiss = model.dialogNavigation::dismiss, + ), + ) + } + } + + @AssistedFactory + interface Factory : WalletSettingsComponent.Factory { + override fun create( + context: AppComponentContext, + params: WalletSettingsComponent.Params, + ): DefaultWalletSettingsComponent + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt new file mode 100644 index 0000000000..90ff4e1255 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.walletsettings.di + +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.component.impl.DefaultRenameWalletComponent +import com.tangem.feature.walletsettings.component.impl.DefaultWalletSettingsComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindWalletSettingsComponentFactory( + factory: DefaultWalletSettingsComponent.Factory, + ): WalletSettingsComponent.Factory + + @Binds + @Singleton + fun bindRenameWalletComponentFactory(factory: DefaultRenameWalletComponent.Factory): RenameWalletComponent.Factory +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt new file mode 100644 index 0000000000..f7c93024a5 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.walletsettings.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.walletsettings.model.WalletSettingsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(WalletSettingsModel::class) + fun provideWalletSettingsModel(model: WalletSettingsModel): Model +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt new file mode 100644 index 0000000000..7926ce7f87 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -0,0 +1,107 @@ +package com.tangem.feature.walletsettings.model + +import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ComponentScoped +internal class WalletSettingsModel @Inject constructor( + private val router: Router, + private val messageSender: UiMessageSender, + private val getWalletUseCase: GetUserWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val itemsBuilder: ItemsBuilder, + private val paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val params: WalletSettingsComponent.Params = paramsContainer.require() + val dialogNavigation = SlotNavigation() + + val state: MutableStateFlow = MutableStateFlow( + value = WalletSettingsUM( + popBack = router::pop, + items = persistentListOf(), + ), + ) + + init { + getWalletUseCase.invokeFlow(params.userWalletId) + .distinctUntilChanged() + .onEach { maybeWallet -> + val wallet = maybeWallet.getOrElse { error -> + error( + """ + Failed to get user wallet + |- User wallet ID: $params + |- Cause: $error + """.trimIndent(), + ) + } + + state.update { value -> + value.copy(items = buildItems(wallet, dialogNavigation)) + } + } + .launchIn(modelScope) + } + + private fun buildItems( + userWallet: UserWallet, + dialogNavigation: SlotNavigation, + ): PersistentList = itemsBuilder.buildItems( + walletName = userWallet.name, + renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, + forgetWallet = { forgetWallet(userWallet.walletId) }, + ) + + private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { + val config = DialogConfig.RenameWallet( + userWalletId = userWallet.walletId, + currentName = userWallet.name, + ) + + dialogNavigation.activate(config) + } + + private fun forgetWallet(userWalletId: UserWalletId) = modelScope.launch { + val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + messageSender.send( + message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), + ) + + return@launch + } + + if (hasUserWallets) { + router.pop() + } else { + router.replaceAll(AppRoute.Home) + } + } +} \ No newline at end of file From 03f6c241ac44f2937ab48764846f2f06729a5ab1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 14:13:06 +0400 Subject: [PATCH 084/223] Updated on 2026-08-14 --- .../block/information/InformationBlock.kt | 37 +++- .../information/InformationBlockContent.kt | 64 +++++-- .../core/ui/components/rows/ArrowRow.kt | 168 ++++++++++++++++++ .../core/ui/components/rows/BlockchainRow.kt | 99 +++++++++++ .../core/ui/components/rows/ChainRow.kt | 117 ++++++++++++ .../core/ui/components/rows/RowComponents.kt | 139 +++++++++++++++ .../components/rows/model/BlockchainRowUM.kt | 21 +++ .../ui/components/rows/model/ChainRowUM.kt | 19 ++ .../com/tangem/core/ui/res/TangemDimens.kt | 2 + .../com/tangem/core/ui/res/TangemTheme.kt | 2 +- .../com/tangem/core/ui/utils/DensityUtils.kt | 18 ++ .../com/tangem/core/ui/utils/PathUtils.kt | 10 ++ 12 files changed, 680 insertions(+), 16 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt index 110b0048aa..027109f071 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -97,6 +97,7 @@ private fun Preview_Grid() { }, content = { GridItems( + itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4), items = persistentListOf( stringReference("Fist item"), stringReference("Second item"), @@ -104,6 +105,7 @@ private fun Preview_Grid() { itemContent = { PreviewItem(text = it) }, + horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) }, ) @@ -141,6 +143,7 @@ private fun Preview_List() { itemContent = { PreviewItem(it) }, + verticalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) }, ) @@ -159,7 +162,7 @@ private fun Preview_Plain() { ) { TooltipText( text = stringReference("Title"), - onInfoClick = { /* [REDACTED_TODO_COMMENT]*/ }, + onInfoClick = { }, ) Text( @@ -176,6 +179,38 @@ private fun Preview_Plain() { } } +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_Tree() { + TangemThemePreview { + InformationBlock( + title = { + TooltipText( + text = stringReference("Tree title"), + onInfoClick = { }, + ) + }, + content = { + ArrowRowItems( + itemPadding = PaddingValues(vertical = TangemTheme.dimens.spacing4), + items = persistentListOf( + stringReference("Fist item"), + stringReference("Second item"), + stringReference("Third item"), + ), + rootContent = { + PreviewItem(stringReference("Root")) + }, + itemContent = { + PreviewItem(it) + }, + ) + }, + ) + } +} + @Composable private fun PreviewItem(text: TextReference) { Box( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt index 02b783e839..1a40b55863 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt @@ -8,26 +8,30 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @Composable -fun InformationBlockContentScope.ListItems( +inline fun InformationBlockContentScope.ListItems( items: ImmutableList, itemContent: @Composable BoxScope.(T) -> Unit, modifier: Modifier = Modifier, + itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), + horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + verticalArragement: Arrangement.Vertical = Arrangement.Top, ) { Column( - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8, Alignment.Top), + modifier = modifier.fillMaxWidth(), + horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArragement, ) { items.fastForEach { item -> Box( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(itemPadding) + .fillMaxWidth(), ) { itemContent(item) } @@ -36,10 +40,13 @@ fun InformationBlockContentScope.ListItems( } @Composable -fun InformationBlockContentScope.GridItems( +inline fun InformationBlockContentScope.GridItems( items: ImmutableList, itemContent: @Composable BoxScope.(T) -> Unit, modifier: Modifier = Modifier, + itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), + verticalAlignment: Alignment.Vertical = Alignment.Top, + horizontalArragement: Arrangement.Horizontal = Arrangement.Start, ) { val rowItems by remember(items) { derivedStateOf { @@ -57,15 +64,15 @@ fun InformationBlockContentScope.GridItems( ) { rowItems.fastForEach { row -> Row( - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start), + modifier = Modifier.fillMaxWidth(), + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArragement, ) { row.fastForEach { item -> Box( - modifier = Modifier.weight(1f), + modifier = Modifier + .padding(itemPadding) + .weight(1f), contentAlignment = Alignment.Center, ) { itemContent(item) @@ -74,4 +81,33 @@ fun InformationBlockContentScope.GridItems( } } } +} + +@Composable +inline fun InformationBlockContentScope.ArrowRowItems( + items: ImmutableList, + rootContent: @Composable BoxScope.() -> Unit, + itemContent: @Composable BoxScope.(T) -> Unit, + modifier: Modifier = Modifier, + itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + content = rootContent, + ) + + items.forEachIndexed { index, item -> + ArrowRow( + modifier = Modifier.fillMaxWidth(), + content = { itemContent(item) }, + contentPadding = itemPadding, + isLastItem = index == items.size - 1, + ) + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt new file mode 100644 index 0000000000..0e594fbd91 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt @@ -0,0 +1,168 @@ +package com.tangem.core.ui.components.rows + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.* + +@Composable +inline fun ArrowRow( + isLastItem: Boolean, + content: @Composable() (BoxScope.() -> Unit), + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), +) { + val density = LocalDensity.current.density + val defaultRowHeight = TangemTheme.dimens.size0 + var itemHeight by remember { mutableStateOf(defaultRowHeight) } + + Row( + modifier = modifier.onSizeChanged { size -> + val height = size.height.toFloat() + if (height != itemHeight.toPx(density)) { + itemHeight = convertPxToDp(px = height, density = density) + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + ChildArrow( + childHeight = itemHeight, + isLastChild = isLastItem, + ) + + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + ) { + content() + } + } +} + +@Suppress("LongParameterList") +@Immutable +private class ChildArrowScope( + val figureRect: Rect, + val arrowHeadRect: Rect, + val curvedArrowRect: Rect, + val arrowStrokeWidth: Float, + val arrowHeadRadius: Float, + val strokeColor: Color, + drawScope: DrawScope, +) : DrawScope by drawScope + +@Composable +fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { + val figureWidth = TangemTheme.dimens.size40 + + val strokeColor = TangemTheme.colors.stroke.secondary + val arrowStrokeWidthDp = TangemTheme.dimens.size1 + + val arrowHeadRadiusDp = TangemTheme.dimens.size1 + + val figureRectDp = DpRect( + origin = DpOffset.Zero, + size = DpSize(width = TangemTheme.dimens.size40, height = childHeight), + ) + + val arrowHeadSize = DpSize( + width = TangemTheme.dimens.size6, + height = TangemTheme.dimens.size6, + ) + val arrowHeadRectDp = DpRect( + origin = DpOffset( + x = figureWidth - arrowHeadSize.width, + y = figureRectDp.size.center.y - arrowHeadSize.center.y, + ), + size = arrowHeadSize, + ) + + val curvedArrowRectDp = DpRect( + top = figureRectDp.top, + left = TangemTheme.dimens.size18, + right = figureRectDp.right - arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + + Canvas( + modifier = Modifier + .width(figureWidth) + .height(childHeight), + ) { + val scope = ChildArrowScope( + figureRect = figureRectDp.toRect(), + arrowHeadRect = arrowHeadRectDp.toRect(), + curvedArrowRect = curvedArrowRectDp.toRect(), + arrowStrokeWidth = arrowStrokeWidthDp.toPx(), + arrowHeadRadius = arrowHeadRadiusDp.toPx(), + strokeColor = strokeColor, + drawScope = this, + ) + + scope.drawCurveArrow() + scope.drawArrowHead() + + if (!isLastChild) { + scope.drawArrowLine() + } + } +} + +private fun ChildArrowScope.drawArrowHead() { + val arrowHeadPath = Path().apply { + moveTo(arrowHeadRect.centerRight) + lineTo(arrowHeadRect.topLeft) + lineTo(arrowHeadRect.bottomLeft) + close() + } + val paint = Paint().apply { + color = strokeColor + style = PaintingStyle.Fill + pathEffect = PathEffect.cornerPathEffect(arrowHeadRadius) + } + drawIntoCanvas { canvas -> + canvas.drawOutline( + outline = Outline.Generic(arrowHeadPath), + paint = paint, + ) + } +} + +private fun ChildArrowScope.drawCurveArrow() { + val curveArrowPath = Path().apply { + moveTo(curvedArrowRect.topLeft) + quadraticBezierTo( + control = curvedArrowRect.bottomLeft, + end = curvedArrowRect.bottomRight, + ) + } + drawPath( + path = curveArrowPath, + color = strokeColor, + style = Stroke(width = arrowStrokeWidth), + ) +} + +private fun ChildArrowScope.drawArrowLine() { + drawLine( + color = strokeColor, + start = curvedArrowRect.topLeft, + end = Offset(curvedArrowRect.left, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt new file mode 100644 index 0000000000..8d6e4165e3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +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.R +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4) + * */ +@Composable +fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier) { + RowContentContainer( + modifier = modifier + .heightIn(min = TangemTheme.dimens.size52) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing8, + ), + icon = { + RowIcon( + modifier = Modifier.size(TangemTheme.dimens.size22), + resId = model.icon.resId, + isColored = model.icon.isColored, + showAccentBadge = model.isAccented, + ) + }, + text = { + RowTitleAndSubtitle( + title = model.name, + subtitle = model.type, + accentTitle = model.usePrimaryTextColor, + accentSubtitle = model.isAccented, + ) + }, + action = action, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvider::class) state: BlockchainRowUM) { + TangemThemePreview { + ArrowRow( + modifier = Modifier.background(TangemTheme.colors.background.primary), + isLastItem = false, + content = { + BlockchainRow( + model = state, + action = { + TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true) + }, + ) + }, + ) + } +} + +private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + BlockchainRowUM( + name = stringReference("BNB BEACON CHAIN"), + type = stringReference("BEP20"), + icon = BlockchainRowUM.Icon(R.drawable.ic_bsc_16, isColored = false), + isAccented = true, + usePrimaryTextColor = false, + ), + BlockchainRowUM( + name = stringReference("1234567890111213141516171819"), + type = stringReference("BEP20"), + icon = BlockchainRowUM.Icon(R.drawable.ic_bsc_16, isColored = false), + isAccented = false, + usePrimaryTextColor = true, + ), + BlockchainRowUM( + name = stringReference("BNB BEACON CHAIN"), + type = stringReference("1234567890111213141516171819"), + icon = BlockchainRowUM.Icon(R.drawable.img_bsc_22, isColored = true), + isAccented = false, + usePrimaryTextColor = false, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt new file mode 100644 index 0000000000..24645b9c17 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -0,0 +1,117 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +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.R +import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1608-1147&t=ewlXfWwbDnRhjw4B-4) + * */ +@Composable +fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composable BoxScope.() -> Unit = {}) { + RowContentContainer( + modifier = modifier + .heightIn(min = TangemTheme.dimens.size68) + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing14, + ), + icon = { + RowIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + resId = model.icon.resId, + isColored = model.icon.isColored, + showAccentBadge = false, + ) + }, + text = { + RowTitleAndSubtitle( + title = model.name, + subtitle = model.type, + accentTitle = true, + accentSubtitle = false, + ) + }, + action = action, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_ChainRow(@PreviewParameter(ChainRowParameterProvider::class) state: ChainRowUM) { + TangemThemePreview { + ChainRow( + modifier = Modifier.background(TangemTheme.colors.background.primary), + model = state, + action = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = { /* [REDACTED_TODO_COMMENT]*/ }, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + tint = TangemTheme.colors.icon.informative, + painter = painterResource(id = R.drawable.ic_chevron_24), + contentDescription = null, + ) + } + }, + ) + } +} + +private class ChainRowParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + ChainRowUM( + name = stringReference("Cardano"), + type = stringReference("ADA"), + icon = ChainRowUM.Icon( + resId = R.drawable.img_cardano_22, + isColored = true, + ), + ), + ChainRowUM( + name = stringReference("Binance"), + type = stringReference("BNB"), + icon = ChainRowUM.Icon( + resId = R.drawable.ic_bsc_16, + isColored = false, + ), + ), + ChainRowUM( + name = stringReference("123456789010111213141516"), + type = stringReference("BNB"), + icon = ChainRowUM.Icon( + resId = R.drawable.ic_bsc_16, + isColored = false, + ), + ), + ChainRowUM( + name = stringReference("123456789010111213141516"), + type = stringReference("123456789010111213141516"), + icon = ChainRowUM.Icon( + resId = R.drawable.ic_bsc_16, + isColored = false, + ), + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt new file mode 100644 index 0000000000..e0ac20cdc2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -0,0 +1,139 @@ +package com.tangem.core.ui.components.rows + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +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.TextOverflow +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal inline fun RowContentContainer( + icon: @Composable BoxScope.() -> Unit, + text: @Composable BoxScope.() -> Unit, + action: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8, Alignment.Start), + ) { + Box( + contentAlignment = Alignment.Center, + content = icon, + ) + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = TangemTheme.dimens.size22), + contentAlignment = Alignment.CenterStart, + content = text, + ) + Box( + modifier = Modifier + .requiredWidthIn(max = TangemTheme.dimens.size48) + .heightIn(min = TangemTheme.dimens.size24), + contentAlignment = Alignment.CenterEnd, + content = action, + ) + } +} + +@Composable +internal fun RowTitleAndSubtitle( + title: TextReference, + subtitle: TextReference, + accentTitle: Boolean, + accentSubtitle: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.weight(weight = 10f, fill = false), + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = if (accentTitle) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + modifier = Modifier.weight(weight = 4f, fill = false), + text = subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = if (accentSubtitle) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +internal fun RowIcon( + @DrawableRes resId: Int, + isColored: Boolean, + showAccentBadge: Boolean, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + if (isColored) { + Image( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = resId), + contentDescription = null, + ) + } else { + Icon( + modifier = Modifier + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .matchParentSize(), + painter = painterResource(id = resId), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + + if (showAccentBadge) { + Badge(modifier = Modifier.align(Alignment.TopEnd)) + } + } +} + +@Composable +private fun Badge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size6) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Box( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing1) + .matchParentSize() + .background( + color = TangemTheme.colors.icon.accent, + shape = CircleShape, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt new file mode 100644 index 0000000000..dbb8ae529d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt @@ -0,0 +1,21 @@ +package com.tangem.core.ui.components.rows.model + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +data class BlockchainRowUM( + val name: TextReference, + val type: TextReference, + val icon: Icon, + val isAccented: Boolean, + val usePrimaryTextColor: Boolean, +) { + + @Immutable + data class Icon( + @DrawableRes val resId: Int, + val isColored: Boolean, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt new file mode 100644 index 0000000000..58e337a805 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.components.rows.model + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +data class ChainRowUM( + val name: TextReference, + val type: TextReference, + val icon: Icon, +) { + + @Immutable + data class Icon( + @DrawableRes val resId: Int, + val isColored: Boolean, + ) +} \ 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 2ad32a8f6f..4d47c4bf0f 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 @@ -45,6 +45,7 @@ data class TangemDimens internal constructor( val size2: Dp = 2.dp, val size4: Dp = 4.dp, val size5: Dp = 5.dp, + val size6: Dp = 6.dp, val size7: Dp = 7.dp, val size8: Dp = 8.dp, val size9: Dp = 9.dp, @@ -56,6 +57,7 @@ data class TangemDimens internal constructor( val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, + val size22: Dp = 22.dp, val size24: Dp = 24.dp, val size28: Dp = 28.dp, val size30: Dp = 30.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 58552b1353..ea874c3bfb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -139,7 +139,7 @@ private fun lightThemeColors(): TangemColors { ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Light2, - secondary = TangemColorPalette.Dark5, + secondary = TangemColorPalette.Light5, transparency = TangemColorPalette.White, ), field = TangemColors.Field( diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt new file mode 100644 index 0000000000..105d2416d3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt @@ -0,0 +1,18 @@ +package com.tangem.core.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@Stable +@Composable +fun Dp.toPx(): Float = toPx(density = LocalDensity.current.density) + +@Stable +@Composable +fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.current.density) + +fun Dp.toPx(density: Float): Float = this.value * density + +fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt new file mode 100644 index 0000000000..8ab1fe470b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path + +fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y) + +fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y) + +fun Path.quadraticBezierTo(control: Offset, end: Offset) = quadraticBezierTo(control.x, control.y, end.x, end.y) \ No newline at end of file From e2cc040d689d27b6b119742a1b14da1e3e428081 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 13:37:39 +0400 Subject: [PATCH 085/223] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 - .../main/java/com/tangem/tap/MainActivity.kt | 5 - .../java/com/tangem/tap/TangemApplication.kt | 5 - .../tap/proxy/redux/DaggerGraphAction.kt | 2 - .../tap/proxy/redux/DaggerGraphReducer.kt | 1 - .../tap/proxy/redux/DaggerGraphState.kt | 4 - .../configs/feature_toggles_config.json | 4 - features/manage-tokens/.gitignore | 1 - .../ManageTokensFeatureToggles.kt | 5 - .../navigation/ExpandableState.kt | 6 - .../managetokens/navigation/ManageTokensUi.kt | 12 - .../di/ManageTokensFeatureTogglesModule.kt | 21 - .../di/ManageTokensRouterModule.kt | 18 - .../DefaultManageTokensFeatureToggles.kt | 11 - .../router/AddCustomTokenRoute.kt | 14 - .../router/AddCustomTokenRouter.kt | 29 -- .../state/AddCustomTokenState.kt | 18 - .../state/AddCustomTokenWarning.kt | 32 -- .../addcustomtoken/state/ButtonState.kt | 6 - .../state/ChooseDerivationState.kt | 13 - .../state/ChooseNetworkState.kt | 12 - .../addcustomtoken/state/CustomTokenData.kt | 18 - .../addcustomtoken/state/Derivation.kt | 9 - .../state/EnterCustomDerivationState.kt | 10 - .../addcustomtoken/state/TextFieldState.kt | 34 -- .../factory/AddCustomTokenStateFactory.kt | 395 -------------- ...stomTokenStateToCryptoCurrencyConverter.kt | 58 --- ...ntractAddressToCustomTokenDataConverter.kt | 39 -- .../FoundTokenToCustomTokenDataConverter.kt | 40 -- .../NetworkToNetworkItemStateConverter.kt | 20 - .../previewdata/AddCustomTokenPreviewData.kt | 50 -- .../ChooseDerivationPreviewData.kt | 42 -- .../ChooseNetworkCustomPreviewData.kt | 32 -- .../ui/AddCustomTokenBottomSheet.kt | 202 -------- .../addcustomtoken/ui/AddCustomTokenScreen.kt | 298 ----------- .../ui/ChooseDerivationScreen.kt | 109 ---- .../ui/ChooseNetworkCustomScreen.kt | 89 ---- .../ui/CustomDerivationDialog.kt | 43 -- .../ui/CustomTokenChooseWalletScreen.kt | 20 - .../addcustomtoken/ui/TokenTextField.kt | 81 --- .../viewmodels/AddCustomTokenClickIntents.kt | 54 -- .../viewmodels/AddCustomTokenViewModel.kt | 484 ------------------ .../common/analytics/ManageTokens.kt | 102 ---- .../presentation/common/state/AlertState.kt | 56 -- .../common/state/ChooseWalletState.kt | 29 -- .../presentation/common/state/Event.kt | 8 - .../common/state/NetworkItemState.kt | 82 --- .../presentation/common/state/WalletState.kt | 8 - .../ChooseWalletStatePreviewData.kt | 27 - .../common/ui/ChooseWalletBottomSheet.kt | 22 - .../common/ui/ChooseWalletScreen.kt | 150 ------ .../presentation/common/ui/EventEffect.kt | 18 - .../common/ui/components/Alert.kt | 52 -- .../common/ui/components/NetworkItem.kt | 167 ------ .../ui/components/SimpleSelectionBlock.kt | 65 --- .../common/utils/CurrencyUtils.kt | 18 - .../managetokens/state/ChooseNetworkState.kt | 11 - .../state/DerivationNotificationState.kt | 34 -- .../managetokens/state/ManageTokensState.kt | 27 - .../managetokens/state/QuotesState.kt | 14 - .../managetokens/state/SearchBarState.kt | 11 - .../managetokens/state/TokenButtonType.kt | 5 - .../managetokens/state/TokenIconState.kt | 10 - .../managetokens/state/TokenItemState.kt | 23 - .../state/factory/ManageTokensStateFactory.kt | 194 ------- .../NetworkToNetworkItemStateConverter.kt | 57 --- .../NetworksToChooseNetworkStateConverter.kt | 36 -- .../factory/QuotesToQuotesStateConverter.kt | 34 -- .../state/factory/TokenConverter.kt | 67 --- .../factory/TokenToCryptoCurrencyConverter.kt | 37 -- .../ChooseNetworkStatePreviewData.kt | 56 -- .../DerivationNotificationStatePreviewData.kt | 12 - .../ManageTokensStatePreviewData.kt | 45 -- .../previewdata/TokenItemStatePreviewData.kt | 77 --- .../ui/ChooseNetworkBottomSheet.kt | 24 - .../managetokens/ui/ChooseNetworkScreen.kt | 201 -------- .../managetokens/ui/ManageTokensScreen.kt | 197 ------- .../ui/components/AddCustomTokenButton.kt | 60 --- .../ui/components/DerivationNotification.kt | 138 ----- .../ui/components/PriceChangesChart.kt | 134 ----- .../managetokens/ui/components/SearchBar.kt | 138 ----- .../managetokens/ui/components/TokenButton.kt | 85 --- .../managetokens/ui/components/TokenIcon.kt | 121 ----- .../ui/components/TokenPriceChange.kt | 82 --- .../ui/components/TokenRowItem.kt | 203 -------- .../managetokens/ui/components/TokensList.kt | 67 --- .../viewmodels/ManageTokensClickIntents.kt | 31 -- .../viewmodels/ManageTokensUiEvents.kt | 8 - .../viewmodels/ManageTokensViewModel.kt | 445 ---------------- .../presentation/router/ManageTokensUiImpl.kt | 25 - .../wallet/presentation/WalletFragment.kt | 5 - .../common/preview/WalletScreenPreviewData.kt | 4 - .../router/DefaultWalletRouter.kt | 20 +- .../presentation/router/InnerWalletRouter.kt | 4 +- .../wallet/state/WalletStateController.kt | 9 +- .../wallet/state/model/WalletScreenState.kt | 4 - .../presentation/wallet/ui/WalletScreen.kt | 315 +----------- .../wallet/viewmodels/WalletViewModel.kt | 8 - 98 files changed, 15 insertions(+), 6216 deletions(-) delete mode 100644 features/manage-tokens/.gitignore delete mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt delete mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt delete mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt delete mode 100644 features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index aec8799cdd..967026bf4c 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -31,7 +31,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository @@ -63,8 +62,6 @@ interface ApplicationEntryPoint { fun getWalletConnectSessionsRepository(): WalletConnectSessionsRepository - fun getManageTokensFeatureToggles(): ManageTokensFeatureToggles - fun getScanCardProcessor(): ScanCardProcessor fun getAppCurrencyRepository(): AppCurrencyRepository diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index dddfa0a863..1a6869a4a5 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -50,7 +50,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.disclaimer.api.DisclaimerRouter -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -133,9 +132,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter - @Inject - lateinit var manageTokensUi: ManageTokensUi - @Inject lateinit var walletConnectInteractor: WalletConnectInteractor @@ -297,7 +293,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, tokenDetailsRouter = tokenDetailsRouter, - manageTokensUi = manageTokensUi, cardSdkConfigRepository = cardSdkConfigRepository, sendRouter = sendRouter, qrScanningRouter = qrScanningRouter, diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index d92288e204..6d94e69c9d 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -46,7 +46,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory @@ -112,9 +111,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletConnectSessionsRepository: WalletConnectSessionsRepository get() = entryPoint.getWalletConnectSessionsRepository() - private val manageTokensFeatureToggles: ManageTokensFeatureToggles - get() = entryPoint.getManageTokensFeatureToggles() - private val scanCardProcessor: ScanCardProcessor get() = entryPoint.getScanCardProcessor() @@ -267,7 +263,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { customTokenFeatureToggles = customTokenFeatureToggles, walletConnectRepository = walletConnect2Repository, walletConnectSessionsRepository = walletConnectSessionsRepository, - manageTokensFeatureToggles = manageTokensFeatureToggles, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, 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 cfd8b41b5f..a141560335 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 @@ -5,7 +5,6 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.disclaimer.api.DisclaimerRouter -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -23,7 +22,6 @@ sealed interface DaggerGraphAction : Action { val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, val tokenDetailsRouter: TokenDetailsRouter, - val manageTokensUi: ManageTokensUi, val cardSdkConfigRepository: CardSdkConfigRepository, val sendRouter: SendRouter, val qrScanningRouter: QrScanningRouter, 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 0f98fea5e6..77f2897541 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,7 +18,6 @@ object DaggerGraphReducer { walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, tokenDetailsRouter = action.tokenDetailsRouter, - manageTokensUi = action.manageTokensUi, cardSdkConfigRepository = action.cardSdkConfigRepository, sendRouter = action.sendRouter, qrScanningRouter = action.qrScanningRouter, 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 6ca986bc42..80779536e5 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 @@ -31,8 +31,6 @@ import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.disclaimer.api.DisclaimerRouter -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -58,8 +56,6 @@ data class DaggerGraphState( val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, - val manageTokensFeatureToggles: ManageTokensFeatureToggles? = null, - val manageTokensUi: ManageTokensUi? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, 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 1bb02865e0..1afb6daf70 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -3,10 +3,6 @@ "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" }, - { - "name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED", - "version": "undefined" - }, { "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "5.10.0" diff --git a/features/manage-tokens/.gitignore b/features/manage-tokens/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/features/manage-tokens/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt deleted file mode 100644 index e29173b5fc..0000000000 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.managetokens.featuretoggles - -interface ManageTokensFeatureToggles { - val isRedesignedScreenEnabled: Boolean -} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt deleted file mode 100644 index 34048e6f11..0000000000 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.managetokens.navigation - -enum class ExpandableState { - EXPANDED, - COLLAPSED, -} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt deleted file mode 100644 index f200ab6a3c..0000000000 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.managetokens.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.unit.Dp - -interface ManageTokensUi { - - @Suppress("TopLevelComposableFunctions") - @Composable - fun Content(onHeaderSizeChange: (Dp) -> Unit, state: State) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt deleted file mode 100644 index 7552eb21df..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.managetokens.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles -import com.tangem.managetokens.featuretoggles.DefaultManageTokensFeatureToggles -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 ManageTokensFeatureTogglesModule { - - @Provides - @Singleton - fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensFeatureToggles { - return DefaultManageTokensFeatureToggles(featureTogglesManager = featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt deleted file mode 100644 index e0ca63c3b2..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.di - -import com.tangem.features.managetokens.navigation.ManageTokensUi -import com.tangem.managetokens.presentation.router.ManageTokensUiImpl -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 ManageTokensRouterModule { - - @Binds - @Singleton - fun provideManageTokensRouter(manageTokensUiImpl: ManageTokensUiImpl): ManageTokensUi -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt deleted file mode 100644 index 967a54e950..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.managetokens.featuretoggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles - -internal class DefaultManageTokensFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : ManageTokensFeatureToggles { - override val isRedesignedScreenEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED") -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt deleted file mode 100644 index fad644416a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.router - -/** - * Add Custom Tokens screens - * @property route route string representation - */ -internal sealed class AddCustomTokenRoute(val route: String) { - object Main : AddCustomTokenRoute("$BASE_ROUTE/main") - object ChooseNetwork : AddCustomTokenRoute("$BASE_ROUTE/choose_network") - object ChooseWallet : AddCustomTokenRoute("$BASE_ROUTE/choose_wallet") - object ChooseDerivation : AddCustomTokenRoute("$BASE_ROUTE/choose_derivation") -} - -private const val BASE_ROUTE = "manage_tokens/add_custom_token" \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt deleted file mode 100644 index 4185ccddd4..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.router - -import androidx.compose.runtime.Stable -import androidx.navigation.NavController - -@Stable -internal class AddCustomTokenRouter( - private val navController: NavController, -) { - /** Pop back stack */ - fun popBackStack() { - navController.popBackStack() - } - - /** Open custom token choose network screen */ - fun openCustomTokenChooseNetwork() { - navController.navigate(AddCustomTokenRoute.ChooseNetwork.route) - } - - /** Open custom token choose derivation screen */ - fun openCustomTokenChooseDerivation() { - navController.navigate(AddCustomTokenRoute.ChooseDerivation.route) - } - - /** Open custom token choose wallet screen */ - fun openCustomTokenChooseWallet() { - navController.navigate(AddCustomTokenRoute.ChooseWallet.route) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt deleted file mode 100644 index 0eafc00654..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.event.consumedEvent -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.Event -import kotlinx.collections.immutable.ImmutableSet - -internal data class AddCustomTokenState( - val chooseWalletState: ChooseWalletState, - val chooseNetworkState: ChooseNetworkState, - val chooseDerivationState: ChooseDerivationState?, - val tokenData: CustomTokenData?, - val warnings: ImmutableSet, - val addTokenButton: ButtonState, - val showChooseWalletScreen: Boolean = false, - val event: StateEvent = consumedEvent(), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt deleted file mode 100644 index 690178c20d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.managetokens.impl.R - -/** - * Warning model of add custom token screen - * - * @property title warning description - */ -internal sealed class AddCustomTokenWarning(val title: TextReference, val subtitle: TextReference? = null) { - - object PotentialScamToken : AddCustomTokenWarning( - title = resourceReference(R.string.custom_token_validation_error_not_found_title), - subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description), - ) - - object InvalidContractAddress : AddCustomTokenWarning( - title = resourceReference(R.string.custom_token_creation_error_invalid_contract_address), - ) - - object WrongDecimals : AddCustomTokenWarning( - title = - resourceReference(R.string.custom_token_creation_error_wrong_decimals, wrappedList(MAXIMUM_DECIMAL_NUMBER)), - ) - - private companion object { - const val MAXIMUM_DECIMAL_NUMBER = 30 - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt deleted file mode 100644 index 42b515ea20..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class ButtonState( - val isEnabled: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt deleted file mode 100644 index 047fae161d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import kotlinx.collections.immutable.ImmutableList - -internal data class ChooseDerivationState( - val derivations: ImmutableList, - val selectedDerivation: Derivation?, - val enterCustomDerivationState: EnterCustomDerivationState?, - val onChooseDerivationClick: () -> Unit, - val onCloseChoosingDerivationClick: () -> Unit, - val onEnterCustomDerivation: () -> Unit, - val show: Boolean = false, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt deleted file mode 100644 index 56b993010b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import kotlinx.collections.immutable.ImmutableList - -internal data class ChooseNetworkState( - val networks: ImmutableList, - val selectedNetwork: NetworkItemState?, - val onChooseNetworkClick: () -> Unit, - val onCloseChoosingNetworkClick: () -> Unit, - val show: Boolean = false, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt deleted file mode 100644 index b481d6767a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class CustomTokenData( - val contractAddressTextField: TextFieldState, - val nameTextField: TextFieldState, - val symbolTextField: TextFieldState, - val decimalsTextField: TextFieldState, -) { - - fun isRequiredInformationProvided(): Boolean { - return contractAddressTextField.isInputValid() && nameTextField.isInputValid() && - symbolTextField.isInputValid() && decimalsTextField.isInputValid() - } - - fun isNameSymbolDecimalsDisabled(): Boolean { - return nameTextField.isDisabled() && symbolTextField.isDisabled() && decimalsTextField.isDisabled() - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt deleted file mode 100644 index 69f41a5f08..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class Derivation( - val networkName: String, - val standardType: String?, - val path: String, - val networkId: String?, - val onDerivationSelected: (Derivation) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt deleted file mode 100644 index 3b6ca1c57a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class EnterCustomDerivationState( - val value: String, - val onValueChange: (String) -> Unit, - val confirmButtonEnabled: Boolean, - val derivationIncorrect: Boolean, - val onConfirmButtonClick: () -> Unit, - val onDismiss: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt deleted file mode 100644 index 3068a0b6e9..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal sealed class TextFieldState { - object Loading : TextFieldState() - - data class Editable( - val value: String, - val isEnabled: Boolean, - val error: AddCustomTokenWarning? = null, - val onValueChange: (String) -> Unit, - val onFocusExit: () -> Unit, - ) : TextFieldState() - - fun isInputValid(): Boolean = this is Editable && value.isNotBlank() && error == null - - fun isDisabled() = this is Editable && !this.isEnabled - - fun copySealed( - value: String = (this as? Editable)?.value ?: "", - isEnabled: Boolean = (this as? Editable)?.isEnabled ?: true, - error: AddCustomTokenWarning? = (this as? Editable)?.error, - onValueChange: (String) -> Unit = (this as? Editable)?.onValueChange ?: {}, - ): TextFieldState { - return when (this) { - is Editable -> this.copy( - value = value, - isEnabled = isEnabled, - error = error, - onValueChange = onValueChange, - ) - is Loading -> this - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt deleted file mode 100644 index 6ad1ac8e68..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt +++ /dev/null @@ -1,395 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.tokens.error.AddCustomTokenError -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.managetokens.presentation.common.state.* -import com.tangem.managetokens.presentation.addcustomtoken.state.* -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents -import com.tangem.utils.Provider -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentSetOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.collections.immutable.toPersistentSet - -@Suppress("LargeClass") -internal class AddCustomTokenStateFactory( - private val currentStateProvider: Provider, - private val clickIntents: AddCustomTokenClickIntents, -) { - - fun getInitialState(): AddCustomTokenState { - return AddCustomTokenState( - chooseWalletState = ChooseWalletState.NoSelection, - chooseNetworkState = ChooseNetworkState( - networks = persistentListOf(), - selectedNetwork = null, - onChooseNetworkClick = clickIntents::onChooseNetworkClick, - onCloseChoosingNetworkClick = clickIntents::onCloseChoosingNetworkClick, - ), - chooseDerivationState = ChooseDerivationState( - derivations = persistentListOf(), - selectedDerivation = null, - enterCustomDerivationState = null, - onChooseDerivationClick = clickIntents::onChooseDerivationClick, - onCloseChoosingDerivationClick = clickIntents::onCloseChoosingDerivationClick, - onEnterCustomDerivation = clickIntents::onEnterCustomDerivation, - ), - tokenData = null, - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick), - ) - } - - fun getFullState( - suitableUserWallets: List, - allUserWallets: List, - selectedWalletId: UserWalletId?, - supportedNetworks: List, - ): AddCustomTokenState { - val derivations = getListOfDerivations(supportedNetworks) - - val chooseDerivationState = createChooseDerivationState(derivations) - val networkConverter = NetworkToNetworkItemStateConverter(clickIntents::onNetworkSelected) - val networks = supportedNetworks.map { networkConverter.convert(it) } - - val chooseWalletState = getNewChooseWalletState(allUserWallets, suitableUserWallets, selectedWalletId) - - return AddCustomTokenState( - chooseWalletState = chooseWalletState, - chooseNetworkState = ChooseNetworkState( - networks = networks.toPersistentList(), - selectedNetwork = null, - onChooseNetworkClick = clickIntents::onChooseNetworkClick, - onCloseChoosingNetworkClick = clickIntents::onBack, - ), - chooseDerivationState = chooseDerivationState, - tokenData = null, - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick), - ) - } - - private fun getListOfDerivations( - networksListToGenerateDerivations: List, - filterOnlyHardenedDerivations: Boolean = false, - ): List { - return networksListToGenerateDerivations.mapNotNull { network -> - network.derivationPath.value?.let { rawPath -> - Derivation( - networkName = network.name, - standardType = network.standardType.name, - path = rawPath, - networkId = network.backendId, - onDerivationSelected = clickIntents::onDerivationSelected, - ) - }.takeIf { derivation -> - if (filterOnlyHardenedDerivations) { - derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false - } else { - true - } - } - } - } - - private fun createChooseDerivationState(derivations: List): ChooseDerivationState? { - return if (derivations.isNotEmpty()) { - ChooseDerivationState( - derivations = derivations.toPersistentList(), - selectedDerivation = null, - enterCustomDerivationState = null, - onChooseDerivationClick = clickIntents::onChooseDerivationClick, - onCloseChoosingDerivationClick = clickIntents::onBack, - onEnterCustomDerivation = clickIntents::onEnterCustomDerivation, - ) - } else { - null - } - } - - fun updateWithNewWalletSelected( - selectedWalletId: UserWalletId, - supportedNetworks: List, - ): AddCustomTokenState { - val derivations = getListOfDerivations(supportedNetworks) - - val chooseDerivationState = createChooseDerivationState(derivations) - val networkConverter = NetworkToNetworkItemStateConverter(clickIntents::onNetworkSelected) - val networks = supportedNetworks.map { networkConverter.convert(it) } - - val currentWalletState = requireNotNull( - currentStateProvider().chooseWalletState as? ChooseWalletState.Choose, - ) { - "If user wallet was chosen, ChooseWalletState type must be Choose" - } - val selectedWalletState = currentWalletState.wallets.find { it.walletId == selectedWalletId.stringValue } - val chooseWalletState = currentWalletState.copy(selectedWallet = selectedWalletState) - - return AddCustomTokenState( - chooseWalletState = chooseWalletState, - chooseNetworkState = ChooseNetworkState( - networks = networks.toPersistentList(), - selectedNetwork = null, - onChooseNetworkClick = clickIntents::onChooseNetworkClick, - onCloseChoosingNetworkClick = clickIntents::onBack, - ), - chooseDerivationState = chooseDerivationState, - tokenData = null, - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick), - ) - } - - private fun getNewChooseWalletState( - suitableUserWallets: List, - allUserWallets: List, - selectedWalletId: UserWalletId?, - ): ChooseWalletState { - val chooseWalletState = if (suitableUserWallets.size == 1) { - ChooseWalletState.NoSelection - } else if (suitableUserWallets.isEmpty() && allUserWallets.all { !it.isMultiCurrency }) { - ChooseWalletState.Warning(ChooseWalletWarning.SINGLE_CURRENCY) - } else { - var selectedWalletState: WalletState? = null - ChooseWalletState.Choose( - wallets = suitableUserWallets.map { wallet -> - val walletState = WalletState( - walletId = wallet.walletId.stringValue, - artworkUrl = wallet.artworkUrl, - onSelected = clickIntents::onWalletSelected, - walletName = wallet.name, - ) - if (wallet.walletId.stringValue == selectedWalletId?.stringValue) { - selectedWalletState = walletState - } - walletState - }.toPersistentList(), - selectedWallet = requireNotNull(selectedWalletState), - onChooseWalletClick = clickIntents::onChooseWalletClick, - onCloseChoosingWalletClick = clickIntents::onCloseChoosingWalletClick, - ) - } - return chooseWalletState - } - - fun removeTokenAddressError(): AddCustomTokenState { - return addTokenAddressFieldError(null) - } - - private fun addTokenAddressFieldError(error: AddCustomTokenWarning?): AddCustomTokenState { - val tokenData = currentStateProvider().tokenData ?: return currentStateProvider() - val contractAddressField = tokenData.contractAddressTextField.copySealed(error = error) - return currentStateProvider().copy(tokenData = tokenData.copy(contractAddressTextField = contractAddressField)) - } - - private fun unlockAndClearNameSymbolAndDecimals(state: AddCustomTokenState): AddCustomTokenState { - val currentTokenData = state.tokenData - return state.copy( - tokenData = currentTokenData?.copy( - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ), - ) - } - - fun getStateAndTriggerEvent( - state: AddCustomTokenState, - event: Event, - setUiState: (AddCustomTokenState) -> Unit, - ): AddCustomTokenState { - return state.copy( - event = triggeredEvent( - data = event, - onConsume = { - val currentState = currentStateProvider() - setUiState(currentState.copy(event = consumedEvent())) - }, - ), - ) - } - - fun updateStateOnNetworkSelected( - networkItemState: NetworkItemState, - supportsTokens: Boolean, - networks: List, - requiresHardenedDerivationOnly: Boolean, - ): AddCustomTokenState { - val uiState = currentStateProvider() - val tokenData = if (supportsTokens) { - uiState.tokenData ?: CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) - } else { - null - } - - val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly) - val chooseDerivationState = createChooseDerivationState(derivations) - - return uiState.copy( - chooseNetworkState = uiState.chooseNetworkState.copy( - selectedNetwork = networkItemState, - ), - chooseDerivationState = chooseDerivationState, - tokenData = tokenData, - addTokenButton = uiState.addTokenButton.copy(isEnabled = true), - ) - } - - fun updateOnCustomDerivationSelected(): AddCustomTokenState { - val uiState = currentStateProvider() - return uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = null, - selectedDerivation = Derivation( - networkName = "", - path = uiState.chooseDerivationState.enterCustomDerivationState?.value ?: "", - networkId = null, - standardType = null, - onDerivationSelected = clickIntents::onDerivationSelected, - ), - ), - ) - } - - fun updateStateOnEnterCustomDerivation(): AddCustomTokenState { - val customDerivationState = EnterCustomDerivationState( - value = "", - onValueChange = clickIntents::onCustomDerivationChange, - confirmButtonEnabled = false, - derivationIncorrect = false, - onConfirmButtonClick = clickIntents::onCustomDerivationSelected, - onDismiss = clickIntents::onCustomDerivationDialogDismissed, - ) - val uiState = currentStateProvider() - return uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = customDerivationState, - ), - ) - } - - fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState { - val uiState = currentStateProvider() - val path = createDerivationPathOrNull(input) - val isWrongDerivationForWallet2 = isWrongDerivationForWallet2( - requiresHardenedDerivationOnly = requiresHardenedDerivationOnly, - derivationPath = path, - ) - val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - confirmButtonEnabled = path != null && !isWrongDerivationForWallet2, - derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2, - ) - return uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = enterDerivationState, - ), - ) - } - - private fun isWrongDerivationForWallet2( - requiresHardenedDerivationOnly: Boolean, - derivationPath: DerivationPath?, - ): Boolean { - return if (requiresHardenedDerivationOnly) { - !allNodesHardened(derivationPath) - } else { - false - } - } - - private fun allNodesHardened(derivationPath: DerivationPath?): Boolean { - return derivationPath?.nodes?.all { it.isHardened } ?: false - } - - private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { - return try { - DerivationPath(rawPath) - } catch (error: Throwable) { - null - } - } - - fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState { - return currentStateProvider().copy( - tokenData = CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = contractAddress, - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - - ), - nameTextField = TextFieldState.Loading, - symbolTextField = TextFieldState.Loading, - decimalsTextField = TextFieldState.Loading, - ), - ) - } - - fun handleAddressError(error: AddCustomTokenError): AddCustomTokenState { - val uiState = currentStateProvider() - return when (error) { - AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> { - removeTokenAddressError().also { unlockAndClearNameSymbolAndDecimals(it) } - .copy( - addTokenButton = uiState.addTokenButton.copy(isEnabled = false), - warnings = uiState.warnings - .filterNot { it is AddCustomTokenWarning.PotentialScamToken } - .toPersistentSet(), - ) - } - AddCustomTokenError.FIELD_IS_EMPTY -> - removeTokenAddressError().also { unlockAndClearNameSymbolAndDecimals(it) } - .copy( - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt deleted file mode 100644 index e6e1b2f84e..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.data.tokens.utils.CryptoCurrencyFactory -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.managetokens.presentation.addcustomtoken.state.AddCustomTokenState -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.utils.converter.Converter - -internal class AddCustomTokenStateToCryptoCurrencyConverter( - private val derivationStyleProvider: DerivationStyleProvider, -) : Converter { - - override fun convert(value: AddCustomTokenState): CryptoCurrency { - val derivationPath = value.chooseDerivationState?.selectedDerivation?.path - val token = parseTokenOrNull(value.tokenData) - - val cryptoCurrency = if (token != null) { - CryptoCurrencyFactory().createToken( - token = token, - networkId = value.chooseNetworkState.selectedNetwork?.id ?: "", - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = derivationPath, - ) - } else { - CryptoCurrencyFactory().createCoin( - networkId = value.chooseNetworkState.selectedNetwork?.id ?: "", - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = derivationPath, - ) - } - return requireNotNull(cryptoCurrency) { - "Unless network is not Unknown blockchain, CryptoCurrency cannot be null" - } - } - - @Suppress("ComplexCondition") - private fun parseTokenOrNull(tokenData: CustomTokenData?): CryptoCurrencyFactory.Token? { - val contractAddress = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value - val symbol = (tokenData?.symbolTextField as? TextFieldState.Editable)?.value - val name = (tokenData?.nameTextField as? TextFieldState.Editable)?.value - val decimals = (tokenData?.decimalsTextField as? TextFieldState.Editable)?.value?.toIntOrNull() - return if ( - !contractAddress.isNullOrBlank() && !symbol.isNullOrBlank() && !name.isNullOrBlank() && decimals != null - ) { - CryptoCurrencyFactory.Token( - symbol = symbol, - name = name, - contractAddress = contractAddress, - decimals = decimals, - id = null, - ) - } else { - null - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt deleted file mode 100644 index eb0897d370..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents -import com.tangem.utils.converter.Converter - -internal class ContractAddressToCustomTokenDataConverter( - private val clickIntents: AddCustomTokenClickIntents, -) : Converter { - override fun convert(value: String): CustomTokenData { - return CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = value, - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt deleted file mode 100644 index 65921700dc..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.domain.tokens.model.FoundToken -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents -import com.tangem.utils.converter.Converter - -internal class FoundTokenToCustomTokenDataConverter( - private val clickIntents: AddCustomTokenClickIntents, -) : Converter { - override fun convert(value: FoundToken): CustomTokenData { - return CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = value.contractAddress, - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = value.name, - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = value.symbol, - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = value.decimals.toString(), - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt deleted file mode 100644 index 74b4eda971..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.core.ui.extensions.getActiveIconResByNetworkId -import com.tangem.domain.tokens.model.Network -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.utils.converter.Converter - -internal class NetworkToNetworkItemStateConverter( - private val onNetworkItemSelected: (NetworkItemState) -> Unit, -) : Converter { - override fun convert(value: Network): NetworkItemState { - return NetworkItemState.Selectable( - name = value.name, - protocolName = value.standardType.name, - iconResId = getActiveIconResByNetworkId(value.backendId), - id = value.backendId, - onNetworkClick = onNetworkItemSelected, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt deleted file mode 100644 index 96e7bf7379..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata - -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.WalletState -import com.tangem.managetokens.presentation.addcustomtoken.state.* -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentSetOf - -internal object AddCustomTokenPreviewData { - val state = AddCustomTokenState( - chooseWalletState = ChooseWalletState.Choose( - wallets = persistentListOf(), - selectedWallet = WalletState( - "", - "", - "My Wallet", - {}, - ), - onChooseWalletClick = { }, - onCloseChoosingWalletClick = { }, - ), - chooseNetworkState = ChooseNetworkState( - networks = persistentListOf(), - selectedNetwork = null, - onChooseNetworkClick = { }, - onCloseChoosingNetworkClick = {}, - ), - chooseDerivationState = ChooseDerivationState( - derivations = persistentListOf(), - selectedDerivation = null, - enterCustomDerivationState = null, - onChooseDerivationClick = { }, - onCloseChoosingDerivationClick = {}, - onEnterCustomDerivation = {}, - ), - tokenData = CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "0x4ace7262705b68bcba5b91de96889349394", - isEnabled = false, - onValueChange = {}, - onFocusExit = {}, - ), - nameTextField = TextFieldState.Loading, - symbolTextField = TextFieldState.Loading, - decimalsTextField = TextFieldState.Loading, - ), - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = true, onClick = {}), - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt deleted file mode 100644 index c4cfd15483..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata - -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseDerivationState -import com.tangem.managetokens.presentation.addcustomtoken.state.Derivation -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -internal object ChooseDerivationPreviewData { - private val derivations: ImmutableList = persistentListOf( - Derivation( - networkName = "Ethereum", - path = "m/44’/9001’/0’/0/0", - networkId = "ethereum", - standardType = "ERC", - onDerivationSelected = {}, - ), - Derivation( - networkName = "Polygon", - path = "m/44’/9001’/0’/0/0", - networkId = "polygon", - standardType = "ERC", - onDerivationSelected = {}, - ), - Derivation( - networkName = "Avalanche", - path = "m/44’/9001’/0’/0/0", - networkId = "avalanche", - standardType = "ERC", - onDerivationSelected = {}, - ), - ) - - val state = ChooseDerivationState( - derivations = derivations, - selectedDerivation = derivations.first(), - enterCustomDerivationState = null, - onEnterCustomDerivation = {}, - onCloseChoosingDerivationClick = {}, - onChooseDerivationClick = {}, - show = true, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt deleted file mode 100644 index 08987dbadf..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata - -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseNetworkState -import kotlinx.collections.immutable.persistentListOf - -internal object ChooseNetworkCustomPreviewData { - val networks = persistentListOf( - NetworkItemState.Selectable( - name = "Ethereum", - protocolName = "ETH", - iconResId = R.drawable.img_kusama_22, - id = "ethereum", - onNetworkClick = { }, - ), - NetworkItemState.Selectable( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = R.drawable.ic_bsc_16, - id = "binance smart chain", - onNetworkClick = { }, - ), - ) - val state = ChooseNetworkState( - networks, - selectedNetwork = networks.first(), - onCloseChoosingNetworkClick = {}, - onChooseNetworkClick = {}, - show = true, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt deleted file mode 100644 index 95dfd58871..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt +++ /dev/null @@ -1,202 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.annotation.SuppressLint -import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.* -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.ModalBottomSheetDefaults -import androidx.compose.material3.ModalBottomSheetProperties -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type -import androidx.compose.ui.platform.LocalDensity -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader -import com.tangem.core.ui.components.bottomsheets.collapse -import com.tangem.core.ui.res.LocalWindowSize -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.WindowInsetsZero -import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRoute -import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) { - val viewModel = hiltViewModel() - - var isVisible by remember { mutableStateOf(value = config.isShow) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } - - if (isVisible) { - // ViewModel cannot be scoped to ModalBottomSheet's lifecycle, - // so we have to manually initialize and dispose its state when bottom sheet enters and leaves a composition - DisposableEffect(viewModel) { - viewModel.onInitialize() - onDispose { viewModel.onDispose() } - } - - ModalBottomSheetWithBackHandling( - modifier = Modifier - .sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), - onDismissRequest = config.onDismissRequest, - sheetState = sheetState, - containerColor = TangemTheme.colors.background.tertiary, - shape = TangemTheme.shapes.bottomSheetLarge, - windowInsets = WindowInsetsZero, - dragHandle = { TangemBottomSheetDraggableHeader(color = TangemTheme.colors.background.tertiary) }, - properties = ModalBottomSheetDefaults.properties(shouldDismissOnBackPress = false), - ) { - Content(onDismissRequest = config.onDismissRequest, viewModel = viewModel) - } - } - - LaunchedEffect(key1 = config.isShow) { - if (config.isShow) { - isVisible = true - } else { - sheetState.collapse { isVisible = false } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ModalBottomSheetWithBackHandling( - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - containerColor: Color = BottomSheetDefaults.ContainerColor, - shape: Shape = BottomSheetDefaults.ExpandedShape, - windowInsets: WindowInsets = BottomSheetDefaults.windowInsets, - dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, - sheetState: SheetState = rememberModalBottomSheetState(), - properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(), - content: @Composable ColumnScope.() -> Unit, -) { - val scope = rememberCoroutineScope() - - BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) { - // Always catch back here, but only let it dismiss if shouldDismissOnBackPress. - // If not, it will have no effect. - if (properties.shouldDismissOnBackPress) { - scope.launch { sheetState.hide() }.invokeOnCompletion { - if (!sheetState.isVisible) { - onDismissRequest() - } - } - } - } - - val requester = remember { FocusRequester() } - val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - - ModalBottomSheet( - onDismissRequest = onDismissRequest, - containerColor = containerColor, - shape = shape, - windowInsets = windowInsets, - dragHandle = dragHandle, - sheetState = sheetState, - modifier = modifier - .focusRequester(requester) - .focusable() - .onPreviewKeyEvent { - if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) { - backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() - return@onPreviewKeyEvent true - } - return@onPreviewKeyEvent false - }, - properties = ModalBottomSheetDefaults.properties( - securePolicy = properties.securePolicy, - isFocusable = properties.isFocusable, - // Set false otherwise the onPreviewKeyEvent doesn't work at all. - // The functionality of shouldDismissOnBackPress is achieved by the BackHandler. - shouldDismissOnBackPress = false, - ), - content = content, - ) - - LaunchedEffect(Unit) { - requester.requestFocus() - } -} - -@SuppressLint("RestrictedApi") -@Composable -private fun Content(viewModel: AddCustomTokenViewModel, onDismissRequest: () -> Unit) { - val navController = rememberNavController() - - LaunchedEffect(navController) { - navController.currentBackStack - .collect { - if (it.isEmpty()) { - onDismissRequest() - } - } - } - - BackHandler(true) { - navController.popBackStack() - } - - val router = remember(navController) { AddCustomTokenRouter(navController) } - - viewModel.router = router - - NavHost( - modifier = Modifier.fillMaxSize(), - navController = navController, - startDestination = AddCustomTokenRoute.Main.route, - ) { - composable( - route = AddCustomTokenRoute.Main.route, - ) { - AddCustomTokenScreen(state = viewModel.uiState) - } - composable( - route = AddCustomTokenRoute.ChooseNetwork.route, - ) { - ChooseNetworkCustomScreen(state = viewModel.uiState.chooseNetworkState) - } - composable( - route = AddCustomTokenRoute.ChooseDerivation.route, - ) { - ChooseDerivationScreen(state = requireNotNull(viewModel.uiState.chooseDerivationState)) - } - composable( - route = AddCustomTokenRoute.ChooseWallet.route, - ) { - CustomTokensChooseWalletScreen(state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt deleted file mode 100644 index d5691e9017..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt +++ /dev/null @@ -1,298 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig -import com.tangem.managetokens.presentation.common.ui.EventEffect -import com.tangem.managetokens.presentation.common.ui.components.Alert -import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock -import com.tangem.managetokens.presentation.addcustomtoken.state.AddCustomTokenState -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.AddCustomTokenPreviewData - -@Composable -internal fun AddCustomTokenScreen(state: AddCustomTokenState, modifier: Modifier = Modifier) { - var alertState by remember { mutableStateOf(value = null) } - - EventEffect( - event = state.event, - onAlertStateSet = { alertState = it }, - ) - alertState?.let { - Alert(state = it, onDismiss = { alertState = null }) - } - Content(state = state, modifier = modifier) -} - -@Composable -private fun Content(state: AddCustomTokenState, modifier: Modifier = Modifier) { - val keyboard by keyboardAsState() - - Column( - modifier = modifier - .background(color = TangemTheme.colors.background.tertiary) - .statusBarsPadding() - .navigationBarsPadding() - .imePadding() - .padding( - top = TangemTheme.dimens.spacing10, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing18, - ) - .fillMaxWidth(), - ) { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing10), - ) - Text( - text = stringResource(id = R.string.custom_token_subtitle), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing16), - ) - CustomTokenItemsList(state = state) - if (keyboard is Keyboard.Closed) { - PrimaryButton( - text = stringResource(id = R.string.custom_token_add_token), - onClick = state.addTokenButton.onClick, - enabled = state.addTokenButton.isEnabled, - modifier = Modifier.fillMaxWidth(), - ) - } - } - - if (state.chooseWalletState is ChooseWalletState.Choose && state.chooseWalletState.show) { - val config = TangemBottomSheetConfig( - isShow = true, - content = ChooseWalletBottomSheetConfig(state.chooseWalletState), - onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick, - ) - ChooseWalletBottomSheet(config) - } -} - -@Composable -private fun ColumnScope.CustomTokenItemsList(state: AddCustomTokenState, modifier: Modifier = Modifier) { - LazyColumn( - modifier - .fillMaxWidth() - .weight(1f), - ) { - if (state.chooseWalletState is ChooseWalletState.Choose) { - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.manage_tokens_network_selector_wallet), - subtitle = state.chooseWalletState.selectedWallet?.walletName ?: "", - onClick = state.chooseWalletState.onChooseWalletClick, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } - } - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.custom_token_network_input_title), - subtitle = state.chooseNetworkState.selectedNetwork?.name - ?: stringResource(id = R.string.manage_tokens_network_selector_title), - onClick = state.chooseNetworkState.onChooseNetworkClick, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } - if (state.tokenData != null) { - item { - TokenFields( - state = state.tokenData, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } - } - if (state.chooseDerivationState != null) { - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.custom_token_derivation_path), - subtitle = state.chooseDerivationState.selectedDerivation?.path - ?: stringResource(id = R.string.custom_token_derivation_path_default), - onClick = state.chooseDerivationState.onChooseDerivationClick, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun TokenFields(state: CustomTokenData, modifier: Modifier = Modifier) { - val focusManager = LocalFocusManager.current - - Column( - modifier = modifier - .fillMaxWidth() - .clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(color = TangemTheme.colors.background.action), - ) { - TokenField( - textFieldState = state.contractAddressTextField, - placeholder = "0x0000000000000000000000000000000", - title = R.string.custom_token_contract_address_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = if (state.isNameSymbolDecimalsDisabled()) { - ImeAction.Done - } else { - ImeAction.Next - }, - ), - onImeAction = { - if (state.isNameSymbolDecimalsDisabled()) { - focusManager.moveFocus(FocusDirection.Exit) - } else { - focusManager.moveFocus(FocusDirection.Down) - } - }, - ) - TokenField( - textFieldState = state.nameTextField, - placeholder = stringResource(id = R.string.custom_token_name_input_placeholder), - title = R.string.custom_token_name_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - ), - onImeAction = { focusManager.moveFocus(FocusDirection.Down) }, - ) - TokenField( - textFieldState = state.symbolTextField, - placeholder = stringResource(id = R.string.custom_token_token_symbol_input_placeholder), - title = R.string.custom_token_token_symbol_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - ), - onImeAction = { focusManager.moveFocus(FocusDirection.Down) }, - ) - TokenField( - textFieldState = state.decimalsTextField, - placeholder = "0", - title = R.string.custom_token_decimals_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), - onImeAction = { focusManager.moveFocus(FocusDirection.Exit) }, - ) - } -} - -@Composable -private fun TokenField( - textFieldState: TextFieldState, - placeholder: String, - title: Int, - onImeAction: () -> Unit, - keyboardOptions: KeyboardOptions, -) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing16), - ) { - TokenTextFieldTitle( - state = textFieldState, - title = stringResource(id = title), - ) - when (textFieldState) { - is TextFieldState.Editable -> TokenTextField( - state = textFieldState, - placeholder = placeholder, - onImeAction = onImeAction, - keyboardOptions = keyboardOptions, - ) - is TextFieldState.Loading -> TokenShimmer() - } - } -} - -@Composable -private fun TokenShimmer() { - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing4) - .size( - height = TangemTheme.dimens.size12, - width = TangemTheme.dimens.size90, - ), - ) -} - -@Composable -private fun TokenTextFieldTitle(state: TextFieldState?, title: String) { - val modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing4) - val error = (state as? TextFieldState.Editable)?.error - if (error == null) { - Text( - text = title, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.subtitle1, - modifier = modifier, - ) - } else { - Text( - text = error.title.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.subtitle1, - modifier = modifier, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseDerivationScreen() { - TangemThemePreview { - AddCustomTokenScreen(state = AddCustomTokenPreviewData.state) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt deleted file mode 100644 index a7805cbb96..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Icon -import androidx.compose.material3.Text -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.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseDerivationState -import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.ChooseDerivationPreviewData - -@Composable -internal fun ChooseDerivationScreen(state: ChooseDerivationState, modifier: Modifier = Modifier) { - if (state.enterCustomDerivationState != null) { - CustomDerivationDialog(state.enterCustomDerivationState) - } - - Column( - modifier = modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.tertiary) - .statusBarsPadding() - .navigationBarsPadding() - .padding( - top = TangemTheme.dimens.spacing10, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing18, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size44) - .padding(bottom = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - modifier = Modifier - .clickable { state.onCloseChoosingDerivationClick() }, - ) - Text( - text = stringResource(id = R.string.custom_token_derivation_path), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing24) - .align(Alignment.Center), - ) - } - DerivationsList(state) - } -} - -@Composable -private fun DerivationsList(state: ChooseDerivationState) { - LazyColumn( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16), - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing12), - ) { - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.custom_token_custom_derivation), - subtitle = stringResource(id = R.string.custom_token_custom_derivation), - onClick = state.onEnterCustomDerivation, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - items(state.derivations.count()) { index -> - val derivationItem = state.derivations[index] - SimpleSelectionBlock( - title = derivationItem.networkName, - subtitle = derivationItem.path, - onClick = { derivationItem.onDerivationSelected(derivationItem) }, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.derivations.lastIndex, - addDefaultPadding = false, - ), - roundedCorners = false, - ) - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseDerivationScreen() { - TangemThemePreview { - ChooseDerivationScreen(state = ChooseDerivationPreviewData.state) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt deleted file mode 100644 index 6777fc4fbc..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Icon -import androidx.compose.material3.Text -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.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.ui.components.NetworkItem -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseNetworkState -import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.ChooseNetworkCustomPreviewData - -@Composable -internal fun ChooseNetworkCustomScreen(state: ChooseNetworkState, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.tertiary) - .statusBarsPadding() - .navigationBarsPadding() - .padding( - top = TangemTheme.dimens.spacing10, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing18, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size44) - .padding(bottom = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - modifier = Modifier - .clickable { state.onCloseChoosingNetworkClick() }, - ) - Text( - text = stringResource(id = R.string.custom_token_network_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing24) - .align(Alignment.Center), - ) - } - LazyColumn( - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing12), - ) { - items(state.networks.count()) { index -> - NetworkItem( - state = state.networks[index], - tokenState = null, - isSelected = state.selectedNetwork == state.networks[index], - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.networks.lastIndex, - addDefaultPadding = false, - ), - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseNetworkScreen() { - TangemThemePreview { - ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt deleted file mode 100644 index 969d7f61fb..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.components.TextInputDialog -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.addcustomtoken.state.EnterCustomDerivationState - -@Composable -internal fun CustomDerivationDialog(state: EnterCustomDerivationState) { - val confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - enabled = state.confirmButtonEnabled, - onClick = state.onConfirmButtonClick, - ) - - val dismissButton = DialogButton( - title = stringResource(id = R.string.common_cancel), - onClick = state.onDismiss, - ) - - val params = AdditionalTextInputDialogParams( - placeholder = stringResource(id = R.string.custom_token_custom_derivation_placeholder), - isError = state.derivationIncorrect, - errorText = if (state.derivationIncorrect) { - stringResource(R.string.custom_token_invalid_derivation_path) - } else { - null - }, - ) - - TextInputDialog( - fieldValue = state.value, - confirmButton = confirmButton, - onDismissDialog = state.onDismiss, - onValueChange = state.onValueChange, - textFieldParams = params, - title = stringResource(id = R.string.custom_token_custom_derivation_title), - dismissButton = dismissButton, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt deleted file mode 100644 index 3b8eb7c2af..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.ui.ChooseWalletScreen - -@Composable -internal fun CustomTokensChooseWalletScreen(state: ChooseWalletState.Choose) { - ChooseWalletScreen( - state = state, - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding(), - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt deleted file mode 100644 index 63521c6775..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.input.key.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState - -@Composable -internal fun TokenTextField( - state: TextFieldState.Editable, - placeholder: String, - onImeAction: () -> Unit, - keyboardOptions: KeyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), -) { - val isInitiallyComposed = remember { mutableStateOf(false) } - LaunchedEffect(key1 = true) { - isInitiallyComposed.value = true - } - - BasicTextField( - value = state.value, - onValueChange = state.onValueChange, - keyboardOptions = keyboardOptions, - singleLine = true, - maxLines = 1, - textStyle = TangemTheme.typography.subtitle1.copy( - fontWeight = FontWeight.Normal, - color = if (state.isEnabled) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.disabled, - ), - cursorBrush = SolidColor(TangemTheme.colors.icon.primary1), - modifier = Modifier - .fillMaxWidth() - .onKeyEvent { keyEvent -> - if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == Key.Enter) { - onImeAction() - true - } else { - false - } - } - .onFocusChanged { - if (!it.isFocused && isInitiallyComposed.value) { - state.onFocusExit() - } - }, - decorationBox = { innerTextField -> - Row(modifier = Modifier.fillMaxWidth()) { - if (state.value.isEmpty()) { - Text( - text = placeholder, - color = if (state.isEnabled) { - TangemTheme.colors.text.tertiary - } else { - TangemTheme.colors.text.disabled - }, - style = TangemTheme.typography.body2, - ) - } - } - innerTextField() - }, - enabled = state.isEnabled, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt deleted file mode 100644 index 5a2e8e299b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.viewmodels - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.addcustomtoken.state.Derivation - -@Suppress("TooManyFunctions") -internal interface AddCustomTokenClickIntents { - - fun onNetworkSelected(networkItemState: NetworkItemState) - - fun onChooseNetworkClick() - - fun onCloseChoosingNetworkClick() - - fun onWalletSelected(walletId: String) - - fun onChooseWalletClick() - - fun onCloseChoosingWalletClick() - - fun onContractAddressChange(input: String) - - fun onTokenNameChange(input: String) - - fun onSymbolChange(input: String) - - fun onDecimalsChange(input: String) - - fun onContractAddressFocusExit() - - fun onTokenNameFocusExit() - - fun onSymbolFocusExit() - - fun onDecimalsFocusExit() - - fun onDerivationSelected(derivation: Derivation) - - fun onChooseDerivationClick() - - fun onCloseChoosingDerivationClick() - - fun onEnterCustomDerivation() - - fun onCustomDerivationChange(input: String) - - fun onCustomDerivationSelected() - - fun onCustomDerivationDialogDismissed() - - fun onAddCustomButtonClick() - - fun onBack() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt deleted file mode 100644 index 44e55b947d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ /dev/null @@ -1,484 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.viewmodels - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase -import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter -import com.tangem.managetokens.presentation.common.analytics.ManageTokens -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.Event -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.addcustomtoken.state.* -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.AddCustomTokenStateToCryptoCurrencyConverter -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.ContractAddressToCustomTokenDataConverter -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.AddCustomTokenStateFactory -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.FoundTokenToCustomTokenDataConverter -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged -import javax.inject.Inject -import kotlin.properties.Delegates - -@Suppress("LongParameterList", "TooManyFunctions", "LargeClass") -@Stable -@HiltViewModel -internal class AddCustomTokenViewModel @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, - private val getWalletsUseCase: GetWalletsUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val selectWalletUseCase: SelectWalletUseCase, - private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val findTokenByContractAddressUseCase: FindTokenByContractAddressUseCase, - private val validateContractAddressUseCase: ValidateContractAddressUseCase, - private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet, - private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase, - private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, -) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver { - - private val debouncer = Debouncer() - - private val stateFactory = AddCustomTokenStateFactory( - currentStateProvider = Provider { uiState }, - clickIntents = this, - ) - - var router: AddCustomTokenRouter by Delegates.notNull() - - var uiState: AddCustomTokenState by mutableStateOf(stateFactory.getInitialState()) - private set - - /** - * Called when the bottom sheet was opened - */ - fun onInitialize() { - viewModelScope.launch(dispatchers.io) { - getWalletsUseCase() - .distinctUntilChanged() - .collectLatest { userWallets -> - val suitableUserWallets = userWallets.filter { it.isMultiCurrency && !it.isLocked } - val selectedWalletId = selectSuitableWallet(suitableUserWallets) - val networks = selectedWalletId?.let { getSupportedNetworks(selectedWalletId) } ?: emptyList() - withContext(dispatchers.main) { - uiState = stateFactory.getFullState( - allUserWallets = suitableUserWallets, - suitableUserWallets = userWallets, - selectedWalletId = selectedWalletId, - supportedNetworks = networks, - ) - } - } - } - } - - /** - * Called after the bottom sheet is closed - */ - fun onDispose() { - // We have to manually cancel viewModelScope's child jobs when bottom sheet is closed - viewModelScope.coroutineContext.cancelChildren() - // and reset state - uiState = stateFactory.getInitialState() - } - - private suspend fun selectSuitableWallet(suitableUserWallets: List): UserWalletId? { - val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() - val selectedWalletId = if (walletSupportsAddingTokens(selectedWallet) && suitableUserWallets.isNotEmpty()) { - val walletId = suitableUserWallets.first().walletId - selectWalletUseCase(walletId) - walletId - } else { - selectedWallet?.walletId - } - return selectedWalletId - } - - private fun walletSupportsAddingTokens(userWallet: UserWallet?): Boolean { - return userWallet != null && userWallet.isMultiCurrency && !userWallet.isLocked - } - - private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List { - return getNetworksSupportedByWallet(userWalletId).fold( - ifLeft = { emptyList() }, - ifRight = { it }, - ) - } - - override fun onNetworkSelected(networkItemState: NetworkItemState) { - analyticsEventHandler.send(ManageTokens.CustomTokenNetworkSelected(networkItemState.name)) - selectNetwork(networkItemState) - router.popBackStack() - } - - private fun selectNetwork(networkItemState: NetworkItemState) { - viewModelScope.launch(dispatchers.io) { - // TODO [REDACTED_TASK_KEY] - val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch - val supportsTokens = areTokensSupportedByNetworkUseCase( - networkId = networkItemState.id, - userWalletId = selectedWalletId, - ).getOrNull() ?: false - - val networksForDerivations = getSupportedNetworks(selectedWalletId) - - withContext(dispatchers.main) { - uiState = stateFactory.updateStateOnNetworkSelected( - networkItemState = networkItemState, - supportsTokens = supportsTokens, - networks = networksForDerivations, - requiresHardenedDerivationOnly = requiresHardenedDerivationOnly( - networkId = networkItemState.id, - userWalletId = selectedWalletId, - ), - ) - } - } - } - - override fun onChooseNetworkClick() { - router.openCustomTokenChooseNetwork() - } - - override fun onCloseChoosingNetworkClick() { - router.popBackStack() - } - - override fun onWalletSelected(walletId: String) { - analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.CustomToken)) - viewModelScope.launch(dispatchers.io) { - val userWalletId = UserWalletId(walletId) - selectWalletUseCase(userWalletId) - val supportedNetworks = getSupportedNetworks(UserWalletId(walletId)) - - withContext(dispatchers.main) { - uiState = stateFactory.updateWithNewWalletSelected( - selectedWalletId = userWalletId, - supportedNetworks = supportedNetworks, - ) - router.popBackStack() - } - } - } - - override fun onChooseWalletClick() { - router.openCustomTokenChooseWallet() - } - - override fun onCloseChoosingWalletClick() { - router.popBackStack() - } - - override fun onContractAddressChange(input: String) { - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - contractAddressTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onContractAddressChange, - onFocusExit = this::onContractAddressFocusExit, - ), - ), - ) - debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - uiState.chooseNetworkState.selectedNetwork?.let { networkItemState -> - validateContractAddressUseCase(input, networkItemState.id).fold( - ifRight = { - uiState = stateFactory.removeTokenAddressError() - .copy( - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - warnings = uiState.warnings - .filterNot { it is AddCustomTokenWarning.PotentialScamToken }.toPersistentSet(), - ) - fetchTokenInformation(contractAddress = input, networkId = networkItemState.id) - }, - ifLeft = { error -> - uiState = stateFactory.handleAddressError(error) - }, - ) - } - } - } - - private fun fetchTokenInformation(contractAddress: String, networkId: String) { - viewModelScope.launch(dispatchers.main) { - uiState = stateFactory.updateStateOnLoadingTokenInfo(contractAddress) - withContext(dispatchers.io) { - findTokenByContractAddressUseCase( - contractAddress = contractAddress, - networkId = networkId, - ).fold( - ifLeft = { - val tokenData = ContractAddressToCustomTokenDataConverter(this@AddCustomTokenViewModel) - .convert(contractAddress) - - val isButtonEnabled = tokenData.isRequiredInformationProvided() - uiState = uiState.copy( - tokenData = tokenData, - warnings = (uiState.warnings + AddCustomTokenWarning.PotentialScamToken).toPersistentSet(), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = isButtonEnabled, - ), - ) - }, - ifRight = { token -> - val tokenData = if (token != null) { - FoundTokenToCustomTokenDataConverter(this@AddCustomTokenViewModel).convert(token) - } else { - ContractAddressToCustomTokenDataConverter(this@AddCustomTokenViewModel).convert( - contractAddress, - ) - } - - val isButtonEnabled = tokenData.isRequiredInformationProvided() - uiState = uiState.copy( - tokenData = tokenData, - addTokenButton = uiState.addTokenButton.copy( - isEnabled = isButtonEnabled, - ), - ) - }, - ) - } - } - } - - override fun onTokenNameChange(input: String) { - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - nameTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onTokenNameChange, - onFocusExit = this::onTokenNameFocusExit, - ), - ), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - - override fun onSymbolChange(input: String) { - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - symbolTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onSymbolChange, - onFocusExit = this::onSymbolFocusExit, - ), - ), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - - override fun onDecimalsChange(input: String) { - val correctInput = input.toIntOrNull() - val error = if (input.isNotBlank() && correctInput == null) { - AddCustomTokenWarning.WrongDecimals - } else { - null - } - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - decimalsTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onDecimalsChange, - error = error, - onFocusExit = this::onDecimalsFocusExit, - ), - ), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - - override fun onContractAddressFocusExit() { - val error = (uiState.tokenData?.contractAddressTextField as? TextFieldState.Editable)?.error - val validated = error !is AddCustomTokenWarning.InvalidContractAddress - analyticsEventHandler.send(ManageTokens.CustomTokenAddress(validated = validated)) - } - - override fun onTokenNameFocusExit() { - analyticsEventHandler.send(ManageTokens.CustomTokenName) - } - - override fun onSymbolFocusExit() { - analyticsEventHandler.send(ManageTokens.CustomTokenSymbol) - } - - override fun onDecimalsFocusExit() { - analyticsEventHandler.send(ManageTokens.CustomTokenDecimals) - } - - override fun onDerivationSelected(derivation: Derivation) { - derivation.standardType?.let { - analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(derivation.networkName)) - } - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation), - ) - router.popBackStack() - } - - override fun onChooseDerivationClick() { - router.openCustomTokenChooseDerivation() - } - - override fun onCloseChoosingDerivationClick() { - router.popBackStack() - } - - override fun onCustomDerivationChange(input: String) { - val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return - - analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value)) - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - value = input, - ), - ), - ) - debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce - uiState = stateFactory.updateOnCustomDerivationEntered( - input = input, - requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId), - ) - } - } - - private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { - return requiresHardenedDerivationOnlyUseCase.invoke( - networkId = networkId, - userWalletId = userWalletId, - ).getOrElse { false } - } - - override fun onCustomDerivationSelected() { - uiState = stateFactory.updateOnCustomDerivationSelected() - router.popBackStack() - } - - override fun onEnterCustomDerivation() { - uiState = stateFactory.updateStateOnEnterCustomDerivation() - } - - override fun onCustomDerivationDialogDismissed() { - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = null, - ), - ) - } - - override fun onAddCustomButtonClick() { - viewModelScope.launch(dispatchers.io) { - val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return@launch - val cryptoCurrency = AddCustomTokenStateToCryptoCurrencyConverter( - selectedWallet.scanResponse.derivationStyleProvider, - ).convert(uiState) - val alreadyAdded = isCryptoCurrencyAlreadyAdded(selectedWallet, cryptoCurrency) - if (alreadyAdded) { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert(AlertState.TokenAlreadyAdded), - setUiState = { uiState = it }, - ) - } else { - sendTokenAddedEvent(cryptoCurrency) - addCryptoCurrenciesUseCase(selectedWallet.walletId, currency = cryptoCurrency) - withContext(dispatchers.main) { router.popBackStack() } - } - } - } - - private suspend fun isCryptoCurrencyAlreadyAdded( - selectedWallet: UserWallet, - cryptoCurrency: CryptoCurrency, - ): Boolean { - val currenciesList = getCurrenciesUseCase.getSync(selectedWallet.walletId).getOrElse { emptyList() } - return when (cryptoCurrency) { - is CryptoCurrency.Coin -> { - currenciesList.any { - it is CryptoCurrency.Coin && - it.id == cryptoCurrency.id && - it.network.derivationPath == cryptoCurrency.network.derivationPath - } - } - is CryptoCurrency.Token -> { - currenciesList.any { - (it as? CryptoCurrency.Token)?.let { - it.id == cryptoCurrency.id && - it.contractAddress == cryptoCurrency.contractAddress && - it.network.id == cryptoCurrency.network.id && - it.network.derivationPath == cryptoCurrency.network.derivationPath - } ?: false - } - } - } - } - - private fun sendTokenAddedEvent(cryptoCurrency: CryptoCurrency) { - val selectedDerivation = uiState.chooseDerivationState?.selectedDerivation - - val derivation = when { - selectedDerivation == null -> ManageTokens.Derivation.DEFAULT.value - selectedDerivation.networkName.isNotEmpty() -> selectedDerivation.networkName - else -> ManageTokens.Derivation.CUSTOM.value - } - when (cryptoCurrency) { - is CryptoCurrency.Token -> { - analyticsEventHandler.send( - ManageTokens.CustomTokenWasAdded( - derivation = derivation, - networkId = cryptoCurrency.network.name, - contractAddress = cryptoCurrency.contractAddress, - token = cryptoCurrency.symbol, - ), - ) - } - is CryptoCurrency.Coin -> { - analyticsEventHandler.send( - ManageTokens.CustomTokenWasAdded( - derivation = derivation, - networkId = cryptoCurrency.network.name, - ), - ) - } - } - } - - override fun onBack() { - router.popBackStack() - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt deleted file mode 100644 index 3a527de7f2..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.managetokens.presentation.common.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam - -sealed class ManageTokens( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent("Manage Tokens", event, params) { - - class ScreenOpened : ManageTokens("Manage Tokens Screen Opened") - - class TokenIsNotFound(userInput: String) : ManageTokens( - event = "Token Is Not Found", - params = mapOf("Input" to userInput), - ) - - class TokenSwitcherChanged( - token: String, - state: AnalyticsParam.OnOffState, - ) : ManageTokens( - event = "Token Switcher Changed", - params = mapOf( - "Token" to token, - "State" to state.value, - ), - ) - - class ButtonAdd(token: String) : ManageTokens( - event = "Button - Add", - params = mapOf("Token" to token), - ) - - class ButtonEdit(token: String) : ManageTokens( - event = "Button - Edit", - params = mapOf("Token" to token), - ) - - object ButtonChooseWallet : ManageTokens(event = "Button - Choose Wallet") - - class WalletSelected(source: Source) : ManageTokens( - event = "Wallet Selected", - params = mapOf("Source" to source.name), - ) { - - enum class Source(name: String) { - MainToken("Main Token"), - CustomToken("Custom Token"), - } - } - - object NoticeNonNativeNetworkClicked : ManageTokens(event = "Notice - Non Native Network Clicked") - - class ButtonGenerateAddresses(cardCount: Int) : ManageTokens( - event = "Button - Get Addresses", - params = mapOf("CardCount" to cardCount.toString()), - ) - - object ButtonCustomToken : ManageTokens("Button - Custom Token") - - class CustomTokenWasAdded( - val derivation: String, - val networkId: String, - val token: String? = null, - val contractAddress: String? = null, - ) : ManageTokens( - event = "Custom Token Was Added", - params = mutableMapOf( - "Derivation" to derivation, - "Network Id" to networkId, - ).apply { - token?.let { put("Token", it) } - contractAddress?.let { put("Contract Address", it) } - }, - ) - - class CustomTokenNetworkSelected(blockchain: String) : ManageTokens( - event = "Custom Token Network Selected", - params = mapOf("blockchain" to blockchain), - ) - - class CustomTokenDerivationSelected(derivation: String) : ManageTokens( - event = "Custom Token Derivation Selected", - params = mapOf("Derivation" to derivation), - ) - - class CustomTokenAddress(validated: Boolean) : ManageTokens( - "Custom Token Address", - params = mapOf("Validation" to if (validated) "Ok" else "Error"), - ) - - object CustomTokenName : ManageTokens("Custom Token Name") - - object CustomTokenSymbol : ManageTokens("Custom Token Symbol") - - object CustomTokenDecimals : ManageTokens("Custom Token Decimals") - - enum class Derivation(val value: String) { - DEFAULT("Default"), - CUSTOM("Custom"), - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt deleted file mode 100644 index 82ba359079..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.managetokens.impl.R - -internal sealed class AlertState { - - abstract val message: TextReference - - class DefaultAlert( - override val message: TextReference, - ) : AlertState() - - class TokenUnavailable( - val onUpvoteClick: () -> Unit, - ) : AlertState() { - override val message: TextReference = resourceReference(R.string.manage_tokens_unavailable_description) - val confirmButtonText: TextReference = resourceReference(R.string.common_close) - val dismissButtonText: TextReference = resourceReference(R.string.manage_tokens_unavailable_vote) - } - - object NonNative : AlertState() { - override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info) - } - - class TokensUnsupported(networkName: String) : AlertState() { - override val message: TextReference = resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(networkName), - ) - } - - object TokensUnsupportedCurve : AlertState() { - override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message) - } - - class TokensUnsupportedBlockchainByCard(networkName: String) : AlertState() { - override val message: TextReference = resourceReference( - id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, - formatArgs = wrappedList(networkName), - ) - } - - class CannotHideNetworkWithTokens(tokenName: String, currencySymbol: String, networkName: String) : AlertState() { - override val message: TextReference = resourceReference( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(tokenName, currencySymbol, networkName), - ) - } - - object TokenAlreadyAdded : AlertState() { - override val message: TextReference = resourceReference(R.string.custom_token_validation_error_already_added) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt deleted file mode 100644 index 6a585d7148..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.managetokens.impl.R -import kotlinx.collections.immutable.ImmutableList - -internal sealed class ChooseWalletState { - data class Choose( - val wallets: ImmutableList, - val selectedWallet: WalletState?, - val onChooseWalletClick: () -> Unit, - val onCloseChoosingWalletClick: () -> Unit, - val show: Boolean = false, - ) : ChooseWalletState() - - object NoSelection : ChooseWalletState() - - class Warning(val type: ChooseWalletWarning) : ChooseWalletState() { - val message: TextReference - get() = when (type) { - ChooseWalletWarning.SINGLE_CURRENCY -> - TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title) - } - } -} - -enum class ChooseWalletWarning { - SINGLE_CURRENCY, -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt deleted file mode 100644 index 4024d7a4d8..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import androidx.compose.runtime.Immutable - -@Immutable -internal sealed interface Event { - data class ShowAlert(val state: AlertState) : Event -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt deleted file mode 100644 index 6255c750f9..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import androidx.compose.runtime.MutableState -import com.tangem.core.ui.extensions.* -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -/** - * Network item state - * - * @property name network name - * @property protocolName network protocol name - * @property id network id - * @property iconRes network icon id from resources - */ -internal sealed interface NetworkItemState { - - val name: String - val protocolName: String - val id: String - - val iconRes: Int - get() = when (this) { - is Selectable -> this.iconResId - is Toggleable -> this.iconResId.value - } - - /** - * Network item state that can be added and deleted - * - * @property name network name - * @property protocolName network protocol name - * @property id network id - * @property iconResId network icon id from resources - * @property isMainNetwork flag that determines if the network is the main network for the token - * @property isAdded flag that determines if the user has saved the network - * @property address contract address - * @property decimals decimal count - * @property onToggleClick lambda be invoked when switch is been toggled - */ - @Suppress("LongParameterList") - data class Toggleable( - override val name: String, - override val protocolName: String, - override val id: String, - val iconResId: MutableState, - val isMainNetwork: Boolean, - val isAdded: MutableState, - val address: String?, - val decimals: Int?, - val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit, - ) : NetworkItemState { - - /** - * Change toggle state [isAdded]. - * - * It is a hack that helps us to change element of flow - */ - fun changeToggleState() { - val reverseState = !isAdded.value - isAdded.value = reverseState - iconResId.value = if (reverseState) getActiveIconResByNetworkId(id) else getGreyedOutIconResByNetworkId(id) - } - } - - /** - * Network item state that can be selected - * - * @property name network name - * @property protocolName network protocol name - * @property iconResId network icon id from resources - * @property id network id - * @property onNetworkClick lambda be invoked when network item is been clicked - * - */ - data class Selectable( - override val name: String, - override val protocolName: String, - val iconResId: Int, - override val id: String, - val onNetworkClick: (NetworkItemState) -> Unit, - ) : NetworkItemState -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt deleted file mode 100644 index 6af48736a4..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -internal data class WalletState( - val walletId: String, - val artworkUrl: String?, - val walletName: String, - val onSelected: (String) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt deleted file mode 100644 index f231654b96..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.managetokens.presentation.common.state.previewdata - -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.WalletState -import kotlinx.collections.immutable.persistentListOf - -internal object ChooseWalletStatePreviewData { - - val state: ChooseWalletState.Choose - get() = ChooseWalletState.Choose( - wallets = persistentListOf( - walletState, - walletState.copy(walletId = "2"), - ), - selectedWallet = walletState, - onChooseWalletClick = {}, - onCloseChoosingWalletClick = {}, - ) - - private val walletState: WalletState - get() = WalletState( - walletName = "My wallet", - walletId = "1", - artworkUrl = "", - onSelected = {}, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt deleted file mode 100644 index 9bf85280d4..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui - -import androidx.compose.runtime.Composable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.common.state.ChooseWalletState - -@Composable -internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - ) { - ChooseWalletScreen(state = it.chooseWalletState) - } -} - -internal class ChooseWalletBottomSheetConfig( - val chooseWalletState: ChooseWalletState.Choose, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt deleted file mode 100644 index 121faaba26..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui - -import android.content.res.Configuration -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.WalletState -import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData - -@Composable -internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) { - LazyColumn( - modifier = modifier - .background(TangemTheme.colors.background.tertiary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - item { - Box( - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44), - ) { - IconButton( - onClick = state.onCloseChoosingWalletClick, - modifier = Modifier - .align(Alignment.CenterStart) - .clickable { state.onCloseChoosingWalletClick() }, - ) { - Icon( - painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - ) - } - Text( - text = stringResource(id = R.string.manage_tokens_wallet_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - textAlign = TextAlign.Center, - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - ) - } - } - items( - count = state.wallets.count(), - key = { index -> state.wallets[index].walletId }, - ) { index -> - WalletItem( - wallet = state.wallets[index], - selectedWallet = state.selectedWallet, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.wallets.lastIndex, - addDefaultPadding = false, - ), - ) - } - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } - } -} - -@Composable -private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clickable { wallet.onSelected(wallet.walletId) } - .background(TangemTheme.colors.background.action) - .defaultMinSize(minHeight = TangemTheme.dimens.size72) - .padding(horizontal = TangemTheme.dimens.spacing16), - verticalAlignment = Alignment.CenterVertically, - ) { - SubcomposeAsyncImage( - modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50), - - model = ImageRequest.Builder(context = LocalContext.current) - .data(wallet.artworkUrl) - .crossfade(enable = true) - .build(), - loading = { - Image( - painter = painterResource(R.drawable.card_placeholder_black), - contentDescription = null, - ) - }, - error = { - Image( - painter = painterResource(R.drawable.card_placeholder_black), - contentDescription = null, - ) - }, - contentDescription = null, - ) - SpacerW12() - Text( - text = wallet.walletName, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - SpacerWMax() - if (selectedWallet == wallet) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseWalletScreen() { - TangemThemePreview { - ChooseWalletScreen( - state = ChooseWalletStatePreviewData.state, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt deleted file mode 100644 index 0bfc404f35..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui - -import androidx.compose.runtime.Composable -import com.tangem.core.ui.event.StateEvent -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.Event - -@Composable -internal fun EventEffect(event: StateEvent, onAlertStateSet: (AlertState) -> Unit) { - com.tangem.core.ui.event.EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is Event.ShowAlert -> onAlertStateSet(value.state) - } - }, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt deleted file mode 100644 index 14e49a02b6..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.AlertState - -@Composable -internal fun Alert(state: AlertState, onDismiss: () -> Unit) { - when (state) { - is AlertState.DefaultAlert, - is AlertState.NonNative, - AlertState.TokensUnsupportedCurve, - is AlertState.TokensUnsupported, - is AlertState.TokensUnsupportedBlockchainByCard, - is AlertState.CannotHideNetworkWithTokens, - is AlertState.TokenAlreadyAdded, - -> DefaultAlert(state, onDismiss) - is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss) - } -} - -@Composable -private fun DefaultAlert(state: AlertState, onDismiss: () -> Unit) { - BasicDialog( - message = state.message.resolveReference(), - confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, - ) -} - -@Composable -private fun TokenUnavailableAlert(state: AlertState.TokenUnavailable, onDismiss: () -> Unit) { - BasicDialog( - message = state.message.resolveReference(), - confirmButton = DialogButton( - title = state.confirmButtonText.resolveReference(), - onClick = onDismiss, - ), - dismissButton = DialogButton( - title = state.dismissButtonText.resolveReference(), - onClick = { state.onUpvoteClick() }, - ), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt deleted file mode 100644 index 553a6bd999..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -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.SpacerW -import com.tangem.core.ui.components.TangemSwitch -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData - -@Composable -internal fun NetworkItem( - state: NetworkItemState, - tokenState: TokenItemState.Loaded?, - modifier: Modifier = Modifier, - isSelected: Boolean = false, -) { - Row( - modifier = modifier - .background(TangemTheme.colors.background.action) - .defaultMinSize(minHeight = TangemTheme.dimens.size68) - .then( - if (state is NetworkItemState.Selectable) { - Modifier.clickable { state.onNetworkClick(state) } - } else { - Modifier - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - NetworkIcon(model = state) - SpacerW(width = TangemTheme.dimens.spacing12) - Text( - text = state.name, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - ) - SpacerW(width = TangemTheme.dimens.spacing6) - Text( - text = state.protocolName, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier - .weight(1f), - ) - if (state is NetworkItemState.Toggleable) { - TangemSwitch( - onCheckedChange = { - state.onToggleClick(tokenState!!, state) - }, - checked = state.isAdded.value, - ) - } else if (state is NetworkItemState.Selectable && isSelected) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } -} - -@Composable -internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) { - Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) { - val isAdded = when (model) { - is NetworkItemState.Selectable -> true - is NetworkItemState.Toggleable -> model.isAdded.value - } - - if (!isAdded) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size36) - .clip(CircleShape) - .background(TangemTheme.colors.control.unchecked), - ) - } - Icon( - painter = painterResource(id = model.iconRes), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size36), - tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, - ) - - if (model is NetworkItemState.Toggleable && model.isMainNetwork) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size10) - .clip(CircleShape) - .background(TangemTheme.colors.stroke.transparency), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size8) - .clip(CircleShape) - .background(TangemTheme.colors.icon.accent), - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_NetworkItem(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { - TangemThemePreview { - NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) - } -} - -private class NetworkItemStateProvider : CollectionPreviewParameterProvider( - collection = listOf( - NetworkItemState.Toggleable( - name = "Ethereum", - protocolName = "ETH", - iconResId = mutableStateOf(R.drawable.img_polygon_22), - isMainNetwork = true, - isAdded = mutableStateOf(true), - id = "", - address = "", - onToggleClick = { _, _ -> }, - decimals = 0, - ), - NetworkItemState.Toggleable( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_16), - isMainNetwork = false, - isAdded = mutableStateOf(false), - id = "", - address = "", - onToggleClick = { _, _ -> }, - decimals = 0, - ), - NetworkItemState.Selectable( - name = "Ethereum", - protocolName = "ETH", - iconResId = R.drawable.img_polygon_22, - id = "", - onNetworkClick = { }, - ), - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt deleted file mode 100644 index e7ad5cf31b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme - -@Composable -fun SimpleSelectionBlock( - title: String, - subtitle: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - roundedCorners: Boolean = true, -) { - Column( - modifier = modifier - .then( - if (roundedCorners) { - Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) - } else { - Modifier - }, - ) - .background(color = TangemTheme.colors.background.action) - .clickable { onClick() } - .padding( - horizontal = TangemTheme.dimens.spacing20, - vertical = TangemTheme.dimens.spacing16, - ) - .fillMaxWidth(), - ) { - Text( - text = title, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - ) - SpacerH(height = TangemTheme.dimens.spacing4) - Text( - text = subtitle, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_SimpleSelectionBlock() { - TangemThemePreview { - SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt deleted file mode 100644 index b005f36dee..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.common.utils - -import com.tangem.domain.tokens.model.CryptoCurrency - -internal object CurrencyUtils { - fun isAdded(address: String?, networkId: String, currencies: Collection): Boolean { - return if (address != null) { - currencies.any { - !it.isCustom && it is CryptoCurrency.Token && it.contractAddress == address && - it.network.backendId == networkId - } - } else { - currencies.any { - !it.isCustom && it is CryptoCurrency.Coin && it.network.backendId == networkId - } - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt deleted file mode 100644 index df2a7f015f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import kotlinx.collections.immutable.ImmutableList - -internal data class ChooseNetworkState( - val nativeNetworks: ImmutableList, - val nonNativeNetworks: ImmutableList, - val onNonNativeNetworkHintClick: () -> Unit, - val onCloseChooseNetworkScreen: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt deleted file mode 100644 index 0cfdfa16d1..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.managetokens.impl.R - -data class DerivationNotificationState( - val totalNeeded: Int, - val totalWallets: Int, - val walletsToDerive: Int, - val onGenerateClick: () -> Unit, -) { - val config = NotificationConfig( - title = resourceReference(id = R.string.warning_missing_derivation_title), - subtitle = pluralReference( - id = R.plurals.warning_missing_derivation_message, - count = totalNeeded, - formatArgs = wrappedList(totalNeeded), - ), - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(id = R.string.common_generate_addresses), - iconResId = R.drawable.ic_tangem_24, - onClick = onGenerateClick, - additionalText = pluralReference( - id = R.plurals.manage_tokens_number_of_wallets_android, - count = totalWallets, - formatArgs = wrappedList(walletsToDerive, totalWallets), - ), - ), - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt deleted file mode 100644 index 6738d56d3f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import androidx.paging.PagingData -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.StateEvent -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.Event -import kotlinx.coroutines.flow.Flow - -internal data class ManageTokensState( - val searchBarState: SearchBarState, - val tokens: Flow>, - val isLoading: Boolean, - val addCustomTokenButton: AddCustomTokenButton, - val chooseWalletState: ChooseWalletState, - val derivationNotification: DerivationNotificationState? = null, - val selectedToken: TokenItemState.Loaded? = null, - val showChooseWalletScreen: Boolean = false, - val customTokenBottomSheetConfig: TangemBottomSheetConfig, - val event: StateEvent, - val onEmptySearchResult: (String) -> Unit, -) - -data class AddCustomTokenButton( - val isVisible: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt deleted file mode 100644 index 3518626707..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import kotlinx.collections.immutable.ImmutableList - -internal sealed class QuotesState { - object Unknown : QuotesState() - - data class Content( - val priceChange: String, - val changeType: PriceChangeType, - val chartData: ImmutableList, - ) : QuotesState() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt deleted file mode 100644 index d64808239e..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -/** - * SearchBar state. - */ -internal data class SearchBarState( - val query: String, - val onQueryChange: (String) -> Unit, - val active: Boolean, - val onActiveChange: (Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt deleted file mode 100644 index 6c47a870ec..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -internal enum class TokenButtonType { - ADD, EDIT, NOT_AVAILABLE -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt deleted file mode 100644 index 0bd0c0d25d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.extensions.ImageReference - -internal data class TokenIconState( - val iconReference: ImageReference?, - val placeholderTint: Color, - val placeholderBackground: Color, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt deleted file mode 100644 index 7262bbe337..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import androidx.compose.runtime.MutableState - -internal sealed class TokenItemState { - - abstract val id: String - - data class Loading(override val id: String) : TokenItemState() - - data class Loaded( - override val id: String, - val name: String, - val currencySymbol: String, - val tokenId: String, - val tokenIcon: TokenIconState, - val quotes: QuotesState, - val rate: String?, - val availableAction: MutableState, - val chooseNetworkState: ChooseNetworkState, - val onButtonClick: (Loaded) -> Unit, - ) : TokenItemState() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt deleted file mode 100644 index d7531b2e85..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt +++ /dev/null @@ -1,194 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import androidx.paging.PagingData -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.domain.tokens.CurrencyCompatibilityError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.managetokens.presentation.common.state.* -import com.tangem.managetokens.presentation.common.utils.CurrencyUtils -import com.tangem.managetokens.presentation.managetokens.state.* -import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensClickIntents -import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensUiEvents -import com.tangem.utils.Provider -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.Flow - -internal class ManageTokensStateFactory( - private val currentStateProvider: Provider, - private val clickIntents: ManageTokensClickIntents, - private val uiIntents: ManageTokensUiEvents, -) { - - fun getInitialState(tokens: Flow>): ManageTokensState { - return ManageTokensState( - searchBarState = SearchBarState( - query = "", - onQueryChange = clickIntents::onSearchQueryChange, - active = false, - onActiveChange = clickIntents::onSearchActiveChange, - ), - tokens = tokens, - addCustomTokenButton = AddCustomTokenButton( - isVisible = false, - onClick = clickIntents::onAddCustomTokensButtonClick, - ), - derivationNotification = null, - isLoading = false, - event = consumedEvent(), - chooseWalletState = ChooseWalletState.NoSelection, - onEmptySearchResult = uiIntents::onEmptySearchResult, - customTokenBottomSheetConfig = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = uiIntents::onAddCustomTokenSheetDismissed, - content = TangemBottomSheetConfigContent.Empty, - ), - ) - } - - fun updateChooseWalletState( - wallets: List, - userWallets: List, - selectedWallet: UserWallet?, - ): ManageTokensState { - val chooseWalletState = when { - wallets.size == 1 -> { - ChooseWalletState.NoSelection - } - wallets.isEmpty() && userWallets.all { !it.isMultiCurrency } -> { - ChooseWalletState.Warning(ChooseWalletWarning.SINGLE_CURRENCY) - } - else -> { - var selectedWalletState: WalletState? = null - ChooseWalletState.Choose( - wallets = wallets.map { wallet -> - val walletState = WalletState( - walletId = wallet.walletId.stringValue, - artworkUrl = wallet.artworkUrl, - onSelected = clickIntents::onWalletSelected, - walletName = wallet.name, - ) - if (wallet.walletId.stringValue == selectedWallet?.walletId?.stringValue) { - selectedWalletState = walletState - } - walletState - }.toPersistentList(), - selectedWallet = selectedWalletState, - onChooseWalletClick = clickIntents::onChooseWalletClick, - onCloseChoosingWalletClick = clickIntents::onCloseChoosingWalletClick, - ) - } - } - return currentStateProvider().copy(chooseWalletState = chooseWalletState) - } - - fun showAddCustomTokensButton(show: Boolean): ManageTokensState { - return currentStateProvider().copy( - addCustomTokenButton = currentStateProvider().addCustomTokenButton.copy(isVisible = show), - ) - } - - fun updateSelectedWallet(selectedWalletId: String?): ManageTokensState { - val chooseWalletState = currentStateProvider().chooseWalletState - return currentStateProvider().copy( - showChooseWalletScreen = false, - chooseWalletState = if (chooseWalletState is ChooseWalletState.Choose) { - chooseWalletState.copy( - selectedWallet = chooseWalletState.wallets.find { - it.walletId == selectedWalletId - } ?: chooseWalletState.wallets.first(), - ) - } else { - chooseWalletState - }, - ) - } - - fun getStateAndTriggerEvent( - state: ManageTokensState, - event: Event, - setUiState: (ManageTokensState) -> Unit, - ): ManageTokensState { - return state.copy( - event = triggeredEvent( - data = event, - onConsume = { - val currentState = currentStateProvider() - setUiState(currentState.copy(event = consumedEvent())) - }, - ), - ) - } - - fun transformAddTokenErrorToAlert(error: CurrencyCompatibilityError, networkName: String): AlertState { - return when (error) { - CurrencyCompatibilityError.SolanaTokensUnsupported -> AlertState.TokensUnsupported(networkName) - CurrencyCompatibilityError.UnsupportedBlockchain -> AlertState.TokensUnsupportedBlockchainByCard( - networkName, - ) - CurrencyCompatibilityError.UnsupportedCurve -> AlertState.TokensUnsupportedCurve - } - } - - fun toggleNetworkState( - token: TokenItemState.Loaded, - network: NetworkItemState.Toggleable, - allAddedCurrencies: Collection, - ) { - network.changeToggleState() - val anyNetworkAdded = isAnyNetworkAdded( - networks = token.chooseNetworkState.nativeNetworks + token.chooseNetworkState.nonNativeNetworks, - allAddedCurrencies = allAddedCurrencies, - ) - val buttonType = if (anyNetworkAdded) TokenButtonType.EDIT else TokenButtonType.ADD - token.availableAction.value = buttonType - } - - private fun isAnyNetworkAdded( - networks: List, - allAddedCurrencies: Collection, - ): Boolean { - return networks.any { - it is NetworkItemState.Toggleable && CurrencyUtils.isAdded( - address = it.address, - networkId = it.id, - currencies = allAddedCurrencies, - ) - } - } - - fun updateTokenNetworksOnTokenSelection( - token: TokenItemState.Loaded, - addedCurrenciesOnWallet: Collection, - ) { - token.chooseNetworkState.nativeNetworks.forEach { - if (it is NetworkItemState.Toggleable) { - val isAdded = CurrencyUtils.isAdded(it.address, it.id, addedCurrenciesOnWallet) - if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() - } - } - token.chooseNetworkState.nonNativeNetworks.forEach { - if (it is NetworkItemState.Toggleable) { - val isAdded = CurrencyUtils.isAdded(it.address, it.id, addedCurrenciesOnWallet) - if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() - } - } - } - - fun updateDerivationNotification(totalNeeded: Int, totalWallets: Int, walletsToDerive: Int): ManageTokensState { - val derivationNotificationState = if (totalNeeded == 0) { - null - } else { - DerivationNotificationState( - totalNeeded = totalNeeded, - totalWallets = totalWallets, - walletsToDerive = walletsToDerive, - onGenerateClick = clickIntents::onGetAddressesClick, - ) - } - return currentStateProvider().copy(derivationNotification = derivationNotificationState) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt deleted file mode 100644 index 01af86d1f2..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import com.tangem.core.ui.extensions.getActiveIconResByNetworkId -import com.tangem.core.ui.extensions.getGreyedOutIconResByNetworkId -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Token -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.common.utils.CurrencyUtils -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class NetworkToNetworkItemStateConverter( - private val addedCurrenciesByWalletProvider: Provider>>, - private val selectedWalletProvider: Provider, - private val onNetworkToggleClick: (token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) -> Unit, -) : Converter { - - override fun convert(value: Token.Network): NetworkItemState { - return createManageNetworkContent(value) - } - - private fun createManageNetworkContent(network: Token.Network): NetworkItemState { - val addedCurrencies = addedCurrenciesByWalletProvider()[selectedWalletProvider()] ?: emptyList() - val isAdded = CurrencyUtils.isAdded( - address = network.address, - networkId = network.networkId, - currencies = addedCurrencies, - ) - return NetworkItemState.Toggleable( - name = network.name, - iconResId = mutableIntStateOf( - getNetworkIconResId(isAdded, network.networkId), // todo - ), - isMainNetwork = isMainNetwork(network), - isAdded = mutableStateOf(isAdded), - id = network.networkId, - protocolName = network.standardType, - address = network.address, - decimals = network.decimalCount, - onToggleClick = onNetworkToggleClick, - ) - } - - private fun getNetworkIconResId(isAdded: Boolean, networkId: String): Int { - return if (isAdded) { - getActiveIconResByNetworkId(networkId) - } else { - getGreyedOutIconResByNetworkId(networkId) - } - } - - private fun isMainNetwork(network: Token.Network) = network.address == null -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt deleted file mode 100644 index 99509e85ea..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import com.tangem.domain.tokens.model.Token -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toPersistentList - -internal class NetworksToChooseNetworkStateConverter( - private val networkToNetworkItemStateConverter: NetworkToNetworkItemStateConverter, - private val onNonNativeNetworkHintClick: () -> Unit, - private val onCloseChooseNetworkScreen: () -> Unit, -) : Converter, ChooseNetworkState> { - - override fun convert(value: List): ChooseNetworkState { - return createChooseNetworksState(value) - } - - private fun createChooseNetworksState(networks: List): ChooseNetworkState { - val nativeNetworks = mutableListOf() - val nonNativeNetworks = mutableListOf() - networks.map { networkToNetworkItemStateConverter.convert(it) }.forEach { - if (it is NetworkItemState.Toggleable && it.isMainNetwork) { - nativeNetworks.add(it) - } else { - nonNativeNetworks.add(it) - } - } - return ChooseNetworkState( - nativeNetworks = nativeNetworks.toPersistentList(), - nonNativeNetworks = nonNativeNetworks.toPersistentList(), - onNonNativeNetworkHintClick = onNonNativeNetworkHintClick, - onCloseChooseNetworkScreen = onCloseChooseNetworkScreen, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt deleted file mode 100644 index 972c004779..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.tokens.model.Quote -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal - -@Suppress("MagicNumber") // TODO: remove when chart data is added (in [REDACTED_TASK_KEY] when endpoint is ready) -internal class QuotesToQuotesStateConverter : Converter { - override fun convert(value: Quote): QuotesState { - val priceChange = value.priceChange - return QuotesState.Content( - priceChange = BigDecimalFormatter.formatPercent( - percent = priceChange.movePointLeft(2), - useAbsoluteValue = true, - ), - changeType = priceChange.getPriceChangeType(), - chartData = // TODO (in [REDACTED_TASK_KEY] when endpoint is ready) - when (priceChange.getPriceChangeType()) { - PriceChangeType.UP -> persistentListOf(0f, 5f, 10f, 30f) - PriceChangeType.DOWN -> persistentListOf(15f, 12f, 13f, 18f, 10f, 3f) - PriceChangeType.NEUTRAL -> persistentListOf(0f, 0f, 0f, 0f) - }, - ) - } - - private fun BigDecimal.getPriceChangeType(): PriceChangeType { - return PriceChangeConverter.fromBigDecimal(value = this) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt deleted file mode 100644 index 156c65d70d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import androidx.compose.runtime.mutableStateOf -import com.tangem.core.ui.extensions.ImageReference -import com.tangem.core.ui.extensions.getTintForTokenIcon -import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Token -import com.tangem.managetokens.presentation.common.utils.CurrencyUtils -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType -import com.tangem.managetokens.presentation.managetokens.state.TokenIconState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class TokenConverter( - private val quotesStateConverter: QuotesToQuotesStateConverter, - private val networksToChooseNetworkStateConverter: NetworksToChooseNetworkStateConverter, - private val allAddedCurrencies: Provider>, - private val selectedAppCurrency: Provider, - private val onTokenItemButtonClick: (TokenItemState.Loaded) -> Unit, -) : Converter { - - override fun convert(value: Token): TokenItemState.Loaded { - val isAnyAdded = value.networks.any { network -> - CurrencyUtils.isAdded( - address = network.address, - networkId = network.networkId, - currencies = allAddedCurrencies(), - ) - } - val buttonType = when { - value.isAvailable && isAnyAdded -> TokenButtonType.EDIT - value.isAvailable && !isAnyAdded -> TokenButtonType.ADD - else -> TokenButtonType.NOT_AVAILABLE - } - - val background = tryGetBackgroundForTokenIcon(value.networks.firstOrNull()?.address ?: "") - val tint = getTintForTokenIcon(background) - - return TokenItemState.Loaded( - id = value.id, - name = value.name, - currencySymbol = value.symbol, - tokenId = value.id, - tokenIcon = TokenIconState( - ImageReference.Url(value.iconUrl), - placeholderBackground = background, - placeholderTint = tint, - ), - quotes = value.quote?.let { quotesStateConverter.convert(it) } ?: QuotesState.Unknown, - rate = value.quote?.fiatRate?.let { rate -> - BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate, - fiatCurrencyCode = selectedAppCurrency().code, - fiatCurrencySymbol = selectedAppCurrency().symbol, - ) - }, - availableAction = mutableStateOf(buttonType), - chooseNetworkState = networksToChooseNetworkStateConverter.convert(value.networks), - onButtonClick = onTokenItemButtonClick, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt deleted file mode 100644 index a7a6832cc6..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import com.tangem.data.tokens.utils.CryptoCurrencyFactory -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.utils.converter.Converter - -internal class TokenToCryptoCurrencyConverter( - private val network: NetworkItemState, - private val derivationStyleProvider: DerivationStyleProvider, -) : Converter { - - override fun convert(value: TokenItemState.Loaded): CryptoCurrency? { - return if (network is NetworkItemState.Toggleable && network.address != null) { - CryptoCurrencyFactory().createToken( - CryptoCurrencyFactory.Token( - symbol = value.currencySymbol, - name = value.name, - id = value.tokenId, - contractAddress = network.address, - decimals = requireNotNull(network.decimals), - ), - networkId = network.id, - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = null, - ) - } else { - CryptoCurrencyFactory().createCoin( - networkId = network.id, - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = null, - ) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt deleted file mode 100644 index f66b65bc1b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import androidx.compose.runtime.mutableStateOf -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState -import kotlinx.collections.immutable.toImmutableList - -internal object ChooseNetworkStatePreviewData { - - val state = ChooseNetworkState( - nativeNetworks = nativeNetworks.toImmutableList(), - nonNativeNetworks = nonNativeNetworks.toImmutableList(), - onNonNativeNetworkHintClick = {}, - onCloseChooseNetworkScreen = {}, - ) -} - -internal val nativeNetworks = listOf( - NetworkItemState.Toggleable( - name = "Ethereum", - protocolName = "ETH", - iconResId = mutableStateOf(R.drawable.img_polygon_22), - isMainNetwork = true, - isAdded = mutableStateOf(true), - id = "", - onToggleClick = { _, _ -> }, - address = "", - decimals = 0, - ), -) - -internal val nonNativeNetworks = listOf( - NetworkItemState.Toggleable( - name = "Ethereum", - protocolName = "ETH", - iconResId = mutableStateOf(R.drawable.img_kusama_22), - isMainNetwork = false, - isAdded = mutableStateOf(true), - id = "1", - onToggleClick = { _, _ -> }, - address = "", - decimals = 0, - ), - NetworkItemState.Toggleable( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_16), - isMainNetwork = false, - isAdded = mutableStateOf(false), - id = "2", - onToggleClick = { _, _ -> }, - address = "", - decimals = 0, - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt deleted file mode 100644 index b5d88b814a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import com.tangem.managetokens.presentation.managetokens.state.DerivationNotificationState - -object DerivationNotificationStatePreviewData { - val state = DerivationNotificationState( - totalNeeded = 5, - totalWallets = 3, - walletsToDerive = 2, - onGenerateClick = {}, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt deleted file mode 100644 index eab841b41f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import androidx.paging.PagingData -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.event.consumedEvent -import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData -import com.tangem.managetokens.presentation.managetokens.state.* -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.SearchBarState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import kotlinx.coroutines.flow.flowOf - -internal object ManageTokensStatePreviewData { - val loadedState: ManageTokensState - get() = ManageTokensState( - searchBarState = searchState, - tokens = flowOf(PagingData.from(tokens)), - isLoading = false, - addCustomTokenButton = AddCustomTokenButton(true, {}), - derivationNotification = DerivationNotificationStatePreviewData.state, - event = consumedEvent(), - chooseWalletState = ChooseWalletStatePreviewData.state, - onEmptySearchResult = {}, - customTokenBottomSheetConfig = TangemBottomSheetConfig(false, {}, TangemBottomSheetConfigContent.Empty), - ) - - val loadingState: ManageTokensState - get() = loadedState.copy(isLoading = true) - - private val tokens: List - get() = listOf( - TokenItemStatePreviewData.loadedPriceDown, - TokenItemStatePreviewData.loadedPriceUp, - TokenItemStatePreviewData.loadedPriceNeutral, - ) - - private val searchState: SearchBarState - get() = SearchBarState( - query = "", - onQueryChange = {}, - active = false, - onActiveChange = {}, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt deleted file mode 100644 index 1ba8f760e6..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType -import com.tangem.managetokens.presentation.managetokens.state.TokenIconState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import kotlinx.collections.immutable.persistentListOf - -internal object TokenItemStatePreviewData { - - val tokenLoading: TokenItemState - get() = TokenItemState.Loading("id") - - val loadedPriceDown: TokenItemState - get() = TokenItemState.Loaded( - id = "BTC", - name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - tokenId = "BTC", - currencySymbol = "BTC", - tokenIcon = tokenIconState, - quotes = QuotesState.Content( - priceChange = "0.43%", - changeType = PriceChangeType.DOWN, - chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f), - ), - rate = "31 285.72$", - availableAction = mutableStateOf(TokenButtonType.ADD), - onButtonClick = {}, - chooseNetworkState = ChooseNetworkStatePreviewData.state, - ) - - val loadedPriceUp: TokenItemState - get() = TokenItemState.Loaded( - id = "BTC", - name = "Bitcoin", - tokenId = "BTC", - currencySymbol = "BTC", - tokenIcon = tokenIconState, - quotes = QuotesState.Content( - priceChange = "0.43%", - changeType = PriceChangeType.UP, - chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f), - ), - rate = "31 285.72$", - availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE), - onButtonClick = {}, - chooseNetworkState = ChooseNetworkStatePreviewData.state, - ) - - val loadedPriceNeutral: TokenItemState - get() = TokenItemState.Loaded( - id = "BTC", - name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - tokenId = "BTC", - currencySymbol = "BTC", - tokenIcon = tokenIconState, - quotes = QuotesState.Content( - priceChange = "0.00%", - changeType = PriceChangeType.NEUTRAL, - chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 10f), - ), - rate = "31 285.72$", - availableAction = mutableStateOf(TokenButtonType.ADD), - onButtonClick = {}, - chooseNetworkState = ChooseNetworkStatePreviewData.state, - ) - - private val tokenIconState: TokenIconState - get() = TokenIconState( - iconReference = null, - placeholderTint = Color.White, - placeholderBackground = Color.Black, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt deleted file mode 100644 index 5cbdc20a93..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui - -import androidx.compose.runtime.Composable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -@Composable -internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - ) { - ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState) - } -} - -internal class ChooseNetworkBottomSheetConfig( - val selectedToken: TokenItemState.Loaded, - val chooseWalletState: ChooseWalletState, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt deleted file mode 100644 index 2f5d51df8a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.WarningCardTitleOnly -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData -import com.tangem.managetokens.presentation.common.ui.components.NetworkItem -import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock -import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData - -@Composable -internal fun ChooseNetworkScreen( - state: TokenItemState.Loaded, - walletState: ChooseWalletState, - modifier: Modifier = Modifier, -) { - val networkState = state.chooseNetworkState - LazyColumn( - contentPadding = PaddingValues( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - modifier = modifier - .background(TangemTheme.colors.background.tertiary), - ) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth(), - ) - } - - when (walletState) { - is ChooseWalletState.Choose -> { - item { - SpacerH(height = TangemTheme.dimens.spacing10) - } - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.manage_tokens_network_selector_wallet), - subtitle = walletState.selectedWallet?.walletName ?: "", - onClick = walletState.onChooseWalletClick, - ) - } - } - ChooseWalletState.NoSelection -> Unit - is ChooseWalletState.Warning -> { - item { - SpacerH(height = TangemTheme.dimens.spacing10) - } - item { - WarningCardTitleOnly( - title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title), - ) - } - } - } - - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } - - if (networkState.nativeNetworks.isNotEmpty()) { - this@LazyColumn.nativeNetworks(networkState = networkState, tokenState = state) - } - - if (networkState.nonNativeNetworks.isNotEmpty()) { - this@LazyColumn.nonNativeNetworks(networkState = networkState, tokenState = state) - } - } -} - -private fun LazyListScope.nativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_native_title), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption1, - ) - SpacerH(height = TangemTheme.dimens.spacing2) - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) - SpacerH(height = TangemTheme.dimens.spacing8) - } - - items( - count = networkState.nativeNetworks.count(), - key = { index -> networkState.nativeNetworks[index].id }, - ) { index -> - NetworkItem( - state = networkState.nativeNetworks[index], - tokenState = tokenState, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = networkState.nativeNetworks.lastIndex, - addDefaultPadding = false, - ), - ) - } - - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } -} - -private fun LazyListScope.nonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { - item { - NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick) - SpacerH(height = TangemTheme.dimens.spacing8) - } - - items( - count = networkState.nonNativeNetworks.count(), - key = { index -> networkState.nonNativeNetworks[index].id }, - ) { index -> - NetworkItem( - state = networkState.nonNativeNetworks[index], - tokenState = tokenState, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = networkState.nonNativeNetworks.lastIndex, - addDefaultPadding = false, - ), - ) - } - - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } -} - -@Composable -private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { - Column { - Row { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption1, - ) - SpacerW(width = TangemTheme.dimens.spacing2) - Icon( - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.inactive, - contentDescription = null, - modifier = Modifier - .size(TangemTheme.dimens.size16) - .clickable { onNonNativeNetworkHintClick() }, - ) - } - SpacerH(height = TangemTheme.dimens.spacing2) - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseNetworkScreen() { - TangemThemePreview { - ChooseNetworkScreen( - state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, - walletState = ChooseWalletStatePreviewData.state, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt deleted file mode 100644 index 6b91d83e18..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui - -import android.content.res.Configuration -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Surface -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -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.dp -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.SpacerH18 -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenBottomSheet -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig -import com.tangem.managetokens.presentation.common.ui.EventEffect -import com.tangem.managetokens.presentation.common.ui.components.Alert -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData -import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification -import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList -import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar - -@Composable -internal fun ManageTokensScreen(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) { - var alertState by remember { mutableStateOf(value = null) } - - EventEffect( - event = state.event, - onAlertStateSet = { alertState = it }, - ) - alertState?.let { - Alert(state = it, onDismiss = { alertState = null }) - } - - Content(state = state, onHeaderSizeChange = onHeaderSizeChange) - - AddCustomTokenBottomSheet(state.customTokenBottomSheetConfig) -} - -@Composable -private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) { - val keyboard by keyboardAsState() - val density = LocalDensity.current - var tokenListAlertBottomPadding by remember(keyboard is Keyboard.Opened) { mutableStateOf(0.dp) } - - Box( - modifier = Modifier - .fillMaxSize() - .imePadding() - .background(color = TangemTheme.colors.background.primary), - ) { - Column { - val listState = rememberLazyListState() - val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } - val elevation by animateDpAsState( - targetValue = if (raiseSearchBar) TangemTheme.dimens.elevation8 else TangemTheme.dimens.elevation0, - label = "top_bar_elevation", - ) - - Surface( - elevation = elevation, - modifier = Modifier.onGloballyPositioned { - with(density) { onHeaderSizeChange(it.size.height.toDp()) } - }, - ) { - TokensSearchBar( - state = state.searchBarState, - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing4, - ), - ) - } - - SpacerH18() - - val tokens = state.tokens.collectAsLazyPagingItems() - val query = state.searchBarState.query - - TrackPossibleEmptySearchResult( - tokens = tokens, - query = query, - onEmptySearchResult = state.onEmptySearchResult, - ) - - TokensList( - modifier = Modifier.padding(bottom = tokenListAlertBottomPadding), - tokens = tokens, - addCustomTokenButton = state.addCustomTokenButton, - ) - } - - state.selectedToken?.let { selectedToken -> - ManageTokensBottomSheet(selectedToken = selectedToken, state = state) - } - - state.derivationNotification?.let { - if (keyboard is Keyboard.Closed) { - DerivationNotification( - config = it.config, - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - with(density) { tokenListAlertBottomPadding = it.size.height.toDp() } - }, - ) - DisposableEffect(Unit) { - onDispose { tokenListAlertBottomPadding = 0.dp } - } - } - } - } -} - -@Composable -private fun TrackPossibleEmptySearchResult( - tokens: LazyPagingItems, - query: String, - onEmptySearchResult: (String) -> Unit, -) { - val wasLoading = remember { mutableStateOf(false) } - - LaunchedEffect(tokens.loadState) { - val isLoading = tokens.loadState.refresh == LoadState.Loading - val stoppedLoading = wasLoading.value && !isLoading - val queryAndTokensCondition = query.isNotEmpty() && tokens.itemSnapshotList.isEmpty() - - if (stoppedLoading && queryAndTokensCondition) { - onEmptySearchResult(query) - } - - wasLoading.value = isLoading - } -} - -@Composable -private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) { - if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) { - val config = TangemBottomSheetConfig( - isShow = true, - content = ChooseWalletBottomSheetConfig(state.chooseWalletState), - onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick, - ) - ChooseWalletBottomSheet(config) - } else { - val config = TangemBottomSheetConfig( - isShow = true, - content = ChooseNetworkBottomSheetConfig( - selectedToken = selectedToken, - chooseWalletState = state.chooseWalletState, - ), - onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen, - ) - ChooseNetworkBottomSheet(config) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ManageTokensScreen( - @PreviewParameter(ManageTokensConfigProvider::class) - state: ManageTokensState, -) { - TangemThemePreview { - ManageTokensScreen(state) {} - } -} - -private class ManageTokensConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - ManageTokensStatePreviewData.loadingState, - ManageTokensStatePreviewData.loadedState, - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt deleted file mode 100644 index 32962c70db..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material3.Text -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.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.managetokens.impl.R - -@Composable -internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size68) - .fillMaxWidth() - .clickable { onButtonClick() } - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size36) - .background(color = TangemTheme.colors.button.secondary, shape = CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_plus_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - SpacerW(width = TangemTheme.dimens.spacing12) - Text( - text = stringResource(id = R.string.add_custom_token_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun AddCustomTokenButton_Preview() { - TangemThemePreview { - AddCustomTokenButton(onButtonClick = { }) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt deleted file mode 100644 index d1f3822c9b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material3.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.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryButtonIconEndTwoLines -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData - -@Composable -internal fun DerivationNotification(config: NotificationConfig, modifier: Modifier = Modifier) { - BaseContainer( - modifier = modifier, - ) { - Column( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - ) { - MainContent( - iconResId = config.iconResId, - iconTint = TangemTheme.colors.icon.accent, - title = config.title, - subtitle = config.subtitle, - ) - val buttonConfig = config.buttonsState - if (buttonConfig is NotificationConfig.ButtonsState.PrimaryButtonConfig) { - PrimaryButtonIconEndTwoLines( - text = buttonConfig.text.resolveReference(), - iconResId = buttonConfig.iconResId ?: R.drawable.ic_tangem_24, - onClick = buttonConfig.onClick, - modifier = Modifier - .fillMaxWidth(), - additionalText = buttonConfig.additionalText?.resolveReference(), - ) - } - } - } -} - -@Composable -private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Card( - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size62) - .fillMaxWidth(), - shape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, - ), - elevation = TangemTheme.dimens.elevation12, - backgroundColor = TangemTheme.colors.background.action, - ) { - Box(content = content) - } -} - -@Composable -private fun MainContent(iconResId: Int, iconTint: Color, title: TextReference, subtitle: TextReference) { - Row { - NotificationIcon(iconResId = iconResId, iconTint = iconTint) - SpacerW(width = TangemTheme.dimens.spacing10) - TextsBlock(title = title, subtitle = subtitle) - } -} - -@Composable -private fun RowScope.NotificationIcon(iconResId: Int, iconTint: Color) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .align(alignment = Alignment.CenterVertically), - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size36) - .background( - color = iconTint.copy(alpha = 0.12f), - shape = CircleShape, - ), - ) - Box( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .background( - color = TangemTheme.colors.background.action, - shape = CircleShape, - ), - ) - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - tint = iconTint, - ) - } -} - -@Composable -private fun TextsBlock(title: TextReference, subtitle: TextReference) { - Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { - Text( - text = title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - ) - - Text( - text = subtitle.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ManageTokensScreen() { - TangemThemePreview { - DerivationNotification(DerivationNotificationStatePreviewData.state.config) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt deleted file mode 100644 index 1a18c5ee93..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -/** - * A chart with a solid line and gradient underneath. It can accept values of any range and number. - * If the last value is bigger or equal to the first, the chart is of accent color, otherwise it's warning color. - * - * @param values a list of float values for a chart. - **/ -@Composable -fun PriceChangesChart(values: ImmutableList, modifier: Modifier = Modifier) { - Row(modifier = modifier) { - if (values.size < 2) return // escape without drawing when there are not enough points - - val lineColor = if (values.last() >= values.first()) { - TangemTheme.colors.icon.accent - } else { - TangemTheme.colors.icon.warning - } - val gradient = Brush.verticalGradient( - colors = listOf(lineColor.copy(alpha = 0.21f), lineColor.copy(alpha = 0.0f)), - ) - Chart(list = values, lineColor = lineColor, gradient = gradient, modifier = Modifier.weight(1f)) - } -} - -@Composable -private fun Chart(list: ImmutableList, lineColor: Color, gradient: Brush, modifier: Modifier = Modifier) { - val max = list.max() - val min = list.min() - val zipList: List> = list.zipWithNext() - - for (pair in zipList) { - val fromValuePercentage = getValuePercentageForRange(pair.first, max, min) - val toValuePercentage = getValuePercentageForRange(pair.second, max, min) - - Canvas( - modifier = modifier.fillMaxHeight(), - onDraw = { - val fromPoint = Offset(x = 0f, y = size.height.times(1 - fromValuePercentage)) - val toPoint = Offset(x = size.width, y = size.height.times(1 - toValuePercentage)) - - val path = drawChartLineAndCreatePath(fromPoint = fromPoint, toPoint = toPoint, lineColor = lineColor) - - fillChart( - path = path, - fromPoint = fromPoint, - toPoint = toPoint, - size = size, - gradient = gradient, - ) - }, - ) - } -} - -private fun DrawScope.drawChartLineAndCreatePath(fromPoint: Offset, toPoint: Offset, lineColor: Color): Path { - val path = Path() - path.moveTo(fromPoint.x, fromPoint.y) - path.lineTo(toPoint.x, toPoint.y) - drawPath( - path = path, - color = lineColor, - style = Stroke(width = 1f), - ) - return path -} - -private fun DrawScope.fillChart(path: Path, fromPoint: Offset, toPoint: Offset, size: Size, gradient: Brush) { - path.lineTo(toPoint.x, size.height) - path.lineTo(fromPoint.x, size.height) - path.lineTo(0f, fromPoint.y) - drawPath( - path = path, - brush = gradient, - ) -} - -private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Float { - return if (max == min) { // to draw a straight line when all values are the same - val modifiedMax = max + 1 - val modifiedMin = min - 1 - (value - modifiedMin) / (modifiedMax - modifiedMin) - } else { - (value - min) / (max - min) - } -} - -@Preview(widthDp = 150, heightDp = 150, showBackground = true) -@Composable -private fun Chart_Positive_Preview() { - TangemThemePreview(isDark = true) { - PriceChangesChart( - persistentListOf(1f, 2f, 4f, 1f, 5f), - ) - } -} - -@Preview(widthDp = 150, heightDp = 150, showBackground = true) -@Composable -private fun Chart_Negative_Preview() { - TangemThemePreview(isDark = true) { - PriceChangesChart( - persistentListOf(10f, 2f, 4f, 1f, 5f), - ) - } -} - -@Preview(widthDp = 150, heightDp = 150, showBackground = true) -@Composable -private fun Chart_Neutral_Preview() { - TangemThemePreview(isDark = true) { - PriceChangesChart( - persistentListOf(5f, 2f, 4f, 1f, 5f), - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt deleted file mode 100644 index 8faf911ebb..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -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.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.SearchBarState - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -internal fun TokensSearchBar(state: SearchBarState, modifier: Modifier = Modifier) { - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current - - TextField( - value = state.query, - onValueChange = state.onQueryChange, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { - keyboardController?.hide() - focusManager.clearFocus() - }, - ), - singleLine = true, - maxLines = 1, - textStyle = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.primary1, - ), - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_search_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - modifier = Modifier.clickable { state.onActiveChange(true) }, - ) - }, - trailingIcon = { - if (state.query.isNotEmpty() || state.active) { - IconButton( - onClick = { - if (state.query.isNotEmpty()) { - state.onQueryChange("") - } - focusManager.clearFocus() - keyboardController?.hide() - state.onActiveChange(false) - }, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_close), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } - }, - placeholder = { - Text( - text = stringResource(R.string.manage_tokens_search_placeholder), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - }, - shape = RoundedCornerShape(TangemTheme.dimens.radius36), - colors = searchbarTextFieldColors(), - modifier = modifier - .fillMaxWidth() - .onFocusChanged { - if (it.isFocused) { - state.onActiveChange(true) - } else { - state.onActiveChange(false) - } - }, - ) -} - -@Composable -private fun searchbarTextFieldColors(): TextFieldColors { - return TextFieldDefaults.textFieldColors( - backgroundColor = TangemTheme.colors.field.primary, - textColor = TangemTheme.colors.text.primary1, - cursorColor = TangemTheme.colors.icon.primary1, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokensSearchBar( - @PreviewParameter(SearchBarkConfigProvider::class) - state: SearchBarState, -) { - TangemThemePreview { - TokensSearchBar(state) - } -} - -private class SearchBarkConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - SearchBarState( - query = "BTC", - onQueryChange = {}, - active = true, - onActiveChange = {}, - ), - SearchBarState( - query = "", - onQueryChange = {}, - active = false, - onActiveChange = {}, - ), - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt deleted file mode 100644 index 3829ec91db..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.components.buttons.PrimarySmallButton -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType - -@Composable -internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: Modifier = Modifier) { - when (type) { - TokenButtonType.ADD -> PrimarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.manage_tokens_add), - onClick = onClick, - ), - modifier = modifier, - ) - TokenButtonType.EDIT -> SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.manage_tokens_edit), - onClick = onClick, - ), - modifier = modifier, - ) - TokenButtonType.NOT_AVAILABLE -> { - Box( - modifier = modifier - .size(height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size46) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( - bounded = false, - radius = TangemTheme.dimens.size20, - ), - onClick = onClick, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } - } -} - -@Preview(backgroundColor = 0xffffff, showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun TokenButton_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { - TangemThemePreview { - TokenButton(type = type, {}) - } -} - -private class TokenButtonTypeProvider : PreviewParameterProvider { - override val values = sequenceOf( - TokenButtonType.ADD, - TokenButtonType.EDIT, - TokenButtonType.NOT_AVAILABLE, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt deleted file mode 100644 index 262ebcf808..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.extensions.ImageReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.TokenIconState -import kotlinx.coroutines.launch - -@Composable -internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { - val iconModifier = modifier.size(TangemTheme.dimens.size36) - if (state.iconReference != null) { - DefaultCurrencyIcon( - modifier = iconModifier, - iconReference = state.iconReference, - errorIcon = { - PlaceholderIcon( - modifier = iconModifier, - tint = state.placeholderTint, - background = state.placeholderBackground, - ) - }, - ) - } else { - PlaceholderIcon( - modifier = iconModifier, - tint = state.placeholderTint, - background = state.placeholderBackground, - ) - } -} - -@Composable -private fun PlaceholderIcon(tint: Color, background: Color, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .background( - color = background, - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier.matchParentSize(), - painter = painterResource(id = R.drawable.ic_custom_token_44), - tint = tint, - contentDescription = null, - ) - } -} - -@Composable -private inline fun DefaultCurrencyIcon( - iconReference: ImageReference, - crossinline errorIcon: @Composable () -> Unit, - modifier: Modifier = Modifier, -) { - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - var isBackgroundColorDefined by remember { mutableStateOf(false) } - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() } - - SubcomposeAsyncImage( - modifier = modifier - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ), - model = ImageRequest.Builder(context = LocalContext.current) - .data(iconReference.getReference()) - .size(size = pixelsSize) - .memoryCacheKey(key = iconReference.getReference().toString() + pixelsSize) - .crossfade(enable = true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (!isBackgroundColorDefined && isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = itemBackgroundColor, - size = pixelsSize, - ).getContrastColor(isDarkTheme = true) - iconBackgroundColor = color - isBackgroundColorDefined = true - } - } - }, - ) - .build(), - loading = { LoadingIcon() }, - error = { errorIcon() }, - contentDescription = null, - ) -} - -@Composable -internal fun LoadingIcon(modifier: Modifier = Modifier) { - CircleShimmer(modifier = modifier) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt deleted file mode 100644 index 28b7691794..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -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.TextOverflow -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.QuotesState - -@Composable -internal fun TokenPriceChange(state: QuotesState, modifier: Modifier = Modifier) { - when (state) { - is QuotesState.Content -> - PriceChangeBlock(modifier = modifier, type = state.changeType, text = state.priceChange) - QuotesState.Unknown -> PriceChangeBlock(modifier = modifier) - } -} - -@Composable -private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeType? = null, text: String? = null) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - PriceChangeIcon(type = type) - SpacerW4() - PriceChangeText(type = type, text = text) - } -} - -@Composable -private fun PriceChangeIcon(type: PriceChangeType?) { - AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType -> - animatedType ?: return@AnimatedContent - - Icon( - painter = painterResource( - id = when (animatedType) { - PriceChangeType.UP -> R.drawable.ic_arrow_up_8 - PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 - PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 - }, - ), - tint = when (animatedType) { - PriceChangeType.UP -> TangemTheme.colors.icon.accent - PriceChangeType.DOWN -> TangemTheme.colors.icon.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive - }, - contentDescription = null, - ) - } -} - -@Composable -private fun PriceChangeText(type: PriceChangeType?, text: String?) { - AnimatedContent(targetState = text, label = "Update the price change's text") { animatedText -> - animatedText ?: return@AnimatedContent - - Text( - text = animatedText, - color = when (type) { - PriceChangeType.UP -> TangemTheme.colors.text.accent - PriceChangeType.DOWN -> TangemTheme.colors.text.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled - null -> TangemTheme.colors.text.primary1 - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt deleted file mode 100644 index 50cf504643..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp -import com.tangem.core.ui.components.* -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData - -private val TOKEN_ITEM_HEIGHT: Dp - @Composable - @ReadOnlyComposable - get() = TangemTheme.dimens.size68 - -@Composable -internal fun TokenRowItem(state: TokenItemState, modifier: Modifier = Modifier) { - when (state) { - is TokenItemState.Loading -> LoadingTokenItem(modifier) - is TokenItemState.Loaded -> LoadedTokenItem(state, modifier) - } -} - -@Composable -private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = Modifier) { - BoxWithConstraints( - modifier = modifier - .fillMaxWidth() - .defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT) - .background(TangemTheme.colors.background.primary), - contentAlignment = Alignment.CenterStart, - ) { - val width = maxWidth - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) { - TokenIcon(state = state.tokenIcon) - SpacerW12() - - Column( - modifier = Modifier - .weight(weight = 1f), - ) { - TokenName(name = state.name, currencyId = state.currencySymbol) - TokenPriceData(price = state.rate, quotesState = state.quotes) - } - SpacerW24() - - if (width > TangemTheme.dimens.size350 && // hide chart for small screens - state.quotes is QuotesState.Content - ) { - Chart(quotes = state.quotes) - SpacerW24() - } - - TokenButton( - type = state.availableAction.value, - onClick = { state.onButtonClick(state) }, - ) - } - } -} - -@Composable -private fun TokenName(name: String, currencyId: String) { - Row { - Text( - text = name, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(weight = 1f, fill = false), - ) - SpacerW(width = TangemTheme.dimens.spacing6) - Text( - text = currencyId, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } -} - -@Composable -private fun TokenPriceData(price: String?, quotesState: QuotesState) { - if (price != null) { - Row { - Text( - text = price, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(weight = 1f, fill = false), - ) - SpacerW(width = TangemTheme.dimens.spacing6) - TokenPriceChange(state = quotesState) - } - } -} - -@Composable -private fun Chart(quotes: QuotesState.Content) { - Box( - modifier = Modifier - .size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size28), - ) { - PriceChangesChart(values = quotes.chartData) - } -} - -@Composable -private fun LoadingTokenItem(modifier: Modifier = Modifier) { - BaseSurface(modifier) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing4, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size36)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10)) { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size70, - height = TangemTheme.dimens.size12, - ), - ) - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size52, - height = TangemTheme.dimens.size12, - ), - ) - } - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size46, - height = TangemTheme.dimens.size12, - ), - ) - } - } - } -} - -@Composable -private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) { - Surface( - modifier = modifier.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT), - color = TangemTheme.colors.background.primary, - ) { - content() - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_Tokens(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemThemePreview { - TokenRowItem(state) - } -} - -private class TokenConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - TokenItemStatePreviewData.tokenLoading, - TokenItemStatePreviewData.loadedPriceDown, - TokenItemStatePreviewData.loadedPriceUp, - TokenItemStatePreviewData.loadedPriceNeutral, - ), -) -// endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt deleted file mode 100644 index ca26ef0621..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -private const val PLACEHOLDER_ITEMS_COUNT = 50 - -@Composable -internal fun TokensList( - tokens: LazyPagingItems, - addCustomTokenButton: AddCustomTokenButton, - modifier: Modifier = Modifier, -) { - LazyColumn(modifier = modifier) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_list_header_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) - } - - if (tokens.loadState.refresh is LoadState.Loading) { - items(PLACEHOLDER_ITEMS_COUNT) { - TokenRowItem(state = TokenItemState.Loading(it.toString())) - } - } else { - val tokensList = tokens.itemSnapshotList - if (tokensList.isEmpty()) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_nothing_found), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - ), - ) - } - } - - items(items = tokensList.items, key = TokenItemState::id) { token -> - TokenRowItem(state = token) - } - - if (addCustomTokenButton.isVisible) { - item { - AddCustomTokenButton(onButtonClick = addCustomTokenButton.onClick) - } - } - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt deleted file mode 100644 index 2b6ba5b3bd..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -internal interface ManageTokensClickIntents { - - fun onAddCustomTokensButtonClick() - - fun onSearchQueryChange(query: String) - - fun onSearchActiveChange(active: Boolean) - - fun onTokenItemButtonClick(token: TokenItemState.Loaded) - - fun onGetAddressesClick() - - fun onBackClick() - - fun onCloseChooseNetworkScreen() - - fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) - - fun onNonNativeNetworkHintClick() - - fun onChooseWalletClick() - - fun onCloseChoosingWalletClick() - - fun onWalletSelected(walletId: String) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt deleted file mode 100644 index 663871cf2a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -internal interface ManageTokensUiEvents { - - fun onEmptySearchResult(query: String) - - fun onAddCustomTokenSheetDismissed() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt deleted file mode 100644 index caa5ca105b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ /dev/null @@ -1,445 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -import androidx.compose.runtime.* -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.map -import arrow.core.getOrElse -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase -import com.tangem.features.managetokens.navigation.ExpandableState -import com.tangem.managetokens.presentation.common.analytics.ManageTokens -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.Event -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.factory.* -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.Collections -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject -import kotlin.collections.set -import kotlin.properties.Delegates - -@Suppress("LongParameterList", "LargeClass") -@HiltViewModel -internal class ManageTokensViewModel @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, - private val getGlobalTokenListUseCase: GetGlobalTokenListUseCase, - private val getWalletsUseCase: GetWalletsUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val selectWalletUseCase: SelectWalletUseCase, - private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, - private val fetchTokenListUseCase: FetchTokenListUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val checkCurrencyCompatibilityUseCase: CheckCurrencyCompatibilityUseCase, - private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, -) : ViewModel(), ManageTokensClickIntents, ManageTokensUiEvents { - - private val stateFactory = ManageTokensStateFactory( - currentStateProvider = Provider { uiState }, - clickIntents = this, - uiIntents = this, - ) - - var uiState: ManageTokensState by mutableStateOf(stateFactory.getInitialState(flowOf(PagingData.from(emptyList())))) - private set - - private var expandableState: ExpandableState = ExpandableState.COLLAPSED - - private val currenciesListJobHolder: JobHolder = JobHolder() - - private val debouncer = Debouncer() - - private var allAddedCurrencies: MutableList = Collections.synchronizedList( - mutableListOf(), - ) - - private var wallets: CopyOnWriteArrayList by Delegates.notNull() - - private var addedCurrenciesByWallet: MutableMap> = ConcurrentHashMap() - - private var selectedWallet: UserWallet? = null - - private var currenciesToGenerateAddresses: Map> = emptyMap() - - private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - - private val quotesStateConverter = QuotesToQuotesStateConverter() - - private val networkToNetworkItemStateConverter = NetworkToNetworkItemStateConverter( - addedCurrenciesByWalletProvider = Provider { addedCurrenciesByWallet }, - selectedWalletProvider = Provider { selectedWallet }, - onNetworkToggleClick = this::onNetworkToggleClick, - ) - - private val networksToChooseNetworkStateConverter = NetworksToChooseNetworkStateConverter( - networkToNetworkItemStateConverter = networkToNetworkItemStateConverter, - onNonNativeNetworkHintClick = this::onNonNativeNetworkHintClick, - onCloseChooseNetworkScreen = this::onCloseChooseNetworkScreen, - ) - - private val tokenConverter = TokenConverter( - quotesStateConverter = quotesStateConverter, - networksToChooseNetworkStateConverter = networksToChooseNetworkStateConverter, - allAddedCurrencies = Provider { allAddedCurrencies }, - selectedAppCurrency = Provider { selectedAppCurrencyFlow.value }, - onTokenItemButtonClick = this::onTokenItemButtonClick, - ) - - init { - analyticsEventHandler.send(ManageTokens.ScreenOpened()) - - viewModelScope.launch(dispatchers.io) { - getWalletsUseCase() - .distinctUntilChanged() - .collectLatest { userWallets -> - launch { - subscribeToCurrencies(userWallets) - }.saveIn(currenciesListJobHolder) - } - } - } - - fun setExpandableState(state: State) { - expandableState = state.value - } - - private suspend fun subscribeToCurrencies(userWallets: List) { - wallets = CopyOnWriteArrayList(userWallets.filter { it.isMultiCurrency && !it.isLocked }) - - combine(wallets.map { getCurrenciesUseCase.invoke(it.walletId).distinctUntilChanged() }) { - if (expandableState == ExpandableState.EXPANDED) return@combine - - allAddedCurrencies.clear() - addedCurrenciesByWallet.clear() - - val walletsWithCurrencies = wallets.zip( - it.map { currencyList -> - currencyList.getOrElse { - Timber.e("Couldn't retrieve currency list") - emptyList() - } - }, - ) - - allAddedCurrencies = walletsWithCurrencies.flatMap { it.second }.toMutableList() - - walletsWithCurrencies.forEach { (wallet, currencies) -> - addedCurrenciesByWallet[wallet] = currencies.toMutableList() - } - - withContext(dispatchers.main) { - uiState = uiState.copy(tokens = getInitialTokensList()) - } - selectedWallet = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { if (!it.isMultiCurrency || it.isLocked) null else it }, - ) - if (selectedWallet == null && wallets.isNotEmpty()) { - selectWalletUseCase(wallets.first().walletId) - selectedWallet = wallets.first() - } - updateDerivationNotificationState() - withContext(dispatchers.main) { - uiState = stateFactory.updateChooseWalletState(wallets, userWallets, selectedWallet) - } - }.collect() - } - - private fun getInitialTokensList(searchText: String = ""): Flow> { - return getGlobalTokenListUseCase(searchText = searchText).map { - it.map { token -> tokenConverter.convert(token) } - } - } - - private fun updateDerivationNotificationState() { - viewModelScope.launch(dispatchers.io) { - getMissedAddressesCryptoCurrenciesUseCase(wallets.map { it.walletId }) - .distinctUntilChanged() - .collectLatest { - it.onRight { mapOfMissingDerivations -> - currenciesToGenerateAddresses = mapOfMissingDerivations - withContext(dispatchers.main) { updateDerivation() } - } - } - } - } - - private fun updateDerivation() { - val totalNeeded = currenciesToGenerateAddresses.values.sumOf { derivations -> derivations.size } - val walletsToDerive = currenciesToGenerateAddresses.values - .filter { derivations -> derivations.isNotEmpty() }.size - uiState = stateFactory.updateDerivationNotification( - totalNeeded = totalNeeded, - totalWallets = wallets.size, - walletsToDerive = walletsToDerive, - ) - } - - private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - } - - override fun onAddCustomTokensButtonClick() { - analyticsEventHandler.send(ManageTokens.ButtonCustomToken) - uiState = uiState.copy(customTokenBottomSheetConfig = uiState.customTokenBottomSheetConfig.copy(isShow = true)) - } - - override fun onSearchQueryChange(query: String) { - uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(query = query)) - - debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val state = stateFactory.showAddCustomTokensButton(query.isNotBlank()) - uiState = state.copy(tokens = getInitialTokensList(query)) - } - } - - override fun onSearchActiveChange(active: Boolean) { - uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(active = active)) - } - - override fun onTokenItemButtonClick(token: TokenItemState.Loaded) { - when (token.availableAction.value) { - TokenButtonType.ADD, TokenButtonType.EDIT -> { - if (token.availableAction.value == TokenButtonType.ADD) { - analyticsEventHandler.send(ManageTokens.ButtonAdd(token.currencySymbol)) - } - if (token.availableAction.value == TokenButtonType.EDIT) { - analyticsEventHandler.send(ManageTokens.ButtonEdit(token.currencySymbol)) - } - - uiState = uiState.copy(selectedToken = token) - val addedCurrenciesOnWallet = addedCurrenciesByWallet[selectedWallet] ?: listOf() - stateFactory.updateTokenNetworksOnTokenSelection(token, addedCurrenciesOnWallet) - } - TokenButtonType.NOT_AVAILABLE -> { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert( - AlertState.TokenUnavailable( - onUpvoteClick = {}, // TODO later, when endpoint is available - ), - ), - setUiState = { uiState = it }, - ) - } - } - } - - override fun onGetAddressesClick() { - if (currenciesToGenerateAddresses.isNotEmpty()) { - viewModelScope.launch(dispatchers.io) { - val cardCount = currenciesToGenerateAddresses.count { it.value.isNotEmpty() } - analyticsEventHandler.send(ManageTokens.ButtonGenerateAddresses(cardCount)) - - currenciesToGenerateAddresses.forEach { (walletId, currenciesToDerive) -> - if (currenciesToDerive.isNotEmpty()) { - derivePublicKeysUseCase(walletId, currenciesToDerive) - .onRight { - updateDerivationNotificationState() - fetchTokenListUseCase(userWalletId = walletId) - } - } - } - } - } - } - - override fun onBackClick() { - TODO("Not yet implemented") // TODO: implement if needed when custom tokens and bottom sheet is complete - } - - override fun onCloseChooseNetworkScreen() { - uiState = uiState.copy(selectedToken = null) - } - - override fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) { - val selectedWallet = selectedWallet ?: return - if (!selectedWallet.isMultiCurrency || selectedWallet.isLocked) return - - if (network.isAdded.value) { - analyticsEventHandler.send( - ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.Off), - ) - toggleToken(token, network, selectedWallet) - } else { - analyticsEventHandler.send( - ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.On), - ) - viewModelScope.launch(dispatchers.io) { - checkCompatibilityAndToggleToken(token, network, selectedWallet) - } - } - } - - private suspend fun checkCompatibilityAndToggleToken( - token: TokenItemState.Loaded, - network: NetworkItemState.Toggleable, - selectedWallet: UserWallet, - ) { - checkCurrencyCompatibilityUseCase( - networkId = network.id, - isMainNetwork = network.address == null, - userWalletId = selectedWallet.walletId, - ) - .fold( - ifLeft = { error -> - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert( - stateFactory.transformAddTokenErrorToAlert( - error, - network.name, - ), - ), - setUiState = { uiState = it }, - ) - }, - ifRight = { - withContext(dispatchers.main) { toggleToken(token, network, selectedWallet) } - }, - ) - } - - private fun toggleToken( - token: TokenItemState.Loaded, - network: NetworkItemState.Toggleable, - selectedWallet: UserWallet, - ) { - val cryptoCurrency = requireNotNull( - TokenToCryptoCurrencyConverter( - network = network, - derivationStyleProvider = selectedWallet.scanResponse.derivationStyleProvider, - ).convert(token), - ) { - "It is only null if Blockchain is Unknown, which mustn't happen here" - } - if (!network.isAdded.value) { - addedCurrenciesByWallet[selectedWallet]?.add(cryptoCurrency) - allAddedCurrencies.add(cryptoCurrency) - viewModelScope.launch(dispatchers.io) { - addCryptoCurrenciesUseCase( - userWalletId = selectedWallet.walletId, - currency = cryptoCurrency, - ) - } - updateUi(token, network) - } else { - viewModelScope.launch(dispatchers.io) { - if (canBeRemovedAndShowAlertIfNot(selectedWallet.walletId, cryptoCurrency)) { - addedCurrenciesByWallet[selectedWallet]?.remove(cryptoCurrency) - allAddedCurrencies.remove(cryptoCurrency) - removeCurrencyUseCase(selectedWallet.walletId, cryptoCurrency) - withContext(dispatchers.main) { updateUi(token, network) } - } - } - } - } - - private fun updateUi(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) { - stateFactory.toggleNetworkState(token, network, allAddedCurrencies) - updateDerivationNotificationState() - } - - private suspend fun canBeRemovedAndShowAlertIfNot( - walletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): Boolean { - return if (cryptoCurrency is CryptoCurrency.Coin && - !isCryptoCurrencyCoinCouldHide(walletId, cryptoCurrency) - ) { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert( - AlertState.CannotHideNetworkWithTokens( - tokenName = cryptoCurrency.name, - currencySymbol = cryptoCurrency.symbol, - networkName = cryptoCurrency.network.name, - ), - ), - setUiState = { uiState = it }, - ) - false - } else { - true - } - } - - override fun onNonNativeNetworkHintClick() { - analyticsEventHandler.send(ManageTokens.NoticeNonNativeNetworkClicked) - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert(AlertState.NonNative), - setUiState = { uiState = it }, - ) - } - - override fun onChooseWalletClick() { - analyticsEventHandler.send(ManageTokens.ButtonChooseWallet) - uiState = uiState.copy( - showChooseWalletScreen = true, - ) - } - - override fun onCloseChoosingWalletClick() { - uiState = uiState.copy( - showChooseWalletScreen = false, - ) - } - - override fun onWalletSelected(walletId: String) { - analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.MainToken)) - viewModelScope.launch(dispatchers.io) { - selectWalletUseCase(UserWalletId(walletId)) - } - selectedWallet = wallets.find { it.walletId.stringValue == walletId } - uiState.selectedToken?.let { onTokenItemButtonClick(it) } - uiState = stateFactory.updateSelectedWallet(selectedWalletId = selectedWallet?.walletId?.stringValue) - } - - override fun onEmptySearchResult(query: String) { - analyticsEventHandler.send(ManageTokens.TokenIsNotFound(query)) - } - - override fun onAddCustomTokenSheetDismissed() { - uiState = uiState.copy(customTokenBottomSheetConfig = uiState.customTokenBottomSheetConfig.copy(isShow = false)) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt deleted file mode 100644 index 9bcf70872f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.managetokens.presentation.router - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.unit.Dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.tangem.features.managetokens.navigation.ExpandableState -import com.tangem.features.managetokens.navigation.ManageTokensUi -import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen -import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel -import javax.inject.Inject - -internal class ManageTokensUiImpl @Inject constructor() : ManageTokensUi { - - @Composable - override fun Content(onHeaderSizeChange: (Dp) -> Unit, state: State) { - val viewModel = hiltViewModel() - viewModel.setExpandableState(state) - - ManageTokensScreen( - state = viewModel.uiState, - onHeaderSizeChange = onHeaderSizeChange, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 6535806903..7c3629f145 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -21,9 +20,6 @@ internal class WalletFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies - @Inject - internal lateinit var manageTokensUi: ManageTokensUi - /** Feature router */ @Inject internal lateinit var walletRouter: WalletRouter @@ -37,7 +33,6 @@ internal class WalletFragment : ComposeFragment() { override fun ScreenContent(modifier: Modifier) { _walletRouter.Initialize( onFinish = requireActivity()::finish, - manageTokensUi = manageTokensUi, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 509a1b3085..5397088c1e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.common.preview -import androidx.compose.runtime.mutableStateOf import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent @@ -12,7 +11,6 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.persistentListOf internal object WalletScreenPreviewData { @@ -151,7 +149,6 @@ internal object WalletScreenPreviewData { internal val walletScreenState = WalletScreenState( onBackClick = {}, - manageTokensExpandableState = mutableStateOf(ExpandableState.COLLAPSED), topBarConfig = topBarConfig, selectedWalletIndex = 0, wallets = persistentListOf( @@ -161,6 +158,5 @@ internal object WalletScreenPreviewData { onWalletChange = {}, event = consumedEvent(), isHidingMode = false, - manageTokenRedesignToggle = false, ) } \ 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 292cea1bde..815f0493b4 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,9 +1,9 @@ package com.tangem.feature.wallet.presentation.router import android.annotation.SuppressLint -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -25,8 +25,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.managetokens.navigation.ExpandableState -import com.tangem.features.managetokens.navigation.ManageTokensUi import kotlin.properties.Delegates /** Default implementation of wallet feature router */ @@ -42,7 +40,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi) { + override fun Initialize(onFinish: () -> Unit) { this.onFinish = onFinish NavHost( @@ -55,20 +53,8 @@ internal class DefaultWalletRouter( subscribeToLifecycle(LocalLifecycleOwner.current) } - var bottomSheetHeaderHeight by remember { mutableStateOf(0.dp) } - WalletScreen( state = viewModel.uiState.collectAsStateWithLifecycle().value, - bottomSheetHeaderHeightProvider = { bottomSheetHeaderHeight }, - bottomSheetContent = { - val state = remember { mutableStateOf(ExpandableState.COLLAPSED) } - // Manage Tokens - manageTokensUi.Content( - onHeaderSizeChange = { bottomSheetHeaderHeight = it }, - state = state, - ) - viewModel.setExpandableState(state) - }, ) } 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 6b05b626ce..d213d227f8 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 @@ -2,10 +2,8 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable - import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter /** @@ -26,7 +24,7 @@ internal interface InnerWalletRouter : WalletRouter { */ @Suppress("TopLevelComposableFunctions") @Composable - fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi) + fun Initialize(onFinish: () -> Unit) /** Pop back stack */ fun popBackStack() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index e8c3c9114c..af25a1257b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state -import androidx.compose.runtime.mutableStateOf import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.wallets.models.UserWalletId @@ -11,8 +10,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarCon import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,9 +23,7 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class WalletStateController @Inject constructor( - private val manageTokensFeatureToggles: ManageTokensFeatureToggles, -) { +internal class WalletStateController @Inject constructor() { val uiState: StateFlow get() = mutableUiState @@ -87,8 +82,6 @@ internal class WalletStateController @Inject constructor( onWalletChange = {}, event = consumedEvent(), isHidingMode = false, - manageTokenRedesignToggle = manageTokensFeatureToggles.isRedesignedScreenEnabled, - manageTokensExpandableState = mutableStateOf(ExpandableState.EXPANDED), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 5c9424f9ee..9285575d9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -1,18 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.model -import androidx.compose.runtime.MutableState import com.tangem.core.ui.event.StateEvent -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.ImmutableList internal data class WalletScreenState( val onBackClick: () -> Unit, - val manageTokensExpandableState: MutableState, val topBarConfig: WalletTopBarConfig, val selectedWalletIndex: Int, val wallets: ImmutableList, val onWalletChange: (Int) -> Unit, val event: StateEvent, val isHidingMode: Boolean, - val manageTokenRedesignToggle: Boolean, ) \ 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 0a72f0cf4e..0de613caa1 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 @@ -2,11 +2,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.TweenSpec -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -15,28 +10,22 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig @@ -66,16 +55,10 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Composable -internal fun WalletScreen( - state: WalletScreenState, - bottomSheetHeaderHeightProvider: () -> Dp, - bottomSheetContent: @Composable () -> Unit, -) { +internal fun WalletScreen(state: WalletScreenState) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -98,9 +81,6 @@ internal fun WalletScreen( snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, - bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, - bottomSheetContent = bottomSheetContent, - alertConfig = alertConfig, ) WalletEventEffect( @@ -120,10 +100,7 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - bottomSheetHeaderHeightProvider: () -> Dp, onAutoScrollReset: () -> Unit, - bottomSheetContent: @Composable () -> Unit, - alertConfig: WalletAlertState?, ) { var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] } @@ -218,282 +195,12 @@ private fun WalletContent( ) } - if (state.manageTokenRedesignToggle) { - BaseScaffoldManageTokenRedesign( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, - bottomSheetContent = bottomSheetContent, - alertConfig = alertConfig, - ) { - scaffoldContent() - } - } else { - BaseScaffold( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - ) { - scaffoldContent() - } - } -} - -@Suppress("LongParameterList", "LongMethod") -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Composable -private fun BaseScaffoldManageTokenRedesign( - state: WalletScreenState, - selectedWallet: WalletState, - snackbarHostState: SnackbarHostState, - bottomSheetHeaderHeightProvider: () -> Dp, - bottomSheetContent: @Composable () -> Unit, - alertConfig: WalletAlertState?, - content: @Composable () -> Unit, -) { - // show the bottom sheet if there is at least one multicurrency wallet - val showManageTokensBottomSheet = remember(state.wallets) { - state.wallets.any { it is WalletState.MultiCurrency } - } - val bottomSheetState = rememberSheetStateEnhanced( - initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden, - confirmValueChange = { sheetValue -> - when { - sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false - sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false - else -> true - } - }, - skipHiddenState = showManageTokensBottomSheet, - ) - - val keyboardShown = keyboardAsState() - - BottomSheetStateEffects( - bottomSheetState = bottomSheetState, + BaseScaffold( state = state, - showManageTokensBottomSheet = showManageTokensBottomSheet, - alertConfig = alertConfig, - keyboardShown = keyboardShown, - ) - - val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = bottomSheetState, + selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, - ) - - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } - val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight - - val coroutineScope = rememberCoroutineScope() - - BottomSheetScaffold( - snackbarHost = { - WalletSnackbarHost( - snackbarHostState = it, - event = state.event, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), - ) - }, - containerColor = TangemTheme.colors.background.secondary, - sheetContainerColor = TangemTheme.colors.background.primary, - scaffoldState = scaffoldState, - sheetPeekHeight = peekHeight, - sheetDragHandle = { - Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary)) - }, - sheetContent = { - BoxWithConstraints { - Box( - modifier = Modifier - .sizeIn(maxHeight = maxHeight - statusBarHeight) - .align(Alignment.BottomCenter), - ) { - bottomSheetContent() - } - } - - // hide bottom sheet when back pressed - BackHandler( - keyboardShown.value is Keyboard.Closed && - bottomSheetState.currentValue == SheetValue.Expanded, - ) { - coroutineScope.launch { bottomSheetState.partialExpand() } - } - }, - content = { _ -> - val pullRefreshState = rememberPullRefreshState( - refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) - }, - ) - - Column { - WalletTopBar(config = state.topBarConfig) - Box( - modifier = Modifier.pullRefresh(pullRefreshState), - ) { - content() - - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) - } - } - - BottomSheetScrim( - color = BottomSheetDefaults.ScrimColor, - visible = bottomSheetState.targetValue == SheetValue.Expanded, - onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, - ) - }, - ) -} - -@Composable -private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { - val alpha by animateFloatAsState( - targetValue = if (visible) 1f else 0f, - animationSpec = TweenSpec(), - label = "scrim", - ) - val dismissSheet = if (visible) { - Modifier - .pointerInput(onDismissRequest) { - detectTapGestures { - onDismissRequest() - } - } - .clearAndSetSemantics {} - } else { - Modifier - } - Canvas( - Modifier - .fillMaxSize() - .then(dismissSheet), ) { - drawRect(color = color, alpha = alpha) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Suppress("CyclomaticComplexMethod", "MagicNumber") -@Composable -private fun BottomSheetStateEffects( - bottomSheetState: SheetState, - state: WalletScreenState, - showManageTokensBottomSheet: Boolean, - alertConfig: WalletAlertState?, - keyboardShown: State, -) { - // Bottom sheet during initialization internally expand partially after its content was remeasured, - // therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected - // so we have to manually restrict expansion in this case - LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) { - if (!showManageTokensBottomSheet && - (bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden) - ) { - bottomSheetState.hide() - } - } - // react to changes in wallet list - LaunchedEffect(showManageTokensBottomSheet) { - when { - showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> { - bottomSheetState.partialExpand() - } - !showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> { - bottomSheetState.hide() - } - } - } - - val systemUiController = rememberSystemUiController() - val navigationBarColor = TangemTheme.colors.background.primary - - LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) { - when (bottomSheetState.targetValue) { - SheetValue.Hidden, - SheetValue.Expanded, - -> systemUiController.setNavigationBarColor( - color = Color.Transparent, - darkIcons = navigationBarColor.luminance() > 0.5f, - navigationBarContrastEnforced = true, - ) - SheetValue.PartiallyExpanded, - -> systemUiController.setNavigationBarColor(navigationBarColor) - } - } - - DisposableEffect(showManageTokensBottomSheet) { - onDispose { - if (showManageTokensBottomSheet) { - systemUiController.setNavigationBarColor( - color = Color.Transparent, - darkIcons = navigationBarColor.luminance() > 0.5f, - navigationBarContrastEnforced = false, - ) - } - } - } - - // expand bottom sheet when keyboard appears - LaunchedEffect(keyboardShown.value is Keyboard.Opened) { - if (keyboardShown.value is Keyboard.Opened && alertConfig == null) { - bottomSheetState.expand() - } - } - - val keyboardController = LocalSoftwareKeyboardController.current - // hide keyboard when bottom sheet is about to be hidden - LaunchedEffect(Unit) { - snapshotFlow { - bottomSheetState.currentValue == SheetValue.Expanded && - bottomSheetState.targetValue == SheetValue.PartiallyExpanded - }.collect { sheetHasBeenHidden -> - if (sheetHasBeenHidden) { - keyboardController?.hide() - } - } - } - - val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded - LaunchedEffect(isSheetHidden) { - if (isSheetHidden) { - state.manageTokensExpandableState.value = ExpandableState.COLLAPSED - } else { - state.manageTokensExpandableState.value = ExpandableState.EXPANDED - } - } -} - -/** - * Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718 - * Current material3 version: 1.2.0 - */ -@Composable -@ExperimentalMaterial3Api -private fun rememberSheetStateEnhanced( - skipPartiallyExpanded: Boolean = false, - confirmValueChange: (SheetValue) -> Boolean = { true }, - initialValue: SheetValue = SheetValue.Hidden, - skipHiddenState: Boolean = false, -): SheetState { - val density = LocalDensity.current - return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) { - SheetState( - skipPartiallyExpanded = skipPartiallyExpanded, - density = density, - initialValue = initialValue, - confirmValueChange = confirmValueChange, - skipHiddenState = skipHiddenState, - ) + scaffoldContent() } } @@ -621,8 +328,6 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - bottomSheetHeaderHeightProvider = { 0.dp }, - bottomSheetContent = {}, ) } } 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 8a7bf1d28d..5a63ee153a 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,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import androidx.compose.runtime.MutableState import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -28,7 +27,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import com.tangem.features.managetokens.navigation.ExpandableState import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -100,12 +98,6 @@ internal class WalletViewModel @Inject constructor( clickIntents.initialize(router, viewModelScope) } - fun setExpandableState(state: MutableState) { - stateHolder.update { - it.copy(manageTokensExpandableState = state) - } - } - fun subscribeToLifecycle(lifecycleOwner: LifecycleOwner) { lifecycleOwner.lifecycle.addObserver(screenLifecycleProvider) } From ff3f1a26a5179e8e2be04ef0e813ba2ec03411a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 25 Jun 2024 18:48:17 +0500 Subject: [PATCH 086/223] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 47 +++---- .../converters/YieldBalanceConverter.kt | 34 ++++- .../converters/YieldBalanceListConverter.kt | 21 ++- .../domain/staking/model/YieldBalance.kt | 17 ++- .../domain/staking/model/YieldBalanceList.kt | 18 ++- .../FetchStakingYieldBalanceUseCase.kt | 5 +- .../staking/GetStakingYieldBalanceUseCase.kt | 9 +- .../staking/repositories/StakingRepository.kt | 14 +- .../tokendetails/TokenDetailsPreviewData.kt | 26 ++-- .../tokendetails/state/StakingBlockState.kt | 10 +- .../state/TokenDetailsBalanceBlockState.kt | 8 +- ...TokenDetailsBalanceSelectStateConverter.kt | 75 ++++++++++ .../TokenDetailsLoadedBalanceConverter.kt | 130 +++++++++++++++--- .../state/factory/TokenDetailsStateFactory.kt | 24 ++-- .../tokendetails/state/utils/BalanceUtils.kt | 12 ++ .../tokendetails/ui/TokenDetailsScreen.kt | 21 +-- .../ui/components/TokenDetailsBalanceBlock.kt | 9 +- .../components/staking/StakingBalanceBlock.kt | 15 +- .../viewmodels/TokenDetailsViewModel.kt | 46 +++++-- 19 files changed, 420 insertions(+), 121 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 379877723a..0acc8d6b43 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -14,7 +14,7 @@ import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.model.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -110,7 +110,7 @@ internal class DefaultStakingRepository( override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - networkStatus: NetworkStatus, + address: String, integrationId: String, refresh: Boolean, ) = withContext(dispatchers.io) { @@ -120,7 +120,7 @@ internal class DefaultStakingRepository( block = { val result = stakeKitApi.getSingleYieldBalance( integrationId = integrationId, - body = getBalanceRequestData(networkStatus), + body = getBalanceRequestData(address, integrationId), ).getOrThrow() stakingBalanceStore.store( @@ -136,20 +136,27 @@ internal class DefaultStakingRepository( override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - networkStatus: NetworkStatus, + address: String, integrationId: String, - ): Flow> = channelFlow { + ): Flow = channelFlow { launch(dispatchers.io) { stakingBalanceStore.get(integrationId) .collectLatest { - send(yieldBalanceConverter.convertList(it)) + send( + yieldBalanceConverter.convert( + YieldBalanceConverter.Data( + balance = it, + integrationId = integrationId, + ), + ), + ) } } withContext(dispatchers.io) { fetchSingleYieldBalance( userWalletId, - networkStatus, + address, integrationId, ) } @@ -157,7 +164,7 @@ internal class DefaultStakingRepository( override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - networks: Set, + addresses: List, integrationId: String, refresh: Boolean, ) = withContext(dispatchers.io) { @@ -166,7 +173,7 @@ internal class DefaultStakingRepository( skipCache = refresh, block = { val result = stakeKitApi.getMultipleYieldBalances( - networks.map(::getBalanceRequestData), + addresses.map { getBalanceRequestData(it.address, integrationId) }, ).getOrThrow() stakingBalanceStore.store(result) @@ -176,18 +183,18 @@ internal class DefaultStakingRepository( override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - networks: Set, + addresses: List, integrationId: String, - ): Flow> = channelFlow { + ): Flow = channelFlow { launch(dispatchers.io) { stakingBalanceStore.get() - .collectLatest { send(yieldBalanceListConverter.convertList(it)) } + .collectLatest { send(yieldBalanceListConverter.convert(it)) } } withContext(dispatchers.io) { fetchMultiYieldBalance( userWalletId, - networks, + addresses, integrationId, ) } @@ -202,23 +209,17 @@ internal class DefaultStakingRepository( return yields.map { yieldConverter.convert(it) } } - private fun getBalanceRequestData(networkStatus: NetworkStatus): YieldBalanceRequestBody { - val networkAddress = when (val network = networkStatus.value) { - NetworkStatus.MissedDerivation -> null - is NetworkStatus.NoAccount -> network.address - is NetworkStatus.Unreachable -> network.address - is NetworkStatus.Verified -> network.address - } - val address = networkAddress?.defaultAddress?.value.orEmpty() + private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody { return YieldBalanceRequestBody( addresses = Address( address = address, additionalAddresses = null, // todo fill additional addresses metadata if needed - explorerUrl = "", // todo fill exporer url + explorerUrl = "", // todo fill exporer url [REDACTED_JIRA] ), args = YieldBalanceRequestBody.YieldBalanceRequestArgs( - validatorAddresses = listOf(), // todo add validator addresses + validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA] ), + integrationId = integrationId, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index 7a3182cc87..0ab969880e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -1,16 +1,36 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.domain.staking.model.BalanceItem import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceItem import com.tangem.utils.converter.Converter -internal class YieldBalanceConverter : Converter { - override fun convert(value: BalanceDTO): YieldBalance { - return YieldBalance( - type = BalanceType.valueOf(value.type.name), - amount = value.amount, - pricePerShare = value.pricePerShare, - ) +internal class YieldBalanceConverter : Converter { + + override fun convert(value: Data): YieldBalance { + return if (value.balance.isEmpty()) { + YieldBalance.Empty + } else { + YieldBalance.Data( + balance = YieldBalanceItem( + items = value.balance.map { item -> + BalanceItem( + type = BalanceType.valueOf(item.type.name), + amount = item.amount, + pricePerShare = item.pricePerShare, + rawCurrencyId = item.tokenDTO.coinGeckoId, + ) + }, + integrationId = value.integrationId, + ), + ) + } } + + data class Data( + val balance: List, + val integrationId: String?, + ) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index d0c4eea878..883ba21acd 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -4,15 +4,26 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.domain.staking.model.YieldBalanceList import com.tangem.utils.converter.Converter -internal class YieldBalanceListConverter : Converter { +internal class YieldBalanceListConverter : Converter, YieldBalanceList> { internal val converter by lazy(LazyThreadSafetyMode.NONE) { YieldBalanceConverter() } - override fun convert(value: YieldBalanceWrapperDTO): YieldBalanceList { - return YieldBalanceList( - balances = converter.convertList(value.balances), - ) + override fun convert(value: List): YieldBalanceList { + return if (value.isEmpty()) { + YieldBalanceList.Empty + } else { + YieldBalanceList.Data( + balances = value.map { + converter.convert( + YieldBalanceConverter.Data( + balance = it.balances, + integrationId = it.integrationId, + ), + ) + }, + ) + } } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt index 817743ac7c..1b22df4bed 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt @@ -2,10 +2,25 @@ package com.tangem.domain.staking.model import java.math.BigDecimal -data class YieldBalance( +sealed class YieldBalance { + + data class Data( + val balance: YieldBalanceItem, + ) : YieldBalance() + + data object Empty : YieldBalance() +} + +data class YieldBalanceItem( + val items: List, + val integrationId: String?, +) + +data class BalanceItem( val type: BalanceType, val amount: BigDecimal, val pricePerShare: BigDecimal, + val rawCurrencyId: String?, ) enum class BalanceType { diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt index 931e8fa879..22db0dfdf1 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt @@ -1,5 +1,17 @@ package com.tangem.domain.staking.model -data class YieldBalanceList( - val balances: List, -) \ No newline at end of file +sealed class YieldBalanceList { + + data class Data( + val balances: List, + ) : YieldBalanceList() { + fun getBalance(rawCurrencyId: String?): YieldBalance? { + return balances.firstOrNull { yield -> + (yield as? YieldBalance.Data)?.balance?.items + ?.any { it.rawCurrencyId == rawCurrencyId } == true + } + } + } + + data object Empty : YieldBalanceList() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 46f0fdf101..bdbb1521d7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( @@ -11,13 +10,13 @@ class FetchStakingYieldBalanceUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - networkStatus: NetworkStatus, + address: String, integrationId: String, refresh: Boolean = false, ): Either = Either.catch { stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, - networkStatus = networkStatus, + address = address, integrationId = integrationId, refresh = refresh, ) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index 8821767a15..5e12367173 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -6,7 +6,6 @@ import arrow.core.right import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -17,14 +16,14 @@ class GetStakingYieldBalanceUseCase( operator fun invoke( userWalletId: UserWalletId, - networkStatus: NetworkStatus, + address: String, integrationId: String, - ): EitherFlow> { + ): EitherFlow { return stakingRepository.getSingleYieldBalanceFlow( userWalletId = userWalletId, - networkStatus = networkStatus, + address = address, integrationId = integrationId, - ).map, Either>> { it.right() } + ).map> { it.right() } .catch { emit(it.left()) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 5eb34854c5..3a81eb1ae7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.* import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -23,27 +23,27 @@ interface StakingRepository { suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - networkStatus: NetworkStatus, + address: String, integrationId: String, refresh: Boolean = false, ) fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - networkStatus: NetworkStatus, + address: String, integrationId: String, - ): Flow> + ): Flow suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, - networks: Set, + addresses: List, integrationId: String, refresh: Boolean = false, ) fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, - networks: Set, + addresses: List, integrationId: String, - ): Flow> + ): Flow } \ 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 79b16936c9..132a42865d 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 @@ -19,6 +19,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal @Suppress("LargeClass") internal object TokenDetailsPreviewData { @@ -121,12 +122,13 @@ internal object TokenDetailsPreviewData { ) val balanceContent = TokenDetailsBalanceBlockState.Content( actionButtons = actionButtons, - fiatBalance = "91,50$", - cryptoBalance = "966,96 XLM", - isStakingEnabled = true, + fiatBalance = BigDecimal.ZERO, + cryptoBalance = BigDecimal.ZERO, balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, selectedBalanceType = BalanceType.ALL, onBalanceSelect = {}, + displayCryptoBalance = "966,96 XLM", + displayFiatBalance = "91,50$", ) val balanceError = TokenDetailsBalanceBlockState.Error( actionButtons = actionButtons, @@ -139,9 +141,12 @@ internal object TokenDetailsPreviewData { private val stakingLoading = StakingBlocksState( stakingAvailable = StakingAvailable.Loading(iconState), stakingBalance = StakingBalance.Content( - cryptoAmount = stringReference("5 SOL"), - fiatAmount = stringReference("456.34 $"), - rewardAmount = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + cryptoValue = stringReference("5 SOL"), + fiatValue = stringReference("456.34 $"), + rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + cryptoAmount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + onStakeClicked = {}, ), ) @@ -320,9 +325,12 @@ internal object TokenDetailsPreviewData { onStakeClicked = {}, ), stakingBalance = StakingBalance.Content( - cryptoAmount = stringReference("5 SOL"), - fiatAmount = stringReference("456.34 $"), - rewardAmount = resourceReference(R.string.staking_details_rewards_to_claim, wrappedList("0.43 $")), + cryptoValue = stringReference("5 SOL"), + fiatValue = stringReference("456.34 $"), + rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + cryptoAmount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + onStakeClicked = {}, ), ), notifications = persistentListOf(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt index 6b5802a796..d1a635dd87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import java.math.BigDecimal internal data class StakingBlocksState( val stakingAvailable: StakingAvailable, @@ -30,8 +31,11 @@ sealed class StakingBalance { data object Empty : StakingBalance() data class Content( - val cryptoAmount: TextReference, - val fiatAmount: TextReference, - val rewardAmount: TextReference, + val cryptoValue: TextReference, + val fiatValue: TextReference, + val rewardValue: TextReference, + val cryptoAmount: BigDecimal?, + val fiatAmount: BigDecimal?, + val onStakeClicked: () -> Unit, ) : StakingBalance() } \ 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 index 21ee681241..825f45f6c5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal internal sealed class TokenDetailsBalanceBlockState { @@ -19,10 +20,11 @@ internal sealed class TokenDetailsBalanceBlockState { override val actionButtons: ImmutableList, override val balanceSegmentedButtonConfig: ImmutableList, override val selectedBalanceType: BalanceType, - val fiatBalance: String, - val cryptoBalance: String, - val isStakingEnabled: Boolean, + val fiatBalance: BigDecimal?, + val cryptoBalance: BigDecimal?, val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit, + val displayCryptoBalance: String, + val displayFiatBalance: String, ) : TokenDetailsBalanceBlockState() data class Error( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt new file mode 100644 index 0000000000..a42e0f24d6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -0,0 +1,75 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenDetailsBalanceSelectStateConverter( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + override fun convert(value: TokenBalanceSegmentedButtonConfig): TokenDetailsState { + return with(currentStateProvider()) { + if (stakingBlocksState.stakingBalance !is StakingBalance.Content) return this + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this + + val stakingCryptoAmount = stakingBlocksState.stakingBalance.cryptoAmount + val stakingFiatAmount = stakingBlocksState.stakingBalance.fiatAmount + + copy( + tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) { + tokenBalanceBlockState.copy( + selectedBalanceType = value.type, + displayFiatBalance = formatFiatAmount( + status = cryptoCurrencyStatus.value, + stakingFiatAmount = stakingFiatAmount, + selectedBalanceType = value.type, + appCurrency = appCurrencyProvider(), + ), + displayCryptoBalance = formatCryptoAmount( + status = cryptoCurrencyStatus, + stakingCryptoAmount = stakingCryptoAmount, + selectedBalanceType = value.type, + ), + ) + } else { + tokenBalanceBlockState + }, + ) + } + } + + private fun formatFiatAmount( + status: CryptoCurrencyStatus.Value, + stakingFiatAmount: BigDecimal?, + selectedBalanceType: BalanceType, + appCurrency: AppCurrency, + ): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount) + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = totalAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatCryptoAmount( + status: CryptoCurrencyStatus, + stakingCryptoAmount: BigDecimal?, + selectedBalanceType: BalanceType, + ): String { + val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) + + return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 87e2d83683..81e8ef5fc0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -6,41 +6,54 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType -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.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val isStakingEnabled: Boolean, private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, -) : Converter, TokenDetailsState> { +) : Converter { private val txHistoryItemConverter by lazy { TokenDetailsTxHistoryTransactionStateConverter(symbol, decimals, clickIntents) } - override fun convert(value: Either): TokenDetailsState { - return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + data class Data( + val maybeCryptoCurrencyStatus: Either, + val maybeYieldBalance: Either?, + ) + + override fun convert(value: Data): TokenDetailsState { + return value.maybeCryptoCurrencyStatus.fold( + ifLeft = { convertError() }, + ifRight = { convert(it, value.maybeYieldBalance?.getOrNull() ?: YieldBalance.Empty) }, + ) } private fun convertError(): TokenDetailsState { val state = currentStateProvider() return state.copy( + isStakingBlockShown = false, tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error( actionButtons = state.tokenBalanceBlockState.actionButtons, balanceSegmentedButtonConfig = state.tokenBalanceBlockState.balanceSegmentedButtonConfig, @@ -51,12 +64,23 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { + private fun convert(status: CryptoCurrencyStatus, yieldBalance: YieldBalance): TokenDetailsState { val state = currentStateProvider() val currencyName = state.marketPriceBlockState.currencySymbol val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList() + val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let { + yieldBalance.balance.items.sumOf { it.amount } + } + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + return state.copy( - tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status), + tokenBalanceBlockState = getBalanceState( + currentState = state.tokenBalanceBlockState, + status = status, + stakingCryptoAmount = stakingCryptoAmount, + stakingFiatAmount = stakingFiatAmount, + ), + stakingBlocksState = getYieldBalance(status, yieldBalance, state), marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName), pendingTxs = pendingTxs, txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) { @@ -70,6 +94,8 @@ internal class TokenDetailsLoadedBalanceConverter( private fun getBalanceState( currentState: TokenDetailsBalanceBlockState, status: CryptoCurrencyStatus, + stakingCryptoAmount: BigDecimal?, + stakingFiatAmount: BigDecimal?, ): TokenDetailsBalanceBlockState { return when (status.value) { is CryptoCurrencyStatus.NoQuote, @@ -78,9 +104,19 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.Custom, -> TokenDetailsBalanceBlockState.Content( actionButtons = currentState.actionButtons, - fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), - cryptoBalance = formatCryptoAmount(status), - isStakingEnabled = isStakingEnabled, + cryptoBalance = status.value.amount, + fiatBalance = status.value.fiatAmount, + displayFiatBalance = formatFiatAmount( + status.value, + stakingFiatAmount, + currentState.selectedBalanceType, + appCurrencyProvider(), + ), + displayCryptoBalance = formatCryptoAmount( + status, + stakingCryptoAmount, + currentState.selectedBalanceType, + ), balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, @@ -88,7 +124,7 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading( actionButtons = currentState.actionButtons, balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, - selectedBalanceType = BalanceType.ALL, + selectedBalanceType = currentState.selectedBalanceType, ) is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.Unreachable, @@ -96,11 +132,58 @@ internal class TokenDetailsLoadedBalanceConverter( -> TokenDetailsBalanceBlockState.Error( actionButtons = currentState.actionButtons, balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, - selectedBalanceType = BalanceType.ALL, + selectedBalanceType = currentState.selectedBalanceType, ) } } + private fun getYieldBalance( + status: CryptoCurrencyStatus, + yieldBalance: YieldBalance, + state: TokenDetailsState, + ): StakingBlocksState { + val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let { + yieldBalance.balance.items.sumOf { it.amount } + } + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + val stakingRewardAmount = (yieldBalance as? YieldBalance.Data)?.let { + yieldBalance.balance.items.sumOf { it.amount.multiply(it.pricePerShare) } + } + val stakingBalance = if (stakingCryptoAmount == null) { + StakingBalance.Empty + } else { + StakingBalance.Content( + cryptoAmount = stakingCryptoAmount, + fiatAmount = stakingFiatAmount, + cryptoValue = stringReference( + BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals), + ), + fiatValue = stringReference( + BigDecimalFormatter.formatFiatAmount( + stakingFiatAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + rewardValue = resourceReference( + R.string.staking_details_rewards_to_claim, + wrappedList( + BigDecimalFormatter.formatFiatAmount( + stakingRewardAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + ), + onStakeClicked = clickIntents::onStakeBannerClick, + ) + } + return state.stakingBlocksState.copy( + stakingAvailable = state.stakingBlocksState.stakingAvailable, + stakingBalance = stakingBalance, + ) + } + private fun getMarketPriceState( status: CryptoCurrencyStatus.Value, currencySymbol: String, @@ -158,19 +241,30 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { + private fun formatFiatAmount( + status: CryptoCurrencyStatus.Value, + stakingFiatAmount: BigDecimal?, + selectedBalanceType: BalanceType, + appCurrency: AppCurrency, + ): String { val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount) return BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, + fiatAmount = totalAmount, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - private fun formatCryptoAmount(status: CryptoCurrencyStatus): String { + private fun formatCryptoAmount( + status: CryptoCurrencyStatus, + stakingCryptoAmount: BigDecimal?, + selectedBalanceType: BalanceType, + ): String { val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) - return BigDecimalFormatter.formatCryptoAmount(amount, status.currency.symbol, status.currency.decimals) + return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index ecb6639506..7e860743ac 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -18,6 +18,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -42,9 +43,9 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, - private val isStakingEnabled: Boolean, symbol: String, decimals: Int, ) { @@ -64,7 +65,6 @@ internal class TokenDetailsStateFactory( TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, - isStakingEnabled = isStakingEnabled, symbol = symbol, decimals = decimals, clickIntents = clickIntents, @@ -107,14 +107,25 @@ internal class TokenDetailsStateFactory( ) } + private val balanceSelectStateConverter by lazy { + TokenDetailsBalanceSelectStateConverter( + currentStateProvider = currentStateProvider, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } + fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert(value = screenArgument) } fun getCurrencyLoadedBalanceState( cryptoCurrencyEither: Either, + yieldBalanceEither: Either?, ): TokenDetailsState { - return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) + return tokenDetailsLoadedBalanceConverter.convert( + TokenDetailsLoadedBalanceConverter.Data(cryptoCurrencyEither, yieldBalanceEither), + ) } fun getManageButtonsState(actions: List): TokenDetailsState { @@ -352,12 +363,7 @@ internal class TokenDetailsStateFactory( fun getStateWithUpdatedBalanceSegmentedButtonConfig( buttonConfig: TokenBalanceSegmentedButtonConfig, ): TokenDetailsState { - return with(currentStateProvider()) { - val updatedState = (tokenBalanceBlockState as? TokenDetailsBalanceBlockState.Content) - ?.copy(selectedBalanceType = buttonConfig.type) - ?: tokenBalanceBlockState - copy(tokenBalanceBlockState = updatedState) - } + return balanceSelectStateConverter.convert(buttonConfig) } private fun TokenDetailsAppBarMenuConfig.updateMenu( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt new file mode 100644 index 0000000000..faa0144943 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils + +import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType +import java.math.BigDecimal + +fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { + return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) { + this.plus(stakingAmount) + } else { + this + } +} \ 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 f076f05240..638e43d98d 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 @@ -108,6 +108,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { modifier = itemModifier, isBalanceHidden = state.isBalanceHidden, state = state.tokenBalanceBlockState, + isStakingAvailable = state.isStakingBlockShown, ) } items( @@ -141,16 +142,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } if (state.isStakingBlockShown) { - item( - key = StakingAvailable::class.java, - contentType = StakingAvailable::class.java, - content = { - TokenStakingBlock( - modifier = itemModifier, - state = state.stakingBlocksState.stakingAvailable, - ) - }, - ) if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) { item( key = StakingBalance::class.java, @@ -163,6 +154,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { }, ) } + item( + key = StakingAvailable::class.java, + contentType = StakingAvailable::class.java, + content = { + TokenStakingBlock( + modifier = itemModifier, + state = state.stakingBlocksState.stakingAvailable, + ) + }, + ) } swapTransactionsItems( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 081fab085e..ac4738e499 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -29,6 +29,7 @@ import kotlinx.collections.immutable.toImmutableList internal fun TokenDetailsBalanceBlock( state: TokenDetailsBalanceBlockState, isBalanceHidden: Boolean, + isStakingAvailable: Boolean, modifier: Modifier = Modifier, ) { Surface( @@ -53,7 +54,7 @@ internal fun TokenDetailsBalanceBlock( .weight(1f) .padding(top = TangemTheme.dimens.spacing12), ) - BalanceButtons(state) + if (isStakingAvailable) BalanceButtons(state) } FiatBalance( state = state, @@ -94,7 +95,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else state.fiatBalance, + text = if (isBalanceHidden) STARS else state.displayFiatBalance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -122,7 +123,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else state.cryptoBalance, + text = if (isBalanceHidden) STARS else state.displayCryptoBalance, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -171,7 +172,7 @@ private fun Preview_TokenDetailsBalanceBlock( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { TangemThemePreview { - TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) + TokenDetailsBalanceBlock(state = state, isBalanceHidden = false, isStakingAvailable = true) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 4a2929a40d..34986f925b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -2,10 +2,14 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components. import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -30,6 +34,11 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = state.onStakeClicked, + ) .padding(TangemTheme.dimens.spacing12), ) { Column( @@ -44,7 +53,7 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi ) Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { Text( - text = state.fiatAmount.resolveReference(), + text = state.fiatValue.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) @@ -54,13 +63,13 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi color = TangemTheme.colors.text.primary1, ) Text( - text = state.cryptoAmount.resolveReference(), + text = state.cryptoValue.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) } Text( - text = state.rewardAmount.resolveReference(), + text = state.rewardValue.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) 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 64dba1df48..848518dbf1 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 @@ -30,6 +30,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.GetStakingYieldBalanceUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* @@ -107,6 +108,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val stakingFeatureToggles: StakingFeatureToggles, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val getYieldUseCase: GetYieldUseCase, + private val getStakingYieldBalanceUseCase: GetStakingYieldBalanceUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -133,6 +135,7 @@ internal class TokenDetailsViewModel @Inject constructor( ?: error("This screen can't open without `CryptoCurrency`") private val userWallet: UserWallet + private var stakingIntegrationId: String? = null lateinit var router: InnerTokenDetailsRouter @@ -149,11 +152,11 @@ internal class TokenDetailsViewModel @Inject constructor( private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, featureToggles = tokenDetailsFeatureToggles, - isStakingEnabled = stakingFeatureToggles.isStakingEnabled, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -217,7 +220,6 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun updateContent() { - subscribeOnCurrencyStatusUpdates() subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) @@ -279,12 +281,36 @@ internal class TokenDetailsViewModel @Inject constructor( ) .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) - maybeCurrencyStatus.onRight { status -> - cryptoCurrencyStatus = status - updateButtons(currencyStatus = status) - updateWarnings(status) - } + maybeCurrencyStatus.fold( + ifLeft = { + internalUiState.value = stateFactory.getCurrencyLoadedBalanceState( + maybeCurrencyStatus, + null, + ) + }, + ifRight = { status -> + cryptoCurrencyStatus = status + + if (stakingIntegrationId != null) { + getStakingYieldBalanceUseCase( + userWalletId = userWalletId, + address = status.value.networkAddress?.defaultAddress?.value.orEmpty(), + integrationId = stakingIntegrationId.orEmpty(), + ).onEach { maybeYieldBalance -> + internalUiState.value = stateFactory.getCurrencyLoadedBalanceState( + maybeCurrencyStatus, + maybeYieldBalance, + ) + }.launchIn(viewModelScope) + } else { + internalUiState.value = + stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus, null) + } + + updateButtons(currencyStatus = status) + updateWarnings(status) + }, + ) currencyStatusAnalyticsSender.send(maybeCurrencyStatus) } .flowOn(dispatchers.main) @@ -385,8 +411,12 @@ internal class TokenDetailsViewModel @Inject constructor( ) internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { + stakingIntegrationId = stakingAvailability.integrationId val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) + subscribeOnCurrencyStatusUpdates() internalUiState.value = stateFactory.getStateWithStaking(stakingInfo) + } else { + subscribeOnCurrencyStatusUpdates() } } } From 0a07579089389ac3a5f6054ce476ff4d03a5e4c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 18:22:59 +0400 Subject: [PATCH 087/223] Updated on 2026-08-14 --- .../feature/qrscanning/viewmodel/QrScanningViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index cfcd24a19b..49b23ff8cd 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -23,7 +23,9 @@ internal class QrScanningViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val source: SourceType = savedStateHandle[AppRoute.QrScanning.SOURCE_KEY] ?: error("Source is mandatory") + private val source: SourceType = savedStateHandle.get(AppRoute.QrScanning.SOURCE_KEY) + ?.let { SourceType.entries[it] } + ?: error("Source is mandatory") private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY] val uiState: StateFlow = stateHolder.uiState From 8224c05d1ae915c5c212e8d2fb2e213427acc095 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 11:30:53 +0300 Subject: [PATCH 088/223] Updated on 2026-08-14 --- .../tangem/tap/domain/model/PendingTransaction.kt | 6 +++--- .../domain/walletconnect/WalletConnectSdkHelper.kt | 3 +-- .../data/transaction/DefaultTransactionRepository.kt | 12 ++++++------ .../utils/TransactionDataToTxHistoryItemConverter.kt | 4 ++-- .../utils/UpdateWalletManagerResultFactory.kt | 4 ++-- .../domain/transaction/TransactionRepository.kt | 2 +- .../send/impl/presentation/state/SendStateFactory.kt | 2 +- .../impl/presentation/viewmodel/SendViewModel.kt | 4 ++-- gradle/dependencies.toml | 2 +- 9 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt index c4a810e12e..8548b11eba 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.Wallet data class PendingTransaction( - val transactionData: TransactionData, + val transactionData: TransactionData.Uncompiled, val type: PendingTransactionType, ) { val address: String? = when (type) { @@ -21,7 +21,7 @@ data class PendingTransaction( enum class PendingTransactionType { Incoming, Outgoing, Unknown } -fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransaction? { +fun TransactionData.Uncompiled.toPendingTransaction(walletAddress: String): PendingTransaction? { if (this.status == TransactionStatus.Confirmed) return null val type: PendingTransactionType = when { @@ -32,7 +32,7 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact return PendingTransaction(this, type) } -fun List.toPendingTransactions(walletAddress: String): List { +fun List.toPendingTransactions(walletAddress: String): List { return this.mapNotNull { it.toPendingTransaction(walletAddress) } } 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 abb7d6ce7e..01fe1e3deb 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 @@ -91,9 +91,8 @@ class WalletConnectSdkHelper { val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" } - val transactionData = TransactionData( + val transactionData = TransactionData.Uncompiled( amount = Amount(value, wallet.blockchain), - // TODO refactoring fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = destinationAddress, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index d36cb4f74b..a69275891f 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -37,7 +37,7 @@ internal class DefaultTransactionRepository( isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, - ): TransactionData? = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -45,7 +45,7 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) - return@withContext walletManager?.createTransactionInternal( + return@withContext walletManager?.createTransactionDataInternal( amount = amount, fee = fee, memo = memo, @@ -78,7 +78,7 @@ internal class DefaultTransactionRepository( val validator = walletManager as? TransactionValidator if (validator != null) { - val transaction = walletManager.createTransactionInternal( + val transactionData = walletManager.createTransactionDataInternal( amount = amount, fee = fee ?: Fee.Common(amount = amount), memo = memo, @@ -89,7 +89,7 @@ internal class DefaultTransactionRepository( hash = hash, ) - validator.validate(transaction = transaction) + validator.validate(transactionData = transactionData) } else { Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") Result.success(Unit) @@ -112,7 +112,7 @@ internal class DefaultTransactionRepository( } @Suppress("LongParameterList") - private fun WalletManager.createTransactionInternal( + private fun WalletManager.createTransactionDataInternal( amount: Amount, fee: Fee, memo: String?, @@ -121,7 +121,7 @@ internal class DefaultTransactionRepository( isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, - ): TransactionData { + ): TransactionData.Uncompiled { // TODO: refactor workaround to use general mechanism in bsdk for build tx for DEX val txAmount = if (isSwap) { createAmountForSwap(amount) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index cd756efd3c..22ae5adecc 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -17,9 +17,9 @@ import java.math.BigDecimal internal class TransactionDataToTxHistoryItemConverter( private val walletAddresses: Set

, private val feePaidCurrency: FeePaidCurrency, -) : Converter { +) : Converter { - override fun convert(value: TransactionData): TxHistoryItem? { + override fun convert(value: TransactionData.Uncompiled): TxHistoryItem? { val hash = value.hash ?: return null val millis = value.date?.timeInMillis ?: return null val amount = getTransactionAmountValue(value.amount, value.fee?.amount) ?: return null 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 index 84b194166b..df5aa57530 100644 --- 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 @@ -90,7 +90,7 @@ internal class UpdateWalletManagerResultFactory { private fun getCurrentTransactions( txHistoryItemConverter: TransactionDataToTxHistoryItemConverter, - recentTransactions: Set, + recentTransactions: Set, ): Set { val unconfirmedTransactions = recentTransactions.filter { it.status == TransactionStatus.Unconfirmed @@ -122,7 +122,7 @@ internal class UpdateWalletManagerResultFactory { private fun createCurrencyTransaction( txHistoryItemConverter: TransactionDataToTxHistoryItemConverter, - data: TransactionData, + data: TransactionData.Uncompiled, ): CryptoCurrencyTransaction? { return when (val type = data.amount.type) { is AmountType.Coin -> { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 52b707e8d1..02240d9dc1 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -19,7 +19,7 @@ interface TransactionRepository { isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, - ): TransactionData? + ): TransactionData.Uncompiled? @Suppress("LongParameterList") suspend fun validateTransaction( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 3a344425d3..3f105fe259 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -150,7 +150,7 @@ internal class SendStateFactory( ) } - fun getTransactionSendState(txData: TransactionData, txUrl: String): SendUiState { + fun getTransactionSendState(txData: TransactionData.Uncompiled, txUrl: String): SendUiState { val state = currentStateProvider() val sendState = state.sendState ?: return state return state.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 6fd5854725..7f0cb473cc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -870,7 +870,7 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun sendTransaction(txData: TransactionData) { + private suspend fun sendTransaction(txData: TransactionData.Uncompiled) { sendTransactionUseCase( txData = txData, userWallet = userWallet, @@ -911,7 +911,7 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun updateTransactionStatus(txData: TransactionData) { + private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) { val txUrl = getExplorerTransactionUrlUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4bbe1a84f0..9836ee2b61 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-684" +tangemBlockchainSdk = "develop-688" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-366" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 488ec01a89abb46111d53334e6463ebf8514a5e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 16:05:04 +0500 Subject: [PATCH 089/223] Updated on 2026-08-14 --- .../presentation/wallet/viewmodels/WalletViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 5a63ee153a..c5aa3e94ac 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 @@ -27,6 +27,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -63,6 +64,7 @@ internal class WalletViewModel @Inject constructor( private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, private val settingsManager: SettingsManager, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -146,7 +148,9 @@ internal class WalletViewModel @Inject constructor( private fun subscribeOnPushNotificationsPermission() { viewModelScope.launch { - if (!shouldAskPermissionUseCase(PUSH_PERMISSION)) return@launch + val isPushToggled = pushNotificationsFeatureToggles.isPushNotificationsEnabled + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (!isPushToggled || !shouldRequestPush) return@launch delay(timeMillis = 1_800) From 7fcf5ddbc0af137a3c224890ff203cfc85c9a000 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 15:34:20 +0300 Subject: [PATCH 090/223] Updated on 2026-08-14 --- .../src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 8b2a9b6a39..8961c88d8a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -343,7 +343,5 @@ private val excludedBlockchains = listOf( Blockchain.NexaTestnet, Blockchain.Mantle, Blockchain.MantleTestnet, - Blockchain.Koinos, - Blockchain.KoinosTestnet, Blockchain.Bittensor, ) \ No newline at end of file From eec5a60676021e306db053e0fcad14b002ca45b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 19:23:49 +0500 Subject: [PATCH 091/223] Updated on 2026-08-14 --- fastlane/Fastfile | 42 ++++++++++++++++++++++++++++++------------ fastlane/README.md | 12 +++++++++++- version.properties | 1 + 3 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 version.properties diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 67b386e4e9..2d59c37cd6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -63,18 +63,20 @@ platform :android do "android.injected.signing.store.password" => options[:store_password], "android.injected.signing.key.alias" => options[:key_alias], "android.injected.signing.key.password" => options[:key_password], - }) - gradle( - task: "assemble", - build_type: "Release", - properties: { - 'versionCode' => options[:versionCode], - 'versionName' => options[:versionName], - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - }) + } + ) + gradle( + task: "assemble", + build_type: "Release", + properties: { + 'versionCode' => options[:versionCode], + 'versionName' => options[:versionName], + "android.injected.signing.store.file" => options[:keystore], + "android.injected.signing.store.password" => options[:store_password], + "android.injected.signing.key.alias" => options[:key_alias], + "android.injected.signing.key.password" => options[:key_password], + } + ) end desc "Submit a new Beta Build to Firebase App Distribution" @@ -85,4 +87,20 @@ platform :android do groups: options[:groups]) end + desc "Publish internal and external builds to Firebase App Distribution" + lane :publishToFirebase do |options| + gradle( + task: "clean assemble", + build_type: "Internal", + properties: { + 'versionCode' => ENV['versionCode'], + 'versionName' => ENV['versionName'], + } + ) + firebase_app_distribution( + app: ENV['app_id_internal'], + apk_path: ENV['apk_path_internal'], + groups: ENV['groups'] + ) + end end diff --git a/fastlane/README.md b/fastlane/README.md index 9df376cd1d..44d6ef904a 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -37,7 +37,7 @@ Build a signed release APK [bundle exec] fastlane android build ``` -Build internal and release APKs +Build external and release APKs ### android beta @@ -47,6 +47,16 @@ Build internal and release APKs Submit a new Beta Build to Firebase App Distribution +### android publishToFirebase + +```sh +[bundle exec] fastlane android publishToFirebase +``` + +Publish internal and external builds to Firebase App Distribution + + + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/version.properties b/version.properties new file mode 100644 index 0000000000..7ef818eaa8 --- /dev/null +++ b/version.properties @@ -0,0 +1 @@ +versionName=5.13 \ No newline at end of file From 20dc8c9d90861ae58bf6d400351db1427142f36a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 20:32:12 +0300 Subject: [PATCH 092/223] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 6 ++ .../transaction/StakingGasEstimateDTO.kt | 2 +- .../data/staking/DefaultStakingRepository.kt | 80 ++++++++++++++++++- .../converters/StakingNetworkTypeConverter.kt | 76 +++++++++++++++++- .../data/staking/converters/TokenConverter.kt | 17 +++- .../data/staking/converters/YieldConverter.kt | 5 +- .../model/transaction/StakingGasEstimate.kt | 2 +- .../InitializeStakingProcessUseCase.kt | 45 +++++++++++ .../staking/repositories/StakingRepository.kt | 17 ++++ .../impl/presentation/state/FeeState.kt | 20 +++++ .../impl/presentation/state/InnerFeeState.kt | 2 - .../presentation/state/StakingStateRouter.kt | 19 +++-- .../impl/presentation/state/StakingUiState.kt | 14 +--- .../ConfirmStakingStatePreviewData.kt | 6 +- .../SetConfirmLoadingStateTransformer.kt | 42 ++++++++++ .../SetConfirmStateDataStateTransformer.kt | 41 +++++++--- .../ui/StakingNavigationButtons.kt | 2 +- .../presentation/ui/block/StakingFeeBlock.kt | 73 +++++++++-------- .../viewmodel/StakingViewModel.kt | 56 ++++++++++++- 19 files changed, 436 insertions(+), 89 deletions(-) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 2a6fb034b9..12aad7794a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -55,4 +55,10 @@ internal object StakingDomainModule { fun provideGetStakingYieldBalanceUseCase(stakingRepository: StakingRepository): GetStakingYieldBalanceUseCase { return GetStakingYieldBalanceUseCase(stakingRepository = stakingRepository) } + + @Provides + @Singleton + fun provideCreateEnterActionUseCase(stakingRepository: StakingRepository): InitializeStakingProcessUseCase { + return InitializeStakingProcessUseCase(stakingRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt index 0e22ee6d9f..93ee18f790 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt @@ -12,5 +12,5 @@ data class StakingGasEstimateDTO( @Json(name = "token") val token: TokenDTO, @Json(name = "gasLimit") - val gasLimit: BigDecimal, + val gasLimit: String?, ) \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 0acc8d6b43..7f1ad03485 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,27 +2,47 @@ package com.tangem.data.staking import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.staking.converters.* +import com.tangem.data.staking.converters.StakingNetworkTypeConverter +import com.tangem.data.staking.converters.TokenConverter +import com.tangem.data.staking.converters.YieldConverter +import com.tangem.data.staking.converters.action.ActionStatusConverter +import com.tangem.data.staking.converters.action.EnterActionResponseConverter +import com.tangem.data.staking.converters.action.StakingActionTypeConverter +import com.tangem.data.staking.converters.transaction.GasEstimateConverter +import com.tangem.data.staking.converters.transaction.StakingTransactionConverter +import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter +import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody +import com.tangem.datasource.api.stakekit.models.request.EnterActionRequestBody +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.converters.* import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.StakingBalanceStore -import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.model.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.toFormattedString import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigDecimal internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, @@ -33,13 +53,31 @@ internal class DefaultStakingRepository( ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() - + private val networkTypeConverter = StakingNetworkTypeConverter() + private val transactionStatusConverter = StakingTransactionStatusConverter() + private val transactionTypeConverter = StakingTransactionTypeConverter() + private val actionStatusConverter = ActionStatusConverter() + private val stakingActionTypeConverter = StakingActionTypeConverter() private val tokenConverter = TokenConverter( stakingNetworkTypeConverter = stakingNetworkTypeConverter, ) private val yieldConverter = YieldConverter( tokenConverter = tokenConverter, ) + private val gasEstimateConverter = GasEstimateConverter( + tokenConverter = tokenConverter, + ) + private val transactionConverter = StakingTransactionConverter( + networkTypeConverter = networkTypeConverter, + transactionStatusConverter = transactionStatusConverter, + transactionTypeConverter = transactionTypeConverter, + gasEstimateConverter = gasEstimateConverter, + ) + private val enterActionResponseConverter = EnterActionResponseConverter( + actionStatusConverter = actionStatusConverter, + stakingActionTypeConverter = stakingActionTypeConverter, + transactionConverter = transactionConverter, + ) private val yieldBalanceConverter = YieldBalanceConverter() @@ -108,6 +146,40 @@ internal class DefaultStakingRepository( } } + override suspend fun createEnterAction( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): EnterAction { + return withContext(dispatchers.io) { + val body = EnterActionRequestBody( + integrationId = integrationId, + addresses = Address(address), + args = EnterActionRequestBody.EnterActionRequestBodyArgs( + amount = amount.toFormattedString(token.decimals), + inputToken = tokenConverter.convertBack(token), + validatorAddress = validatorAddress, + ), + ) + val response = stakeKitApi.createEnterAction(body) + + enterActionResponseConverter.convert(response.getOrThrow()) + } + } + + override suspend fun constructTransaction(transactionId: String): StakingTransaction { + return withContext(dispatchers.io) { + val transactionResponse = stakeKitApi.constructTransaction( + transactionId = transactionId, + body = ConstructTransactionRequestBody(), + ) + + transactionConverter.convert(transactionResponse.getOrThrow()) + } + } + override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, address: String, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt index f7cd02c340..a768417427 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt @@ -2,10 +2,10 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.domain.staking.model.NetworkType -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter @Suppress("CyclomaticComplexMethod", "LongMethod") -class StakingNetworkTypeConverter : Converter { +class StakingNetworkTypeConverter : TwoWayConverter { override fun convert(value: NetworkTypeDTO): NetworkType { return when (value) { @@ -78,4 +78,76 @@ class StakingNetworkTypeConverter : Converter { else -> NetworkType.UNKNOWN } } + + override fun convertBack(value: NetworkType): NetworkTypeDTO { + return when (value) { + NetworkType.AVALANCHE_C -> NetworkTypeDTO.AVALANCHE_C + NetworkType.AVALANCHE_ATOMIC -> NetworkTypeDTO.AVALANCHE_ATOMIC + NetworkType.AVALANCHE_P -> NetworkTypeDTO.AVALANCHE_P + NetworkType.ARBITRUM -> NetworkTypeDTO.ARBITRUM + NetworkType.BINANCE -> NetworkTypeDTO.BINANCE + NetworkType.CELO -> NetworkTypeDTO.CELO + NetworkType.ETHEREUM -> NetworkTypeDTO.ETHEREUM + NetworkType.ETHEREUM_GOERLI -> NetworkTypeDTO.ETHEREUM_GOERLI + NetworkType.ETHEREUM_HOLESKY -> NetworkTypeDTO.ETHEREUM_HOLESKY + NetworkType.FANTOM -> NetworkTypeDTO.FANTOM + NetworkType.HARMONY -> NetworkTypeDTO.HARMONY + NetworkType.OPTIMISM -> NetworkTypeDTO.OPTIMISM + NetworkType.POLYGON -> NetworkTypeDTO.POLYGON + NetworkType.GNOSIS -> NetworkTypeDTO.GNOSIS + NetworkType.MOONRIVER -> NetworkTypeDTO.MOONRIVER + NetworkType.OKC -> NetworkTypeDTO.OKC + NetworkType.ZKSYNC -> NetworkTypeDTO.ZKSYNC + NetworkType.VICTION -> NetworkTypeDTO.VICTION + NetworkType.AGORIC -> NetworkTypeDTO.AGORIC + NetworkType.AKASH -> NetworkTypeDTO.AKASH + NetworkType.AXELAR -> NetworkTypeDTO.AXELAR + NetworkType.BAND_PROTOCOL -> NetworkTypeDTO.BAND_PROTOCOL + NetworkType.BITSONG -> NetworkTypeDTO.BITSONG + NetworkType.CANTO -> NetworkTypeDTO.CANTO + NetworkType.CHIHUAHUA -> NetworkTypeDTO.CHIHUAHUA + NetworkType.COMDEX -> NetworkTypeDTO.COMDEX + NetworkType.COREUM -> NetworkTypeDTO.COREUM + NetworkType.COSMOS -> NetworkTypeDTO.COSMOS + NetworkType.CRESCENT -> NetworkTypeDTO.CRESCENT + NetworkType.CRONOS -> NetworkTypeDTO.CRONOS + NetworkType.CUDOS -> NetworkTypeDTO.CUDOS + NetworkType.DESMOS -> NetworkTypeDTO.DESMOS + NetworkType.DYDX -> NetworkTypeDTO.DYDX + NetworkType.EVMOS -> NetworkTypeDTO.EVMOS + NetworkType.FETCH_AI -> NetworkTypeDTO.FETCH_AI + NetworkType.GRAVITY_BRIDGE -> NetworkTypeDTO.GRAVITY_BRIDGE + NetworkType.INJECTIVE -> NetworkTypeDTO.INJECTIVE + NetworkType.IRISNET -> NetworkTypeDTO.IRISNET + NetworkType.JUNO -> NetworkTypeDTO.JUNO + NetworkType.KAVA -> NetworkTypeDTO.KAVA + NetworkType.KI_NETWORK -> NetworkTypeDTO.KI_NETWORK + NetworkType.MARS_PROTOCOL -> NetworkTypeDTO.MARS_PROTOCOL + NetworkType.NYM -> NetworkTypeDTO.NYM + NetworkType.OKEX_CHAIN -> NetworkTypeDTO.OKEX_CHAIN + NetworkType.ONOMY -> NetworkTypeDTO.ONOMY + NetworkType.OSMOSIS -> NetworkTypeDTO.OSMOSIS + NetworkType.PERSISTENCE -> NetworkTypeDTO.PERSISTENCE + NetworkType.QUICKSILVER -> NetworkTypeDTO.QUICKSILVER + NetworkType.REGEN -> NetworkTypeDTO.REGEN + NetworkType.SECRET -> NetworkTypeDTO.SECRET + NetworkType.SENTINEL -> NetworkTypeDTO.SENTINEL + NetworkType.SOMMELIER -> NetworkTypeDTO.SOMMELIER + NetworkType.STAFI -> NetworkTypeDTO.STAFI + NetworkType.STARGAZE -> NetworkTypeDTO.STARGAZE + NetworkType.STRIDE -> NetworkTypeDTO.STRIDE + NetworkType.TERITORI -> NetworkTypeDTO.TERITORI + NetworkType.TGRADE -> NetworkTypeDTO.TGRADE + NetworkType.UMEE -> NetworkTypeDTO.UMEE + NetworkType.POLKADOT -> NetworkTypeDTO.POLKADOT + NetworkType.KUSAMA -> NetworkTypeDTO.KUSAMA + NetworkType.WESTEND -> NetworkTypeDTO.WESTEND + NetworkType.BINANCEBEACON -> NetworkTypeDTO.BINANCEBEACON + NetworkType.NEAR -> NetworkTypeDTO.NEAR + NetworkType.SOLANA -> NetworkTypeDTO.SOLANA + NetworkType.TEZOS -> NetworkTypeDTO.TEZOS + NetworkType.TRON -> NetworkTypeDTO.TRON + else -> NetworkTypeDTO.UNKNOWN + } + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt index 480d4574bc..2148e18511 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt @@ -2,11 +2,11 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.domain.staking.model.Token -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter class TokenConverter( private val stakingNetworkTypeConverter: StakingNetworkTypeConverter, -) : Converter { +) : TwoWayConverter { override fun convert(value: TokenDTO): Token { return Token( @@ -20,4 +20,17 @@ class TokenConverter( isPoints = value.isPoints, ) } + + override fun convertBack(value: Token): TokenDTO { + return TokenDTO( + name = value.name, + network = stakingNetworkTypeConverter.convertBack(value.network), + symbol = value.symbol, + decimals = value.decimals, + address = value.address, + coinGeckoId = value.coinGeckoId, + logoURI = value.logoURI, + isPoints = value.isPoints, + ) + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 8dd5a5536f..09e1026127 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -20,7 +20,10 @@ class YieldConverter( rewardRate = value.rewardRate, rewardType = convertRewardType(value.rewardType), metadata = convertMetadata(value.metadata), - validators = value.validators.map { convertValidator(it) }, + validators = value.validators + .filter { it.preferred } + .map { convertValidator(it) } + .sortedByDescending { it.apr }, isAvailable = value.isAvailable, ) } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt index 9dacd68d30..292adc35d5 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt @@ -6,5 +6,5 @@ import java.math.BigDecimal data class StakingGasEstimate( val amount: BigDecimal, val token: Token, - val gasLimit: BigDecimal, + val gasLimit: String?, ) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt new file mode 100644 index 0000000000..a12b75f7ae --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt @@ -0,0 +1,45 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.repositories.StakingRepository +import kotlinx.coroutines.delay +import java.math.BigDecimal + +/** + * Use case for creating enter action + */ +class InitializeStakingProcessUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): Either> { + return Either.catch { + val createAction = stakingRepository.createEnterAction( + integrationId = integrationId, + amount = amount, + address = address, + validatorAddress = validatorAddress, + token = token, + ) + + // workaround, sometimes transaction is not created immediately after actions/enter + delay(PATCH_TRANSACTION_REQUEST_DELAY) + + val createdTransaction = createAction.transactions?.get(0) ?: error("No available transaction to patch") + val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) + + createAction to patchedTransaction + } + } + + companion object { + private const val PATCH_TRANSACTION_REQUEST_DELAY = 1000L + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 3a81eb1ae7..c48ed07b5c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -1,5 +1,12 @@ package com.tangem.domain.staking.repositories +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction +import java.math.BigDecimal import com.tangem.domain.staking.model.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -46,4 +53,14 @@ interface StakingRepository { addresses: List, integrationId: String, ): Flow + + suspend fun createEnterAction( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): EnterAction + + suspend fun constructTransaction(transactionId: String): StakingTransaction } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt new file mode 100644 index 0000000000..c58332a870 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt @@ -0,0 +1,20 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.appcurrency.model.AppCurrency +import java.math.BigDecimal + +sealed class FeeState { + + data class Content( + val fee: Fee?, + val rate: BigDecimal?, + val isFeeConvertibleToFiat: Boolean, + val appCurrency: AppCurrency, + val isFeeApproximate: Boolean, + ) : FeeState() + + data object Loading : FeeState() + + data object Error : FeeState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt index 2511ffa889..7a0acf09f9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt @@ -1,9 +1,7 @@ package com.tangem.features.staking.impl.presentation.state -import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.TransactionFee -@Immutable internal sealed class InnerFeeState { data class Content( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 79fff44a09..eef49189ea 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -7,22 +7,20 @@ internal class StakingStateRouter( private val fragmentManager: WeakReference, private val stateController: StakingStateController, ) { - fun clear() { - stateController.update { it.copy(currentStep = getInitialState()) } - } - fun popBackStack() { + private fun closeStaking() { fragmentManager.get()?.popBackStack() + stateController.clear() } fun onBackClick(isSuccess: Boolean = false) { val type = stateController.uiState.value.currentStep when { - isSuccess -> popBackStack() + isSuccess -> closeStaking() else -> when (type) { StakingStep.Amount -> showInitial() StakingStep.Confirm -> showAmount() - else -> popBackStack() + else -> closeStaking() } } } @@ -34,14 +32,17 @@ internal class StakingStateRouter( StakingStep.Amount, -> showConfirm() StakingStep.Confirm -> showSuccess() - StakingStep.Success -> onBackClick() + StakingStep.Success -> closeStaking() } } fun onPrevClick() { when (stateController.uiState.value.currentStep) { StakingStep.Amount -> showInitial() - else -> popBackStack() + StakingStep.Confirm -> showAmount() + StakingStep.Success -> closeStaking() + StakingStep.Validators -> showConfirm() + else -> closeStaking() } } @@ -64,6 +65,4 @@ internal class StakingStateRouter( fun showSuccess() { stateController.update { it.copy(currentStep = StakingStep.Success) } } - - private fun getInitialState() = StakingStep.InitialInfo } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 5d20156911..e0112121e4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -1,16 +1,13 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable -import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList -import java.math.BigDecimal /** * Ui states of the staking screen @@ -79,21 +76,12 @@ internal sealed class StakingStates { override val isPrimaryButtonEnabled: Boolean = false, ) : ConfirmStakingState() } - - data class FeeState( - val innerFeeState: InnerFeeState, - val fee: Fee?, - val rate: BigDecimal?, - val isFeeConvertibleToFiat: Boolean, - val appCurrency: AppCurrency, - val isFeeApproximate: Boolean, - ) } enum class StakingStep { InitialInfo, Amount, - Validators, Confirm, + Validators, Success, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt index 3e81a056fc..161f39ee33 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt @@ -3,10 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType.Coin import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.Yield -import com.tangem.features.staking.impl.presentation.state.InnerFeeState +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.ValidatorState @@ -65,8 +64,7 @@ internal object ConfirmStakingStatePreviewData { val confirmStakingState = StakingStates.ConfirmStakingState.Data( isPrimaryButtonEnabled = true, - feeState = StakingStates.FeeState( - innerFeeState = InnerFeeState.Content(TransactionFee.Single(fee)), + feeState = FeeState.Content( fee = fee, rate = BigDecimal.ONE, appCurrency = AppCurrency.Default, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt new file mode 100644 index 0000000000..76ff266521 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +@Suppress("UnusedPrivateMember") +internal class SetConfirmLoadingStateTransformer( + private val yield: Yield, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val possibleConfirmStakingState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data + val possibleValidatorState = possibleConfirmStakingState?.validatorState as? ValidatorState.Content + val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0] // TODO staking add sorting + + return prevState.copy( + confirmStakingState = StakingStates.ConfirmStakingState.Data( + isPrimaryButtonEnabled = false, + feeState = FeeState.Loading, + validatorState = ValidatorState.Content( + chosenValidator = chosenValidator, + availableValidators = yield.validators, + ), + notifications = persistentListOf( + StakingNotification.Warning.EarnRewards( + currencyName = yield.token.name, + days = yield.metadata.cooldownPeriod.days, + ), + ), + footerText = "", + isSuccess = false, + isStaking = false, // TODO staking move to enum + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt index 1097794d4b..d37ad3406b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt @@ -2,34 +2,49 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer +import com.tangem.blockchain.common.Amount @Suppress("UnusedPrivateMember") internal class SetConfirmStateDataStateTransformer( - private val yield: Yield, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, - private val isFeeApproximateUseCase: IsFeeApproximateUseCase, + private val stakingGasEstimate: StakingGasEstimate, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - // TODO staking fill with real data return prevState.copy( - confirmStakingState = ConfirmStakingStatePreviewData.confirmStakingState, + confirmStakingState = prevState.confirmStakingState.copyWrapped(stakingGasEstimate), ) } - private fun isFeeApproximate(fee: Fee): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - return isFeeApproximateUseCase( - networkId = cryptoCurrencyStatus.currency.network.id, - amountType = fee.amount.type, - ) + private fun StakingStates.ConfirmStakingState.copyWrapped( + gasEstimate: StakingGasEstimate, + ): StakingStates.ConfirmStakingState { + if (this is StakingStates.ConfirmStakingState.Data) { + return copy( + feeState = FeeState.Content( + fee = Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), + rate = cryptoCurrencyStatusProvider().value.fiatRate, + isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + appCurrency = appCurrencyProvider(), + isFeeApproximate = false, + ), + ) + } else { + return this + } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 9ae31fc482..025e254aab 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -116,8 +116,8 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean { return when (uiState.currentStep) { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled - StakingStep.Validators -> uiState.confirmStakingState.isPrimaryButtonEnabled StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled StakingStep.Success -> true + StakingStep.Validators -> true } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 3aa945bd20..4dff5385fb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemThemePreview @@ -26,12 +25,11 @@ import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.utils.getCryptoReference import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.staking.impl.presentation.state.InnerFeeState -import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.FeeState import java.math.BigDecimal @Composable -internal fun StakingFeeBlock(feeState: StakingStates.FeeState) { +internal fun StakingFeeBlock(feeState: FeeState) { Column( modifier = Modifier .fillMaxWidth() @@ -48,37 +46,45 @@ internal fun StakingFeeBlock(feeState: StakingStates.FeeState) { Box( modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), ) { - val feeAmount = feeState.fee?.amount - val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 - SelectorRowItem( - titleRes = title, - iconRes = icon, - preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), - postDot = if (feeState.isFeeConvertibleToFiat) { - getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) - } else { - null - }, - ellipsizeOffset = feeAmount?.currencySymbol?.length, - isSelected = true, - showDivider = false, - showSelectedAppearance = false, - paddingValues = PaddingValues(), - ) - FeeLoading(feeState.innerFeeState) - FeeError(feeState.innerFeeState) + when (feeState) { + is FeeState.Content -> { + val feeAmount = feeState.fee?.amount + val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + SelectorRowItem( + titleRes = title, + iconRes = icon, + preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), + postDot = if (feeState.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) + } else { + null + }, + ellipsizeOffset = feeAmount?.currencySymbol?.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + } + is FeeState.Loading -> { + FeeLoading(feeState) + } + is FeeState.Error -> { + FeeError(feeState) + } + } } } } @Composable -private fun BoxScope.FeeLoading(feeSelectorState: InnerFeeState) { +private fun BoxScope.FeeLoading(feeState: FeeState) { AnimatedContent( - targetState = feeSelectorState, + targetState = feeState, label = "Fee Loading State Change", modifier = Modifier.align(Alignment.CenterEnd), ) { - if (it == InnerFeeState.Loading) { + if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( @@ -91,13 +97,13 @@ private fun BoxScope.FeeLoading(feeSelectorState: InnerFeeState) { } @Composable -private fun BoxScope.FeeError(feeSelectorState: InnerFeeState) { +private fun BoxScope.FeeError(feeState: FeeState) { AnimatedContent( - targetState = feeSelectorState, + targetState = feeState, label = "Fee Error State Change", modifier = Modifier.align(Alignment.CenterEnd), ) { - if (it == InnerFeeState.Error) { + if (it == FeeState.Error) { Text( text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, color = TangemTheme.colors.text.primary1, @@ -111,7 +117,7 @@ private fun BoxScope.FeeError(feeSelectorState: InnerFeeState) { @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: StakingStates.FeeState) { +private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState.Content) { TangemThemePreview { StakingFeeBlock( feeState = value, @@ -119,9 +125,9 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va } } -private class FeeBlockPreviewProvider : PreviewParameterProvider { +private class FeeBlockPreviewProvider : PreviewParameterProvider { - override val values: Sequence + override val values: Sequence get() = sequenceOf( feeState, ) @@ -135,8 +141,7 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider { + initStaking() + } + StakingStep.InitialInfo -> { + // TODO staking + } + StakingStep.Amount -> { + // TODO staking + } + StakingStep.Success -> { + // TODO staking + } + StakingStep.Validators -> { + // TODO staking + } + } + } + + private fun initStaking() { + viewModelScope.launch { + stateController.update( + SetConfirmLoadingStateTransformer( + yield = yield, + ), + ) + + val actionWithTransaction = initializeStakingProcessUseCase( + integrationId = yield.id, + amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided"), + address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address"), + validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), + token = yield.token, + ) + val (enterAction, stakingTransaction) = actionWithTransaction.getOrElse { + error("Can't retrieve action and transaction") + } + + val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info") + + stateController.update( + SetConfirmStateDataStateTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = stakingGasEstimate, + ), + ) + } } override fun onPrevClick() { - stakingStateRouter.onBackClick() + stakingStateRouter.onPrevClick() } override fun onInfoClick(infoType: InfoType) { From bae0f4596f7850128cc9c6cb455272872277d9a0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 21:46:28 +0300 Subject: [PATCH 093/223] Updated on 2026-08-14 --- app/src/debug/res/values/values.xml | 4 ++++ app/src/external/res/values/values.xml | 4 ++++ app/src/internal/res/values/values.xml | 4 ++++ app/src/main/AndroidManifest.xml | 1 + app/src/main/res/values/values.xml | 4 ++++ app/src/mocked/res/values/values.xml | 4 ++++ 6 files changed, 21 insertions(+) create mode 100644 app/src/debug/res/values/values.xml create mode 100644 app/src/external/res/values/values.xml create mode 100644 app/src/internal/res/values/values.xml create mode 100644 app/src/main/res/values/values.xml create mode 100644 app/src/mocked/res/values/values.xml diff --git a/app/src/debug/res/values/values.xml b/app/src/debug/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/debug/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/external/res/values/values.xml b/app/src/external/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/external/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/internal/res/values/values.xml b/app/src/internal/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/internal/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be57eab1df..62d47b8ec8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,7 @@ android:fullBackupContent="false" android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" + android:resizeableActivity="@bool/resizeable_activity" android:label="@string/tangem_app_name" android:largeHeap="@bool/largeHeap" android:networkSecurityConfig="@xml/network_security_config" diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml new file mode 100644 index 0000000000..5bbea5d78d --- /dev/null +++ b/app/src/main/res/values/values.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/app/src/mocked/res/values/values.xml b/app/src/mocked/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/mocked/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file From fb34b46df66d9573160eb7aafe3db70721953f22 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 13:21:26 +0500 Subject: [PATCH 094/223] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 33 ++- .../models/request/YieldBalanceRequestBody.kt | 2 +- .../tangem/utils/extensions/BigDecimalExt.kt | 10 + data/staking/build.gradle.kts | 2 + .../data/staking/DefaultStakingRepository.kt | 257 +++++++++++++----- .../data/staking/di/StakingDataModule.kt | 3 + .../repository/DefaultNetworksRepository.kt | 54 ++++ domain/staking/build.gradle.kts | 2 + .../domain/staking/model/YieldBalance.kt | 16 +- .../domain/staking/model/YieldBalanceList.kt | 6 +- .../staking/FetchStakingTokensUseCase.kt | 4 +- .../FetchStakingYieldBalanceUseCase.kt | 5 +- .../staking/GetStakingYieldBalanceUseCase.kt | 5 +- .../staking/repositories/StakingRepository.kt | 26 +- domain/tokens/build.gradle.kts | 3 + domain/tokens/models/build.gradle.kts | 12 +- .../tokens/model/CryptoCurrencyStatus.kt | 7 + .../tokens/FetchCardTokenListUseCase.kt | 19 +- .../domain/tokens/GetCardTokensListUseCase.kt | 3 + .../tokens/GetCryptoCurrencyActionsUseCase.kt | 1 + .../GetCryptoCurrencyStatusSyncUseCase.kt | 4 + .../GetCryptoCurrencyStatusesSyncUseCase.kt | 3 + .../tokens/GetCurrencyStatusUpdatesUseCase.kt | 3 + .../tokens/GetCurrencyWarningsUseCase.kt | 3 + ...tFeePaidCryptoCurrencyStatusSyncUseCase.kt | 3 + .../tokens/GetNetworkCoinStatusUseCase.kt | 4 + .../GetPrimaryCurrencyStatusUpdatesUseCase.kt | 3 + .../domain/tokens/GetTokenListUseCase.kt | 4 + .../tokens/GetWalletTotalBalanceUseCase.kt | 3 + .../mapper/CurrencyStatusErrorMappers.kt | 1 + .../error/mapper/TokenListErrorMappers.kt | 1 + .../CurrenciesStatusesLceOperations.kt | 44 ++- .../CurrenciesStatusesOperations.kt | 132 +++++++-- .../operations/CurrencyStatusOperations.kt | 9 +- .../TokenListFiatBalanceOperations.kt | 16 +- .../tokens/repository/NetworksRepository.kt | 26 ++ ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 4 + .../domain/tokens/GetTokenListUseCaseTest.kt | 2 + .../domain/tokens/mock/MockTokenLists.kt | 9 +- .../domain/tokens/mock/MockTokensStates.kt | 3 + .../repository/MockNetworksRepository.kt | 32 ++- .../repository/MockStakingRepository.kt | 203 ++++++++++++++ features/swap/domain/build.gradle.kts | 1 + .../swap/domain/di/SwapDomainModule.kt | 5 + features/swap/presentation/build.gradle.kts | 1 + .../feature/swap/di/SwapPresentationModule.kt | 4 +- .../tokendetails/TokenDetailsPreviewData.kt | 1 + .../state/TokenDetailsBalanceBlockState.kt | 1 + .../TokenDetailsLoadedBalanceConverter.kt | 51 ++-- .../state/factory/TokenDetailsStateFactory.kt | 9 +- .../tokendetails/ui/TokenDetailsScreen.kt | 1 - .../ui/components/TokenDetailsBalanceBlock.kt | 7 +- .../viewmodels/TokenDetailsViewModel.kt | 45 +-- .../converter/TokenItemStateConverter.kt | 9 +- 54 files changed, 909 insertions(+), 208 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt create mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.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 ca10e45669..92b7c76800 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 @@ -55,8 +55,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetTokenListUseCase { - return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository) + return GetTokenListUseCase( + currenciesRepository, + quotesRepository, + networksRepository, + stakingRepository, + ) } @Provides @@ -65,8 +71,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetCardTokensListUseCase { - return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository) + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) } @Provides @@ -84,9 +91,16 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyStatusUpdatesUseCase { - return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetCurrencyStatusUpdatesUseCase( + currenciesRepository, + quotesRepository, + networksRepository, + stakingRepository, + dispatchers, + ) } @Provides @@ -101,6 +115,7 @@ internal object TokensDomainModule { currencyChecksRepository: CurrencyChecksRepository, showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, promoRepository: PromoRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( @@ -113,6 +128,7 @@ internal object TokensDomainModule { swapRepository = swapRepository, showSwapPromoTokenUseCase = showSwapPromoTokenUseCase, promoRepository = promoRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -123,12 +139,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetPrimaryCurrencyStatusUpdatesUseCase { return GetPrimaryCurrencyStatusUpdatesUseCase( currenciesRepository, quotesRepository, networksRepository, + stakingRepository, dispatchers, ) } @@ -149,8 +167,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): FetchCardTokenListUseCase { - return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) + return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) } @Provides @@ -212,12 +231,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetNetworkCoinStatusUseCase { return GetNetworkCoinStatusUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -228,12 +249,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetFeePaidCryptoCurrencyStatusSyncUseCase { return GetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -366,11 +389,13 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetWalletTotalBalanceUseCase { return GetWalletTotalBalanceUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index bbfe66a6af..b950b05390 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.Json data class YieldBalanceRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") val args: YieldBalanceRequestArgs, - @Json(name = "integrationId") val integrationId: String? = null, + @Json(name = "integrationId") val integrationId: String, ) { data class YieldBalanceRequestArgs( diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt new file mode 100644 index 0000000000..d09f3f6a6d --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt @@ -0,0 +1,10 @@ +package com.tangem.utils.extensions + +import java.math.BigDecimal + +/** + * Converts `BigDecimal?` to `BigDecimal` + * + * If `BigDecimal?` is `null`, returns `BigDecimal.ZERO` + */ +fun BigDecimal?.orZero(): BigDecimal = this ?: BigDecimal.ZERO \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index e0b3458636..009048e07e 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation(projects.domain.staking) implementation(projects.domain.wallets.models) + /** Feature Api modules */ + implementation(projects.features.staking.api) // region DI implementation(deps.hilt.android) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 7f1ad03485..52f0aef09c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.staking +import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.data.staking.converters.StakingNetworkTypeConverter @@ -29,27 +30,33 @@ import com.tangem.data.staking.converters.* import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.StakingBalanceStore +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.toFormattedString import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.cancellable import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal +@Suppress("LargeClass") internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingYieldsStore: StakingYieldsStore, private val stakingBalanceStore: StakingBalanceStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, + private val stakingFeatureToggle: StakingFeatureToggles, ) : StakingRepository { private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() @@ -83,15 +90,24 @@ internal class DefaultStakingRepository( private val yieldBalanceListConverter = YieldBalanceListConverter() + private val isYieldBalanceFetching = MutableStateFlow( + value = emptyMap(), + ) + override fun isStakingSupported(currencyId: String): Boolean { - return integrationIds.contains(currencyId) + return integrationIdMap.containsKey(currencyId) } - override suspend fun fetchEnabledYields() { + override suspend fun fetchEnabledYields(refresh: Boolean) { withContext(dispatchers.io) { - val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() - - stakingYieldsStore.store(stakingTokensWithYields.data) + cacheRegistry.invokeOnExpire( + key = YIELDS_STORE_KEY, + skipCache = refresh, + block = { + val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + stakingYieldsStore.store(stakingTokensWithYields.data) + }, + ) } } @@ -182,24 +198,32 @@ internal class DefaultStakingRepository( override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: String, - integrationId: String, + address: CryptoCurrencyAddress, refresh: Boolean, ) = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) return@withContext + + val cryptoCurrency = address.cryptoCurrency + val rawCurrencyId = + cryptoCurrency.id.rawCurrencyId ?: error("Staking custom tokens is not available") + + val integrationId = integrationIdMap[rawCurrencyId] ?: return@withContext + cacheRegistry.invokeOnExpire( key = getYieldBalancesKey(userWalletId), skipCache = refresh, block = { + val requestBody = getBalanceRequestData(address.address, integrationId) val result = stakeKitApi.getSingleYieldBalance( - integrationId = integrationId, - body = getBalanceRequestData(address, integrationId), + integrationId = requestBody.integrationId, + body = requestBody, ).getOrThrow() stakingBalanceStore.store( - integrationId, + requestBody.integrationId, YieldBalanceWrapperDTO( balances = result, - integrationId = integrationId, + integrationId = requestBody.integrationId, ), ) }, @@ -208,70 +232,157 @@ internal class DefaultStakingRepository( override fun getSingleYieldBalanceFlow( userWalletId: UserWalletId, - address: String, - integrationId: String, + address: CryptoCurrencyAddress, ): Flow = channelFlow { - launch(dispatchers.io) { - stakingBalanceStore.get(integrationId) - .collectLatest { - send( - yieldBalanceConverter.convert( - YieldBalanceConverter.Data( - balance = it, - integrationId = integrationId, + if (!stakingFeatureToggle.isStakingEnabled) { + send(YieldBalance.Empty) + } else { + launch(dispatchers.io) { + val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId] + ?: error("Could not get integrationId") + stakingBalanceStore.get(integrationId) + .collectLatest { + send( + yieldBalanceConverter.convert( + YieldBalanceConverter.Data( + balance = it, + integrationId = integrationId, + ), ), - ), - ) - } - } + ) + } + } - withContext(dispatchers.io) { - fetchSingleYieldBalance( - userWalletId, - address, - integrationId, - ) + withContext(dispatchers.io) { + fetchSingleYieldBalance( + userWalletId, + address, + ) + } } }.cancellable() + override suspend fun getSingleYieldBalanceSync( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): YieldBalance = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) { + YieldBalance.Empty + } else { + fetchSingleYieldBalance(userWalletId, address) + + val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId] + ?: error("Could not get integrationId") + val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error + yieldBalanceConverter.convert( + YieldBalanceConverter.Data( + balance = result, + integrationId = integrationId, + ), + ) + } + } + override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, addresses: List, - integrationId: String, refresh: Boolean, ) = withContext(dispatchers.io) { - cacheRegistry.invokeOnExpire( - key = getYieldBalancesKey(userWalletId), - skipCache = refresh, - block = { - val result = stakeKitApi.getMultipleYieldBalances( - addresses.map { getBalanceRequestData(it.address, integrationId) }, - ).getOrThrow() + if (!stakingFeatureToggle.isStakingEnabled) return@withContext + try { + isYieldBalanceFetching.update { + it + (userWalletId to true) + } + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val result = stakeKitApi.getMultipleYieldBalances( + addresses + .mapNotNull { networkAddress -> + val cryptoCurrency = networkAddress.cryptoCurrency + val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: error("Currency raw id is null") + val integrationId = integrationIdMap[rawCurrencyId] - stakingBalanceStore.store(result) - }, - ) + if (integrationId != null) { + networkAddress.address to integrationId + } else { + null + } + } + .distinct() + .map { getBalanceRequestData(it.first, it.second) }, + ).getOrThrow() + + stakingBalanceStore.store(result) + }, + ) + } finally { + isYieldBalanceFetching.update { + it - userWalletId + } + } } override fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, addresses: List, - integrationId: String, ): Flow = channelFlow { - launch(dispatchers.io) { - stakingBalanceStore.get() - .collectLatest { send(yieldBalanceListConverter.convert(it)) } - } + if (!stakingFeatureToggle.isStakingEnabled) { + send(YieldBalanceList.Empty) + } else { + launch(dispatchers.io) { + stakingBalanceStore.get() + .collectLatest { send(yieldBalanceListConverter.convert(it)) } + } - withContext(dispatchers.io) { - fetchMultiYieldBalance( - userWalletId, - addresses, - integrationId, - ) + withContext(dispatchers.io) { + fetchMultiYieldBalance( + userWalletId, + addresses, + ) + } } }.cancellable() + override fun getMultiYieldBalanceLce( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow = lceFlow { + if (!stakingFeatureToggle.isStakingEnabled) { + send(YieldBalanceList.Empty) + } else { + launch(dispatchers.io) { + combine( + stakingBalanceStore.get(), + isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, + ) { result, isFetching -> + val balances = yieldBalanceListConverter.convert(result) + send(balances, isStillLoading = isFetching) + }.collect() + } + withContext(dispatchers.io) { + catch( + block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) }, + catch = { raise(it) }, + ) + } + } + } + + override suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + addresses: List, + ): YieldBalanceList = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) { + YieldBalanceList.Empty + } else { + fetchMultiYieldBalance(userWalletId, addresses) + val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error + yieldBalanceListConverter.convert(result) + } + } + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } @@ -297,19 +408,33 @@ internal class DefaultStakingRepository( private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" - companion object { - private val integrationIds = setOf( - Blockchain.Solana.toCoinId(), - Blockchain.Cosmos.toCoinId(), - Blockchain.Polkadot.toCoinId(), - Blockchain.Polygon.toCoinId(), - Blockchain.Avalanche.toCoinId(), - Blockchain.Tron.toCoinId(), - Blockchain.Cronos.toCoinId(), - Blockchain.Binance.toCoinId(), - Blockchain.Kava.toCoinId(), - Blockchain.Near.toCoinId(), - Blockchain.Tezos.toCoinId(), + private companion object { + const val YIELDS_STORE_KEY = "yields" + + const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" + const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" + const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" + const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking" + const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" + const val TRON_INTEGRATION_ID = "tron-trx-native-staking" + const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" + const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking" + const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" + const val NEAR_INTEGRATION_ID = "near-near-native-staking" + const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + + val integrationIdMap = mapOf( + Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID, + Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID, + Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID, + Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID, + Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID, + Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID, + Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID, + Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID, + Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID, + Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID, + Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID, ) } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 16c1bed364..b855fe3386 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,6 +25,7 @@ internal object StakingDataModule { stakingTokenStore: StakingYieldsStore, stakingBalanceStore: StakingBalanceStore, dispatchers: CoroutineDispatcherProvider, + stakingFeatureToggle: StakingFeatureToggles, cacheRegistry: CacheRegistry, ): StakingRepository { return DefaultStakingRepository( @@ -32,6 +34,7 @@ internal object StakingDataModule { stakingBalanceStore = stakingBalanceStore, dispatchers = dispatchers, cacheRegistry = cacheRegistry, + stakingFeatureToggle = stakingFeatureToggle, ) } } \ 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 index c4a12be730..ce8d705b9f 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/DefaultNetworksRepository.kt @@ -124,6 +124,60 @@ internal class DefaultNetworksRepository( } } + override suspend fun getNetworkAddress( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyAddress = withContext(dispatchers.io) { + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = walletManagersFacade.getAddresses(userWalletId, currency.network) + .firstOrNull { it.type == AddressType.Default } + ?.value.orEmpty(), + ) + } + + override fun getNetworkAddressFlow( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow = channelFlow { + launch(dispatchers.io) { + send(getNetworkAddress(userWalletId, currency)) + } + } + + override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List = + withContext(dispatchers.io) { + // Get list of currencies matching [network] + val currencies = getCurrencies(userWalletId) + + // There is no currencies matching given [networks] in [userWalletId] + if (currencies.toList().isEmpty()) return@withContext emptyList() + + currencies.toList().map { currency -> + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = walletManagersFacade.getAddresses(userWalletId, currency.network) + .firstOrNull { it.type == AddressType.Default } + ?.value.orEmpty(), + ) + } + } + + override fun getNetworkAddressesFlow( + userWalletId: UserWalletId, + network: Network, + ): Flow> = channelFlow { + launch(dispatchers.io) { + send(getNetworkAddresses(userWalletId, network)) + } + } + + override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { + launch(dispatchers.io) { + send(getNetworkAddresses(userWalletId)) + } + } + private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 38ec70430c..e92bc23b4c 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -18,4 +18,6 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + + implementation(projects.features.staking.api) } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt index 1b22df4bed..e8d227b3fc 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt @@ -6,9 +6,23 @@ sealed class YieldBalance { data class Data( val balance: YieldBalanceItem, - ) : YieldBalance() + ) : YieldBalance() { + fun getTotalStakingBalance(): BigDecimal { + return balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount * it.pricePerShare } + } + + fun getRewardStakingBalance(): BigDecimal { + return balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount * it.pricePerShare } + } + } data object Empty : YieldBalance() + + data object Error : YieldBalance() } data class YieldBalanceItem( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt index 22db0dfdf1..c75d5c9f10 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt @@ -5,13 +5,15 @@ sealed class YieldBalanceList { data class Data( val balances: List, ) : YieldBalanceList() { - fun getBalance(rawCurrencyId: String?): YieldBalance? { + fun getBalance(rawCurrencyId: String?): YieldBalance { return balances.firstOrNull { yield -> (yield as? YieldBalance.Data)?.balance?.items ?.any { it.rawCurrencyId == rawCurrencyId } == true - } + } ?: YieldBalance.Error } } data object Empty : YieldBalanceList() + + data object Error : YieldBalanceList() } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index b8609b2d1d..47e9960974 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -12,10 +12,10 @@ import com.tangem.domain.staking.repositories.StakingRepository class FetchStakingTokensUseCase( private val stakingRepository: StakingRepository, ) { - suspend operator fun invoke(): Either { + suspend operator fun invoke(isRefresh: Boolean = false): Either { return either { catch( - block = { stakingRepository.fetchEnabledYields() }, + block = { stakingRepository.fetchEnabledYields(isRefresh) }, catch = { StakingTokensError.DataError(it) }, ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index bdbb1521d7..525fd569cb 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( @@ -10,14 +11,12 @@ class FetchStakingYieldBalanceUseCase( suspend operator fun invoke( userWalletId: UserWalletId, - address: String, - integrationId: String, + address: CryptoCurrencyAddress, refresh: Boolean = false, ): Either = Either.catch { stakingRepository.fetchSingleYieldBalance( userWalletId = userWalletId, address = address, - integrationId = integrationId, refresh = refresh, ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index 5e12367173..f2e7c1440a 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.right import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -16,13 +17,11 @@ class GetStakingYieldBalanceUseCase( operator fun invoke( userWalletId: UserWalletId, - address: String, - integrationId: String, + address: CryptoCurrencyAddress, ): EitherFlow { return stakingRepository.getSingleYieldBalanceFlow( userWalletId = userWalletId, address = address, - integrationId = integrationId, ).map> { it.right() } .catch { emit(it.left()) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index c48ed07b5c..854aa92a93 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.action.EnterAction import com.tangem.domain.staking.model.transaction.StakingTransaction import java.math.BigDecimal +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -17,7 +18,7 @@ interface StakingRepository { fun isStakingSupported(currencyId: String): Boolean - suspend fun fetchEnabledYields() + suspend fun fetchEnabledYields(refresh: Boolean) suspend fun getEntryInfo(integrationId: String): StakingEntryInfo @@ -30,30 +31,35 @@ interface StakingRepository { suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, - address: String, - integrationId: String, + address: CryptoCurrencyAddress, refresh: Boolean = false, ) - fun getSingleYieldBalanceFlow( - userWalletId: UserWalletId, - address: String, - integrationId: String, - ): Flow + fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow + + suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, addresses: List, - integrationId: String, refresh: Boolean = false, ) fun getMultiYieldBalanceFlow( userWalletId: UserWalletId, addresses: List, - integrationId: String, ): Flow + fun getMultiYieldBalanceLce( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow + + suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + addresses: List, + ): YieldBalanceList + suspend fun createEnterAction( integrationId: String, amount: BigDecimal, diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 56b3e441ab..989a7e9c5d 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -24,7 +24,10 @@ dependencies { implementation(projects.domain.settings) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + + /** Project - Api */ implementation(projects.features.send.api) + implementation(projects.features.staking.api) /** Project - Other */ implementation(projects.core.utils) diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index a243ada498..c51f155acf 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -10,12 +10,20 @@ android { } dependencies { - implementation(deps.kotlin.serialization) - implementation(projects.domain.txhistory.models) + /** Project - Core */ implementation(projects.core.analytics.models) + + /** Project - Domain */ + implementation(projects.domain.txhistory.models) + implementation(projects.domain.staking.models) + + /** SDK dependencies */ implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } + + /** Other dependencies */ + implementation(deps.kotlin.serialization) implementation(deps.jodatime) implementation(deps.timber) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 6b098d5be3..cda5592ee6 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal @@ -45,6 +46,9 @@ data class CryptoCurrencyStatus( /** The network address */ open val networkAddress: NetworkAddress? = null + + /** Staking yield balance */ + open val yieldBalance: YieldBalance? = null } /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ @@ -107,6 +111,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: BigDecimal, override val fiatRate: BigDecimal, override val priceChange: BigDecimal, + override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, @@ -128,6 +133,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: BigDecimal?, override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, + override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, @@ -143,6 +149,7 @@ data class CryptoCurrencyStatus( */ data class NoQuote( override val amount: BigDecimal, + override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 7448714472..ca9266b2d1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -19,6 +20,7 @@ class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, + private val stakingRepository: StakingRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { @@ -39,8 +41,13 @@ class FetchCardTokenListUseCase( refresh = refresh, ) } - - awaitAll(fetchStatuses, fetchQuotes) + val yieldBalances = async { + fetchYieldBalances( + userWalletId = userWalletId, + refresh = refresh, + ) + } + awaitAll(fetchStatuses, fetchQuotes, yieldBalances) } } } @@ -69,4 +76,12 @@ class FetchCardTokenListUseCase( catch = { /* Ignore error */ }, ) } + + private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) { + val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + catch( + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) }, + catch = { /* Ignore error */ }, + ) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt index 31b6ccc631..698c3be4f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens import arrow.core.left import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -21,6 +22,7 @@ class GetCardTokensListUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -43,6 +45,7 @@ class GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCardCurrenciesStatusesFlow() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 871067c3ec..8cb5dbfc99 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -43,6 +43,7 @@ class GetCryptoCurrencyActionsUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt index 8150cfc734..9c87e00297 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency @@ -16,6 +17,7 @@ class GetCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, + internal val stakingRepository: StakingRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { @@ -29,6 +31,7 @@ class GetCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrencyStatusSync(cryptoCurrencyId, isSingleWalletWithTokens) @@ -41,6 +44,7 @@ class GetCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getPrimaryCurrencyStatusSync() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index cdf58b3080..4c3e6b1b71 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -15,6 +16,7 @@ class GetCryptoCurrencyStatusesSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, + internal val stakingRepository: StakingRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { @@ -24,6 +26,7 @@ class GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatusesSync() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index 92e660af07..dceb8551cf 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency @@ -24,6 +25,7 @@ class GetCurrencyStatusUpdatesUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -60,6 +62,7 @@ class GetCurrencyStatusUpdatesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index eb7296acea..1e64511bf5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.HederaWarnings @@ -26,6 +27,7 @@ class GetCurrencyWarningsUseCase( private val networksRepository: NetworksRepository, private val swapRepository: SwapRepository, private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -43,6 +45,7 @@ class GetCurrencyWarningsUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) return combine( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt index d38fa6a951..473b7c10b9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -16,6 +17,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, + internal val stakingRepository: StakingRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { @@ -30,6 +32,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return either { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 8b03aef79d..bd77e54e3b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -17,6 +18,7 @@ class GetNetworkCoinStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -49,6 +51,7 @@ class GetNetworkCoinStatusUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) val maybeCurrency = if (isSingleWalletWithTokens) { @@ -69,6 +72,7 @@ class GetNetworkCoinStatusUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) val networkFlow = if (isSingleWalletWithTokens) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt index f1216be10f..fdc6dfaf8a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -24,6 +25,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -46,6 +48,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) 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 afb4450d1c..ef0b96e10b 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 com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -26,6 +27,7 @@ class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -35,6 +37,7 @@ class GetTokenListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens -> @@ -55,6 +58,7 @@ class GetTokenListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 36da5cae5c..ce783509f9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance @@ -23,6 +24,7 @@ class GetWalletTotalBalanceUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { suspend operator fun invoke( @@ -72,6 +74,7 @@ class GetWalletTotalBalanceUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatuses( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt index 6b5e9429d8..000588f24b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError { return when (this) { is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.EmptyYieldBalances, is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, 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 27b4e3956f..4f918d6a79 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 @@ -11,6 +11,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, + is CurrenciesStatusesOperations.Error.EmptyYieldBalances, -> TokenListError.EmptyTokens } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 3dd2b0582b..a4265dd621 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -7,6 +7,9 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -20,6 +23,7 @@ internal class CurrenciesStatusesLceOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { fun getCurrenciesStatuses( @@ -65,11 +69,18 @@ internal class CurrenciesStatusesLceOperations( val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val addresses = networksRepository.getNetworkAddresses(userWalletId) combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), - ) { maybeQuotes, maybeNetworksStatuses -> - val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + getYieldBalances(userWalletId, addresses), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> + val statuses = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworksStatuses, + maybeYieldBalances = maybeYieldBalances, + ) emit(statuses) }.collect() } @@ -84,9 +95,10 @@ internal class CurrenciesStatusesLceOperations( lceLoading() } else { createCurrenciesStatuses( - nonEmptyCurrencies, + currencies = nonEmptyCurrencies, maybeNetworkStatuses = null, maybeQuotes = null, + maybeYieldBalances = null, ) } @@ -113,6 +125,7 @@ internal class CurrenciesStatusesLceOperations( currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Lce>?, + maybeYieldBalances: Lce?, ): Lce> = lce { isLoading.set(maybeNetworkStatuses == null) @@ -127,11 +140,20 @@ internal class CurrenciesStatusesLceOperations( null } + val yieldBalances = maybeYieldBalances?.getOrNull() + currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) - createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + yieldBalance = yieldBalance, + ignoreQuote = quotesRetrievingFailed, + ) } } @@ -139,12 +161,14 @@ internal class CurrenciesStatusesLceOperations( currency: CryptoCurrency, quote: Quote?, networkStatus: NetworkStatus?, + yieldBalance: YieldBalance?, ignoreQuote: Boolean, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( currency = currency, quote = quote, networkStatus = networkStatus, + yieldBalance = yieldBalance, ignoreQuote = ignoreQuote, ) @@ -167,6 +191,18 @@ internal class CurrenciesStatusesLceOperations( } } + private fun getYieldBalances( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow { + return stakingRepository.getMultiYieldBalanceLce( + userWalletId = userWalletId, + addresses = addresses, + ).map { maybeBalances -> + maybeBalances.mapError { TokenListError.DataError(it) } + } + } + private fun getIds(currencies: List): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> currency.id to currency.network 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 de1768de08..24bb880d94 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 @@ -3,6 +3,9 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -17,6 +20,7 @@ internal class CurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val userWalletId: UserWalletId, ) { @@ -42,6 +46,7 @@ internal class CurrenciesStatusesOperations( currencies = nonEmptyCurrencies, maybeNetworkStatuses = null, maybeQuotes = null, + maybeYieldBalances = null, ) emit(maybeLoadingCurrenciesStatuses) @@ -51,8 +56,14 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + getYieldBalances(), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> + createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworksStatuses, + maybeYieldBalances = maybeYieldBalances, + ) } emitAll(currenciesFlow) @@ -70,7 +81,9 @@ internal class CurrenciesStatusesOperations( val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() val networkStatuses = networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses) + val yieldBalances = getYieldBalancesSync() + + return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances) }, catch = { raise(Error.DataError(it)) }, ) @@ -98,7 +111,9 @@ internal class CurrenciesStatusesOperations( ).firstOrNull { it.network == currency.network }.right() - return createCurrencyStatus(currency, quotes, networkStatuses) + val yieldBalances = getYieldBalanceSync(currency) + + return createCurrencyStatus(currency, quotes, networkStatuses, yieldBalances) }, catch = { raise(Error.DataError(it)) }, ) @@ -142,8 +157,9 @@ internal class CurrenciesStatusesOperations( }, catch = { Error.DataError(it).left() }, ) + val yieldBalances = getYieldBalanceSync(currency) - return createCurrencyStatus(currency, quotes, networkStatus) + return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) } fun getCardCurrenciesStatusesFlow(): Flow>> { @@ -167,6 +183,7 @@ internal class CurrenciesStatusesOperations( currencies = nonEmptyCurrencies, maybeNetworkStatuses = null, maybeQuotes = null, + maybeYieldBalances = null, ) emit(maybeLoadingCurrenciesStatuses) @@ -176,8 +193,9 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + getYieldBalances(), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> + createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances) } emitAll(currenciesFlow) @@ -255,8 +273,10 @@ internal class CurrenciesStatusesOperations( } } - return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> - createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus) + val yieldBalanceFlow = getYieldBalance(currency) + + return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus, maybeYieldBalance) } } @@ -264,6 +284,7 @@ internal class CurrenciesStatusesOperations( currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, ): Either> = either { var quotesRetrievingFailed = false @@ -281,11 +302,19 @@ internal class CurrenciesStatusesOperations( }, ) + val yieldBalances = maybeYieldBalances?.getOrNull() + currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - - createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = quotesRetrievingFailed, + yieldBalance = yieldBalance, + ) } } @@ -293,6 +322,7 @@ internal class CurrenciesStatusesOperations( currency: CryptoCurrency, maybeQuote: Either, maybeNetworkStatus: Either, + maybeYieldBalance: Either?, ): Either = either { var quoteRetrievingFailed = false @@ -301,8 +331,15 @@ internal class CurrenciesStatusesOperations( quoteRetrievingFailed = true null } + val yieldBalance = maybeYieldBalance?.getOrNull() - createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = quoteRetrievingFailed, + yieldBalance = yieldBalance, + ) } private fun createCurrencyStatus( @@ -310,12 +347,14 @@ internal class CurrenciesStatusesOperations( quote: Quote?, networkStatus: NetworkStatus?, ignoreQuote: Boolean, + yieldBalance: YieldBalance?, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( currency = currency, quote = quote, networkStatus = networkStatus, ignoreQuote = ignoreQuote, + yieldBalance = yieldBalance, ) return currencyStatusOperations.createTokenStatus() @@ -396,6 +435,65 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } + @OptIn(ExperimentalCoroutinesApi::class) + private fun getYieldBalances(): EitherFlow { + return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses -> + stakingRepository.getMultiYieldBalanceFlow( + userWalletId = userWalletId, + addresses = addresses, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } + } + } + + private suspend fun getYieldBalancesSync(): Either { + return catch( + block = { + val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + stakingRepository.getMultiYieldBalanceSync( + userWalletId, + networkAddresses, + ).right() + }, + catch = { + Error.EmptyYieldBalances.left() + }, + ) + } + + private suspend fun getYieldBalanceSync( + cryptoCurrency: CryptoCurrency, + ): Either { + return catch( + block = { + val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency) + stakingRepository.getSingleYieldBalanceSync( + userWalletId, + address, + ).right() + }, + catch = { + Error.EmptyYieldBalances.left() + }, + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow { + return networksRepository.getNetworkAddressFlow( + userWalletId, + cryptoCurrency, + ).flatMapLatest { address -> + stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + address = address, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } + } + } + private fun getIds( currencies: NonEmptyList, ): Pair, NonEmptySet> { @@ -413,14 +511,16 @@ internal class CurrenciesStatusesOperations( sealed class Error { - object EmptyCurrencies : Error() + data object EmptyCurrencies : Error() - object EmptyQuotes : Error() + data object EmptyQuotes : Error() - object EmptyNetworksStatuses : Error() + data object EmptyNetworksStatuses : Error() - object UnableToCreateCurrencyStatus : Error() + data object UnableToCreateCurrencyStatus : Error() data class DataError(val cause: Throwable) : Error() + + data object EmptyYieldBalances : 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 8284d1667e..ee8fa082a7 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,6 @@ package com.tangem.domain.tokens.operations +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.* import java.math.BigDecimal @@ -7,6 +8,7 @@ internal class CurrencyStatusOperations( private val currency: CryptoCurrency, private val quote: Quote?, private val networkStatus: NetworkStatus?, + private val yieldBalance: YieldBalance?, private val ignoreQuote: Boolean, ) { @@ -18,7 +20,7 @@ internal class CurrencyStatusOperations( is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() is NetworkStatus.Unreachable -> createUnreachableStatus(status) is NetworkStatus.NoAccount -> createNoAccountStatus(status) - is NetworkStatus.Verified -> createStatus(status) + is NetworkStatus.Verified -> createStatus(status, yieldBalance) } } @@ -42,7 +44,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, ) - private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Value { + private fun createStatus(status: NetworkStatus.Verified, yieldBalance: YieldBalance?): CryptoCurrencyStatus.Value { val amount = when (val amount = status.amounts[currency.id]) { null -> { return CryptoCurrencyStatus.Loading @@ -62,6 +64,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, + yieldBalance = yieldBalance, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, @@ -71,6 +74,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, + yieldBalance = yieldBalance, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -81,6 +85,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, + yieldBalance = yieldBalance, ) } } 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 2ff01885c9..8f28b74ecc 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,8 +1,10 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.utils.extensions.orZero import java.math.BigDecimal internal class TokenListFiatBalanceOperations( @@ -56,10 +58,13 @@ internal class TokenListFiatBalanceOperations( currentBalance: TotalFiatBalance, ): TotalFiatBalance { return with(currentBalance) { + val stakingBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val fiatStakingBalance = status.fiatRate.times(stakingBalance) + (this as? TotalFiatBalance.Loaded)?.copy( - amount = this.amount + status.fiatAmount, + amount = this.amount + status.fiatAmount + fiatStakingBalance, ) ?: TotalFiatBalance.Loaded( - amount = status.fiatAmount, + amount = status.fiatAmount + fiatStakingBalance, isAllAmountsSummarized = true, ) } @@ -71,12 +76,13 @@ internal class TokenListFiatBalanceOperations( ): TotalFiatBalance { return with(currentBalance) { val isTokenAmountCanBeSummarized = status.fiatAmount != null - + val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero() (this as? TotalFiatBalance.Loaded)?.copy( - amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO), + amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance, isAllAmountsSummarized = isTokenAmountCanBeSummarized, ) ?: TotalFiatBalance.Loaded( - amount = status.fiatAmount ?: BigDecimal.ZERO, + amount = status.fiatAmount.orZero() + fiatYieldBalance, isAllAmountsSummarized = isTokenAmountCanBeSummarized, ) } 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 a2033bb707..a2a99ddea2 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,6 +1,7 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus @@ -62,8 +63,33 @@ interface NetworksRepository { fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean + /** + * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] + */ + fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow> + /** * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List + + /** + * Returns address of [cryptoCurrency] in selected wallet [userWalletId] + */ + suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress + + /** + * Returns address of [cryptoCurrency] in selected wallet [userWalletId] + */ + fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow + + /** + * Returns list of addresses and crypto currency info in selected wallet [userWalletId] + */ + fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> + + /** + * Returns list of addresses and crypto currency info in selected wallet [userWalletId] + */ + suspend fun getNetworkAddresses(userWalletId: UserWalletId): List } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index b5c027ac68..4f52744205 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes @@ -13,6 +14,7 @@ import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockStakingRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals @@ -121,6 +123,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + yieldBalance = YieldBalance.Error, ), ) } @@ -171,5 +174,6 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { ), quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), + stakingRepository = MockStakingRepository(), ) } \ 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 613246a85c..b5cd747c50 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 @@ -16,6 +16,7 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockStakingRepository import com.tangem.domain.wallets.models.UserWalletId import junit.framework.TestCase.assertEquals import kotlinx.coroutines.delay @@ -316,5 +317,6 @@ internal class GetTokenListUseCaseTest { ), quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), + stakingRepository = MockStakingRepository(), ) } \ 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 e08a5348dd..3f15752baf 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 @@ -49,7 +49,8 @@ internal object MockTokenLists { val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( - currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() ?: emptyList(), + currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() + ?: emptyList(), totalFiatBalance = TotalFiatBalance.Loading, ) } @@ -60,7 +61,9 @@ internal object MockTokenLists { groups = groups.map { group -> group.copy( currencies = group.currencies - .map { it.copy(value = CryptoCurrencyStatus.Loading) }, + .map { it.copy(value = CryptoCurrencyStatus.Loading) } + .toNonEmptyListOrNull() + ?: emptyList(), ) }.toNonEmptyListOrNull()!!, ) @@ -109,7 +112,7 @@ internal object MockTokenLists { val sortedGroupedTokenList: TokenList.GroupedByNetwork get() { - val groups = sortedNetworksGroups + val groups = sortedNetworksGroups.toNonEmptyList() return unsortedGroupedTokenList.copy( groups = groups, 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 7490827a6c..e3f88d9b52 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,6 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -151,6 +152,7 @@ internal object MockTokensStates { pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, + yieldBalance = YieldBalance.Error, ), ) } @@ -166,6 +168,7 @@ internal object MockTokensStates { .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, + yieldBalance = YieldBalance.Error, ), ) } 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 78e06e7087..5b5019dfd6 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,13 +3,15 @@ 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.CryptoCurrencyAddress import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.toLce +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -44,10 +46,38 @@ internal class MockNetworksRepository( } override fun isNeedToCreateAccountWithoutReserve(network: Network) = false + + override fun getNetworkAddressesFlow( + userWalletId: UserWalletId, + network: Network, + ): Flow> = channelFlow { + send(emptyList()) + } + + override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { + send(emptyList()) + } + override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network, ): List { return emptyList() } + + override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List { + return emptyList() + } + + override suspend fun getNetworkAddress( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "") + + override fun getNetworkAddressFlow( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow = channelFlow { + send(CryptoCurrencyAddress(currency, "")) + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt new file mode 100644 index 0000000000..e6436b5f62 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -0,0 +1,203 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.action.StakingActionStatus +import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.transaction.StakingTransactionStatus +import com.tangem.domain.staking.model.transaction.StakingTransactionType +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import org.joda.time.DateTime +import java.math.BigDecimal + +class MockStakingRepository : StakingRepository { + override fun isStakingSupported(currencyId: String): Boolean = true + + override suspend fun fetchEnabledYields(refresh: Boolean) { /* no-op */ + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo = StakingEntryInfo( + interestRate = 1.toBigDecimal(), + periodInDays = 2, + tokenSymbol = "SOL", + ) + + override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield( + id = "1", + token = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = "solana", + logoURI = null, + isPoints = null, + ), + tokens = listOf(), + args = Yield.Args( + enter = Yield.Args.Enter( + addresses = Yield.Args.Enter.Addresses( + address = AddressArgument( + required = false, + network = null, + minimum = null, + maximum = null, + ), + additionalAddresses = mapOf(), + ), + args = mapOf(), + ), + exit = null, + ), + status = Yield.Status(enter = false, exit = null), + apy = 1.toBigDecimal(), + rewardRate = 2.3, + rewardType = Yield.RewardType.APR, + metadata = Yield.Metadata( + name = "Yield", + logoUri = "", + description = "", + documentation = null, + gasFeeToken = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + token = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + tokens = listOf(), + type = "auto", + rewardSchedule = "1", + cooldownPeriod = Yield.Metadata.Period(days = 1), + warmupPeriod = Yield.Metadata.Period(days = 1), + rewardClaiming = "1", + defaultValidator = null, + minimumStake = null, + supportsMultipleValidators = false, + revshare = Yield.Metadata.Enabled(enabled = false), + fee = Yield.Metadata.Enabled(enabled = false), + ), + validators = listOf(), + isAvailable = false, + ) + + override suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability = StakingAvailability.Unavailable + + override suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + refresh: Boolean, + ) { + /* no-op */ + } + + override fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): Flow = channelFlow { + send(YieldBalance.Error) + } + + override suspend fun getSingleYieldBalanceSync( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): YieldBalance = YieldBalance.Error + + override suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + addresses: List, + refresh: Boolean, + ) { + /* no-op */ + } + + override fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + addresses: List, + ): Flow = channelFlow { + send( + YieldBalanceList.Data( + balances = listOf(YieldBalance.Error), + ), + ) + } + + override fun getMultiYieldBalanceLce( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow = lceFlow { + send( + YieldBalanceList.Data( + balances = listOf(YieldBalance.Error), + ), + ) + } + + override suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + addresses: List, + ): YieldBalanceList = YieldBalanceList.Data( + balances = listOf(YieldBalance.Error), + ) + + override suspend fun createEnterAction( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): EnterAction = EnterAction( + id = "quis", + integrationId = "persequeris", + status = StakingActionStatus.PROCESSING, + type = StakingActionType.CLAIM_REWARDS, + currentStepIndex = 8701, + amount = BigDecimal.ZERO, + validatorAddress = null, + validatorAddresses = listOf(), + transactions = listOf(), + createdAt = DateTime.now(), + ) + + override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction( + id = "id", + network = NetworkType.SOLANA, + status = StakingTransactionStatus.SIGNED, + type = StakingTransactionType.FREEZE_ENERGY, + hash = null, + signedTransaction = null, + unsignedTransaction = null, + stepIndex = 9368, + error = null, + gasEstimate = null, + stakeId = null, + explorerUrl = null, + ledgerHwAppId = null, + isMessage = false, + ) +} \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 73348d6d0b..c40cbbcc83 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.domain.txhistory.models) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.domain.staking) /** Core modules */ implementation(projects.core.utils) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index daa4bcc969..76f5998502 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -95,12 +96,14 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyStatusesSyncUseCase { return GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -112,11 +115,13 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetCardTokensListUseCase { return GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) } diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index aa70954160..f82d33b863 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.domain.staking) /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt index 441abf4a2e..db8287c844 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt @@ -1,10 +1,10 @@ package com.tangem.feature.swap.di +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.feature.swap.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -23,11 +23,13 @@ class SwapPresentationModule { dispatcherProvider: CoroutineDispatcherProvider, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetCryptoCurrencyStatusSyncUseCase { return GetCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatcherProvider, ) } 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 132a42865d..d685def542 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 @@ -129,6 +129,7 @@ internal object TokenDetailsPreviewData { onBalanceSelect = {}, displayCryptoBalance = "966,96 XLM", displayFiatBalance = "91,50$", + isBalanceSelectorEnabled = false, ) val balanceError = TokenDetailsBalanceBlockState.Error( actionButtons = actionButtons, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index 825f45f6c5..3828253254 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -25,6 +25,7 @@ internal sealed class TokenDetailsBalanceBlockState { val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit, val displayCryptoBalance: String, val displayFiatBalance: String, + val isBalanceSelectorEnabled: Boolean, ) : TokenDetailsBalanceBlockState() data class Error( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 81e8ef5fc0..5a1524feaa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -19,9 +19,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -32,21 +34,17 @@ internal class TokenDetailsLoadedBalanceConverter( private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, -) : Converter { + private val stakingFeatureToggles: StakingFeatureToggles, +) : Converter, TokenDetailsState> { private val txHistoryItemConverter by lazy { TokenDetailsTxHistoryTransactionStateConverter(symbol, decimals, clickIntents) } - data class Data( - val maybeCryptoCurrencyStatus: Either, - val maybeYieldBalance: Either?, - ) - - override fun convert(value: Data): TokenDetailsState { - return value.maybeCryptoCurrencyStatus.fold( + override fun convert(value: Either): TokenDetailsState { + return value.fold( ifLeft = { convertError() }, - ifRight = { convert(it, value.maybeYieldBalance?.getOrNull() ?: YieldBalance.Empty) }, + ifRight = { convert(it) }, ) } @@ -64,23 +62,17 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun convert(status: CryptoCurrencyStatus, yieldBalance: YieldBalance): TokenDetailsState { + private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { val state = currentStateProvider() val currencyName = state.marketPriceBlockState.currencySymbol val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList() - val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let { - yieldBalance.balance.items.sumOf { it.amount } - } - val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } return state.copy( tokenBalanceBlockState = getBalanceState( currentState = state.tokenBalanceBlockState, status = status, - stakingCryptoAmount = stakingCryptoAmount, - stakingFiatAmount = stakingFiatAmount, ), - stakingBlocksState = getYieldBalance(status, yieldBalance, state), + stakingBlocksState = getYieldBalance(status, state), marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName), pendingTxs = pendingTxs, txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) { @@ -94,9 +86,10 @@ internal class TokenDetailsLoadedBalanceConverter( private fun getBalanceState( currentState: TokenDetailsBalanceBlockState, status: CryptoCurrencyStatus, - stakingCryptoAmount: BigDecimal?, - stakingFiatAmount: BigDecimal?, ): TokenDetailsBalanceBlockState { + val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance() + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + val isBalanceSelectorEnabled = stakingFeatureToggles.isStakingEnabled && !stakingCryptoAmount.isNullOrZero() return when (status.value) { is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, @@ -120,6 +113,7 @@ internal class TokenDetailsLoadedBalanceConverter( balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, + isBalanceSelectorEnabled = isBalanceSelectorEnabled, ) is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading( actionButtons = currentState.actionButtons, @@ -137,19 +131,14 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun getYieldBalance( - status: CryptoCurrencyStatus, - yieldBalance: YieldBalance, - state: TokenDetailsState, - ): StakingBlocksState { - val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let { - yieldBalance.balance.items.sumOf { it.amount } - } + private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlocksState { + val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + + val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() + val stakingRewardAmount = yieldBalance?.getRewardStakingBalance() val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } - val stakingRewardAmount = (yieldBalance as? YieldBalance.Data)?.let { - yieldBalance.balance.items.sumOf { it.amount.multiply(it.pricePerShare) } - } - val stakingBalance = if (stakingCryptoAmount == null) { + + val stakingBalance = if (stakingCryptoAmount.isNullOrZero()) { StakingBalance.Empty } else { StakingBalance.Content( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 7e860743ac..952b0d2dc6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -18,7 +18,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -32,6 +31,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -46,6 +46,7 @@ internal class TokenDetailsStateFactory( private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, + stakingFeatureToggles: StakingFeatureToggles, symbol: String, decimals: Int, ) { @@ -68,6 +69,7 @@ internal class TokenDetailsStateFactory( symbol = symbol, decimals = decimals, clickIntents = clickIntents, + stakingFeatureToggles = stakingFeatureToggles, ) } @@ -121,11 +123,8 @@ internal class TokenDetailsStateFactory( fun getCurrencyLoadedBalanceState( cryptoCurrencyEither: Either, - yieldBalanceEither: Either?, ): TokenDetailsState { - return tokenDetailsLoadedBalanceConverter.convert( - TokenDetailsLoadedBalanceConverter.Data(cryptoCurrencyEither, yieldBalanceEither), - ) + return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither) } fun getManageButtonsState(actions: List): TokenDetailsState { 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 638e43d98d..2c71568659 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 @@ -108,7 +108,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { modifier = itemModifier, isBalanceHidden = state.isBalanceHidden, state = state.tokenBalanceBlockState, - isStakingAvailable = state.isStakingBlockShown, ) } items( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index ac4738e499..394e52f37b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -29,7 +29,6 @@ import kotlinx.collections.immutable.toImmutableList internal fun TokenDetailsBalanceBlock( state: TokenDetailsBalanceBlockState, isBalanceHidden: Boolean, - isStakingAvailable: Boolean, modifier: Modifier = Modifier, ) { Surface( @@ -54,7 +53,7 @@ internal fun TokenDetailsBalanceBlock( .weight(1f) .padding(top = TangemTheme.dimens.spacing12), ) - if (isStakingAvailable) BalanceButtons(state) + BalanceButtons(state) } FiatBalance( state = state, @@ -138,7 +137,7 @@ private fun CryptoBalance( @Composable private fun BalanceButtons(state: TokenDetailsBalanceBlockState) { - if (state !is TokenDetailsBalanceBlockState.Content) return + if (state !is TokenDetailsBalanceBlockState.Content || !state.isBalanceSelectorEnabled) return SegmentedButtons( config = state.balanceSegmentedButtonConfig, @@ -172,7 +171,7 @@ private fun Preview_TokenDetailsBalanceBlock( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { TangemThemePreview { - TokenDetailsBalanceBlock(state = state, isBalanceHidden = false, isStakingAvailable = true) + TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) } } 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 848518dbf1..9553f54131 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 @@ -30,7 +30,6 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase -import com.tangem.domain.staking.GetStakingYieldBalanceUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* @@ -108,7 +107,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val stakingFeatureToggles: StakingFeatureToggles, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val getYieldUseCase: GetYieldUseCase, - private val getStakingYieldBalanceUseCase: GetStakingYieldBalanceUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -135,7 +133,6 @@ internal class TokenDetailsViewModel @Inject constructor( ?: error("This screen can't open without `CryptoCurrency`") private val userWallet: UserWallet - private var stakingIntegrationId: String? = null lateinit var router: InnerTokenDetailsRouter @@ -157,6 +154,7 @@ internal class TokenDetailsViewModel @Inject constructor( symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, featureToggles = tokenDetailsFeatureToggles, + stakingFeatureToggles = stakingFeatureToggles, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -220,6 +218,7 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun updateContent() { + subscribeOnCurrencyStatusUpdates() subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) @@ -281,36 +280,12 @@ internal class TokenDetailsViewModel @Inject constructor( ) .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - maybeCurrencyStatus.fold( - ifLeft = { - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState( - maybeCurrencyStatus, - null, - ) - }, - ifRight = { status -> - cryptoCurrencyStatus = status - - if (stakingIntegrationId != null) { - getStakingYieldBalanceUseCase( - userWalletId = userWalletId, - address = status.value.networkAddress?.defaultAddress?.value.orEmpty(), - integrationId = stakingIntegrationId.orEmpty(), - ).onEach { maybeYieldBalance -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState( - maybeCurrencyStatus, - maybeYieldBalance, - ) - }.launchIn(viewModelScope) - } else { - internalUiState.value = - stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus, null) - } - - updateButtons(currencyStatus = status) - updateWarnings(status) - }, - ) + internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) + maybeCurrencyStatus.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(currencyStatus = status) + updateWarnings(status) + } currencyStatusAnalyticsSender.send(maybeCurrencyStatus) } .flowOn(dispatchers.main) @@ -411,12 +386,8 @@ internal class TokenDetailsViewModel @Inject constructor( ) internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { - stakingIntegrationId = stakingAvailability.integrationId val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) - subscribeOnCurrencyStatusUpdates() internalUiState.value = stateFactory.getStateWithStaking(stakingInfo) - } else { - subscribeOnCurrencyStatusUpdates() } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index f41d4d4ee7..73198feee8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -5,11 +5,13 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero import java.math.BigDecimal internal class TokenItemStateConverter( @@ -61,13 +63,16 @@ internal class TokenItemStateConverter( } private fun CryptoCurrencyStatus.getFormattedAmount(): String { - val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val amount = value.amount?.plus(yieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val fiatYieldBalance = value.fiatRate?.times(yieldBalance).orZero() + val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) From 2d8d9453bd95cc2940d98f13b0901f4ccedf03b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 14:07:11 +0300 Subject: [PATCH 095/223] Updated on 2026-08-14 --- .../currency/tokenicon/ContentIcon.kt | 2 +- .../marketprice/MarketPriceBlock.kt | 52 +- .../marketprice/PriceChangeInPercent.kt | 96 ++++ .../tangem/core/ui/windowsize/WindowSize.kt | 16 +- .../src/main/res/drawable/ic_plus_mini_28.xml | 10 + features/markets/api/.gitignore | 1 + features/markets/api/build.gradle.kts | 18 + .../features/markets/BottomSheetState.kt | 6 + .../features/markets/MarketsListComponent.kt | 12 + features/markets/impl/.gitignore | 1 + features/markets/impl/build.gradle.kts | 24 + .../features/markets/ui/MarketsListItem.kt | 448 ++++++++++++++++++ .../markets/ui/models/MarketsListItemModel.kt | 33 ++ .../MarketChartListItemPreviewDataProvider.kt | 94 ++++ features/wallet/impl/build.gradle.kts | 1 + .../extension/BaseExtensionConfigurations.kt | 1 + settings.gradle.kts | 3 + 17 files changed, 763 insertions(+), 55 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt create mode 100644 core/ui/src/main/res/drawable/ic_plus_mini_28.xml create mode 100644 features/markets/api/.gitignore create mode 100644 features/markets/api/build.gradle.kts create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt create mode 100644 features/markets/impl/.gitignore create mode 100644 features/markets/impl/build.gradle.kts create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt index cdf96d7b53..1814d2593d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt @@ -58,7 +58,7 @@ internal fun ContentIcon( } @Composable -private fun CoinIcon( +fun CoinIcon( url: String?, @DrawableRes fallbackResId: Int, alpha: Float, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 606416093a..8ef37d1f7c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -4,14 +4,12 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -20,8 +18,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter 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.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter /** @@ -116,7 +114,10 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { ) { Price(price = marketPriceBlockState.price, modifier = priceModifier) - PriceChangeInPercent(marketPriceBlockState.priceChangeConfig) + PriceChangeInPercent( + valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent, + type = marketPriceBlockState.priceChangeConfig.type, + ) } } else { Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) @@ -136,47 +137,6 @@ private fun Price(price: String, modifier: Modifier = Modifier) { ) } -@Composable -private fun PriceChangeInPercent(config: PriceChangeState.Content) { - AnimatedContent( - targetState = config.type, - contentAlignment = Alignment.CenterStart, - label = "Update price change", - ) { type -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2), - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size8), - painter = painterResource( - id = when (type) { - PriceChangeType.UP -> R.drawable.ic_arrow_up_8 - PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 - PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 - }, - ), - tint = when (type) { - PriceChangeType.UP -> TangemTheme.colors.icon.accent - PriceChangeType.DOWN -> TangemTheme.colors.icon.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive - }, - contentDescription = null, - ) - - Text( - text = config.valueInPercent, - color = when (type) { - PriceChangeType.UP -> TangemTheme.colors.text.accent - PriceChangeType.DOWN -> TangemTheme.colors.text.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled - }, - style = TangemTheme.typography.body2, - ) - } - } -} - @Composable private fun LoadingContent() { Row( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt new file mode 100644 index 0000000000..d2c55ed576 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt @@ -0,0 +1,96 @@ +package com.tangem.core.ui.components.marketprice + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun PriceChangeInPercent( + valueInPercent: String, + type: PriceChangeType, + modifier: Modifier = Modifier, + textStyle: TextStyle = TangemTheme.typography.body2, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2), + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .align(Alignment.CenterVertically), + imageVector = ImageVector.vectorResource( + id = when (type) { + PriceChangeType.UP -> R.drawable.ic_arrow_up_8 + PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 + }, + ), + tint = when (type) { + PriceChangeType.UP -> TangemTheme.colors.icon.accent + PriceChangeType.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive + }, + contentDescription = null, + ) + + Text( + text = valueInPercent, + color = when (type) { + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled + }, + style = textStyle, + overflow = TextOverflow.Visible, + maxLines = 1, + ) + } +} + +//region Preview + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Column { + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.NEUTRAL, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.UP, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.DOWN, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.DOWN, + textStyle = TangemTheme.typography.caption2, + ) + } + } +} + +//endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt b/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt index a8928b86f8..ed47c74673 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt @@ -46,18 +46,18 @@ data class WindowSize( fun heightAtMost(value: Dp): Boolean = this.height <= value private fun getWidthWindowSizeType(windowDp: Dp): WindowSizeType = when { - windowDp < 300.dp -> WindowSizeType.ExtraSmall - windowDp < 380.dp -> WindowSizeType.Small - windowDp < 540.dp -> WindowSizeType.Normal - windowDp < 700.dp -> WindowSizeType.Large + windowDp <= 320.dp -> WindowSizeType.ExtraSmall + windowDp <= 360.dp -> WindowSizeType.Small + windowDp <= 540.dp -> WindowSizeType.Normal + windowDp <= 700.dp -> WindowSizeType.Large else -> WindowSizeType.ExtraLarge } private fun getHeightWindowSizeType(heightDp: Dp): WindowSizeType = when { - heightDp < 480.dp -> WindowSizeType.ExtraSmall - heightDp < 640.dp -> WindowSizeType.Small - heightDp < 860.dp -> WindowSizeType.Normal - heightDp < 1100.dp -> WindowSizeType.Large + heightDp <= 480.dp -> WindowSizeType.ExtraSmall + heightDp <= 640.dp -> WindowSizeType.Small + heightDp <= 860.dp -> WindowSizeType.Normal + heightDp <= 1100.dp -> WindowSizeType.Large else -> WindowSizeType.ExtraLarge } } diff --git a/core/ui/src/main/res/drawable/ic_plus_mini_28.xml b/core/ui/src/main/res/drawable/ic_plus_mini_28.xml new file mode 100644 index 0000000000..e6e483128c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_plus_mini_28.xml @@ -0,0 +1,10 @@ + + + diff --git a/features/markets/api/.gitignore b/features/markets/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/markets/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/markets/api/build.gradle.kts b/features/markets/api/build.gradle.kts new file mode 100644 index 0000000000..0248feb86d --- /dev/null +++ b/features/markets/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.markets.api" +} + +dependencies { + implementation(deps.compose.foundation) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt new file mode 100644 index 0000000000..0084b73528 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt @@ -0,0 +1,6 @@ +package com.tangem.features.markets + +enum class BottomSheetState { + EXPANDED, + COLLAPSED, +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt new file mode 100644 index 0000000000..e987e0d66e --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets + +import androidx.compose.runtime.Stable + +@Stable +interface MarketsListComponent { + + // TODO + + // @Composable + // fun BottomSheetContent(state: State, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier) +} \ No newline at end of file diff --git a/features/markets/impl/.gitignore b/features/markets/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/markets/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts new file mode 100644 index 0000000000..80288a27be --- /dev/null +++ b/features/markets/impl/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.markets.impl" +} + +dependencies { + implementation(deps.compose.coil) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + + implementation(deps.kotlin.immutable.collections) + + implementation(projects.core.ui) + implementation(projects.common.ui) + implementation(projects.common.uiCharts) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt new file mode 100644 index 0000000000..67dd8c574e --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt @@ -0,0 +1,448 @@ +package com.tangem.features.markets.ui + +import android.content.res.Configuration +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.* +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.* +import androidx.compose.foundation.interaction.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.IntOffset +import com.tangem.common.ui.charts.MarketChartMini +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.currency.tokenicon.CoinIcon +import com.tangem.core.ui.components.marketprice.PriceChangeInPercent +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.LocalWindowSize +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.windowsize.WindowSizeType +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.ui.models.MarketsListItemModel +import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlin.math.roundToInt +import kotlin.random.Random + +internal enum class DragValue { Start, End } + +const val SWIPE_THRESHOLD_PERCENT = 0.8f +const val SWIPE_VELOCITY_THRESHOLD = 20f + +@Suppress("LongMethod") +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MarketsListItem( + model: MarketsListItemModel, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onSwipeToAction: () -> Unit = {}, +) { + val actionWidth = TangemTheme.dimens.size68 + val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() } + + val hapticManager = LocalHapticManager.current + + val anchors = DraggableAnchors { + DragValue.Start at 0f + DragValue.End at -actionWidthPx + } + val state = remember { + AnchoredDraggableState( + initialValue = DragValue.Start, + anchors = anchors, + positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) }, + velocityThreshold = { SWIPE_VELOCITY_THRESHOLD }, + animationSpec = tween(easing = FastOutSlowInEasing), + confirmValueChange = { it == DragValue.Start }, + ) + } + val dragInteractionSource = remember { MutableInteractionSource() } + val clickInteractionSource = remember { MutableInteractionSource() } + val isInDraggedState by dragInteractionSource.collectIsDraggedAsState() + + LaunchedEffect(Unit) { + var actionPerformed = false + var releasePerformed = true + launch { + snapshotFlow { state.offset } + .collect { + val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT + if (it < border && actionPerformed.not()) { + hapticManager.vibrateLong() + actionPerformed = true + releasePerformed = false + } + + if (it > border) { + if (releasePerformed.not()) { + hapticManager.vibrateShort() + releasePerformed = true + } + actionPerformed = false + } + } + } + launch { + snapshotFlow { isInDraggedState } + .collect { + if (it.not() && actionPerformed) { + releasePerformed = true + onSwipeToAction() + } + } + } + } + + Box( + modifier = Modifier + .height(intrinsicSize = IntrinsicSize.Min) + .fillMaxWidth(), + ) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .fillMaxHeight() + .offset { + IntOffset( + x = actionWidthPx.roundToInt() + + state + .requireOffset() + .toInt(), + y = 0, + ) + } + .width(actionWidth) + .background(TangemTheme.colors.control.checked), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.size(TangemTheme.dimens.size28), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2), + contentDescription = null, + ) + } + + Box( + modifier = modifier + .align(Alignment.CenterStart) + .clip(RectangleShape) + .offset { + IntOffset( + x = state + .requireOffset() + .toInt(), + y = 0, + ) + } + .anchoredDraggable( + state = state, + orientation = Orientation.Horizontal, + interactionSource = dragInteractionSource, + ) + .clickable( + enabled = true, + interactionSource = clickInteractionSource, + indication = rememberRipple(), + onClick = onClick, + ), + ) { + MarketsListItemContent(model = model) + } + } +} + +@Composable +private fun MarketsListItemContent(model: MarketsListItemModel, modifier: Modifier = Modifier) { + val windowSize = LocalWindowSize.current + + Row( + modifier = modifier.padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing15, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = model.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + + SpacerW12() + + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TokenTitle( + modifier = Modifier.weight(1f, fill = false), + name = model.name, + currencySymbol = model.currencySymbol, + ) + SpacerW8() + TokenPriceText( + modifier = Modifier.alignByBaseline(), + price = model.price.text, + priceChangeType = model.price.changeType, + ) + } + + SpacerH(height = TangemTheme.dimens.spacing2) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + TokenSubtitle( + modifier = Modifier + .weight(1f, fill = false) + .alignByBaseline(), + ratingPosition = model.ratingPosition, + marketCap = model.marketCap, + ) + PriceChangeInPercent( + modifier = Modifier.alignByBaseline(), + textStyle = TangemTheme.typography.caption2, + type = model.trendType, + valueInPercent = model.trendPercentText, + ) + } + } + + if (windowSize.widthAtLeast(WindowSizeType.Small)) { + Spacer(Modifier.width(TangemTheme.dimens.spacing10)) + + Chart( + chartType = model.chartType, + chartRawData = model.chardData, + ) + } + } +} + +@Composable +private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) { + Row(modifier = modifier) { + Text( + modifier = Modifier + .weight(1f, fill = false) + .alignByBaseline(), + text = name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerW4() + Text( + modifier = Modifier.alignByBaseline(), + text = currencySymbol, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + maxLines = 1, + overflow = TextOverflow.Visible, + ) + } +} + +@Composable +private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + TokenRatingPlace(ratingPosition = ratingPosition) + SpacerW4() + TokenMarketCapText(text = marketCap ?: "") + } +} + +@Composable +private fun RowScope.TokenRatingPlace(ratingPosition: String?) { + Box( + modifier = Modifier + .alignByBaseline() + .heightIn(min = TangemTheme.dimens.size16) + .background( + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersSmall2, + ) + .padding(horizontal = TangemTheme.dimens.size5), // TODO check + ) { + Text( + text = ratingPosition ?: "-", + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Composable +private fun RowScope.TokenMarketCapText(text: String) { + Text( + modifier = Modifier.alignByBaseline(), + text = text, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { + val growColor = TangemTheme.colors.text.accent + val fallColor = TangemTheme.colors.text.warning + val generalColor = TangemTheme.colors.text.primary1 + + val color = remember { Animatable(generalColor) } + + LaunchedEffect(price, growColor, fallColor, generalColor) { + snapshotFlow { price } + .drop(1) + .distinctUntilChanged() + .collectLatest { + val nextColor = when (priceChangeType) { + PriceChangeType.NEUTRAL, + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + null -> generalColor + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + } + + Text( + modifier = modifier, + text = price, + color = color.value, + maxLines = 1, + style = TangemTheme.typography.body2, + overflow = TextOverflow.Visible, + ) +} + +@Composable +private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) { + val chartWidth = TangemTheme.dimens.size56 + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .size(height = TangemTheme.dimens.size32, width = chartWidth), + ) { + if (chartRawData != null) { + MarketChartMini( + rawData = chartRawData, + type = chartType, + ) + } else { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size15) // TODO check + .align(Alignment.Center), + ) + } + } +} + +// region preview +@Preview(showBackground = true, widthDp = 360, name = "normal") +@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 320, name = "small width") +@Composable +private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemModel) { + TangemThemePreview { + var state1 by remember { mutableStateOf(state) } + var state2 by remember { mutableStateOf(state) } + var prices by remember { + mutableStateOf( + listOf( + 100 to PriceChangeType.NEUTRAL, + 200 to PriceChangeType.NEUTRAL, + ), + ) + } + + Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + MarketsListItem( + modifier = Modifier, + model = state1, + ) + MarketsListItem( + modifier = Modifier, + model = state2, + ) + Row { + Button( + onClick = { + state1 = state1.copy( + trendType = PriceChangeType.entries.random(), + ) + state2 = state2.copy( + trendType = PriceChangeType.entries.random(), + ) + }, + ) { Text(text = "trend") } + + Button( + onClick = { + prices = prices.map { + if (Random.nextBoolean()) { + it.first.inc() to PriceChangeType.UP + } else { + it.first.dec() to PriceChangeType.DOWN + } + } + state1 = state1.copy( + price = MarketsListItemModel.Price( + text = "0.${prices[0].first}023 $", + changeType = prices[0].second, + ), + ) + state2 = state2.copy( + price = MarketsListItemModel.Price( + text = "0.${prices[1].first}023 $", + changeType = prices[1].second, + ), + ) + }, + ) { Text(text = "price") } + } + } + } +} + +// endregion preview \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt new file mode 100644 index 0000000000..4c8d21f229 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.markets.ui.models + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.marketprice.PriceChangeType + +@Immutable +data class MarketsListItemModel( + val id: String, + val name: String, + val currencySymbol: String, + val iconUrl: String?, + val ratingPosition: String?, + val marketCap: String?, + val price: Price, + val trendPercentText: String, + val trendType: PriceChangeType, + val chardData: MarketChartRawData?, +) { + val chartType: MarketChartLook.Type = when (trendType) { + PriceChangeType.UP, + PriceChangeType.NEUTRAL, + -> MarketChartLook.Type.Growing + PriceChangeType.DOWN -> MarketChartLook.Type.Falling + } + + @Immutable + data class Price( + val text: String, + val changeType: PriceChangeType = PriceChangeType.NEUTRAL, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt new file mode 100644 index 0000000000..6121a35222 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -0,0 +1,94 @@ +@file:Suppress("MagicNumber") +package com.tangem.features.markets.ui.preview + +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.features.markets.ui.models.MarketsListItemModel + +internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = "", + ratingPosition = "10", + marketCap = "$6.233 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = "10", + marketCap = "$6.233 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.NEUTRAL, + chardData = null, + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = "10", + marketCap = "$6.23348172384781234 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.DOWN, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = "10", + marketCap = null, + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = null, + marketCap = "$6.233 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = null, + marketCap = null, + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index f9f232db02..bbfc624efd 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -90,4 +90,5 @@ dependencies { implementation(projects.features.manageTokens.api) implementation(projects.features.details.api) implementation(projects.features.pushNotifications.api) + implementation(projects.features.markets.api) } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 67de36d6df..1612e170ed 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -25,6 +25,7 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 14ba5ff49e..3637dd3cc9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -186,6 +186,9 @@ include(":features:push-notifications:impl") include(":features:wallet-settings:api") include(":features:wallet-settings:impl") + +include(":features:markets:api") +include(":features:markets:impl") // endregion Feature modules // region Domain modules From f916d7394b4e478f878e3d70847346b28b5c41ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 16:39:42 +0500 Subject: [PATCH 096/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 5 + core/res/src/main/res/values/strings.xml | 13 ++ .../core/ui/components/BoxWithGradient.kt | 41 ---- .../components/containers}/FooterContainer.kt | 4 +- .../ui/components/inputrow/InputRowDefault.kt | 11 +- .../components/inputrow/InputRowImageBase.kt | 7 +- .../inputrow/InputRowImageChevron.kt | 39 +++- .../components/inputrow/InputRowImageInfo.kt | 114 ++++++++++ .../core/ui/extensions/MarkdownExtension.kt | 4 +- .../data/staking/DefaultStakingRepository.kt | 21 +- .../converters/YieldBalanceConverter.kt | 1 + .../domain/staking/model/YieldBalance.kt | 1 + .../impl/presentation/ui/fee/SendCustomFee.kt | 2 +- .../ui/recipient/SendRecipientContent.kt | 2 +- .../ui/recipient/TextFieldWithPaste.kt | 2 +- .../state/InnerYieldBalanceState.kt | 30 +++ .../impl/presentation/state/StakingUiState.kt | 1 + .../previewdata/InitialStakingStatePreview.kt | 59 +++++ .../SetInitialDataStateTransformer.kt | 110 ++++++++- .../ui/StakingInitialInfoContent.kt | 214 ++++++++++++------ .../ui/StakingNavigationButtons.kt | 31 ++- 21 files changed, 559 insertions(+), 153 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt rename {features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common => core/ui/src/main/java/com/tangem/core/ui/components/containers}/FooterContainer.kt (92%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index a0be0aea2a..053247f58d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -111,6 +111,7 @@ Далее Нет Нет адреса + Сейчас OK Основная карта Парольная фраза @@ -139,6 +140,7 @@ Поддержка Обмен условия участия + Сегодня Ошибка транзакции Транзакции Перевод @@ -309,6 +311,8 @@ Голосовать Выберите кошелек Кошелёк не поддерживает более одной сети + Ссылки + Метрики Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -681,6 +685,7 @@ Сессии WalletConnect Подключение к dApps WalletConnect + Подключение может занять несколько секунд Рыночная цена %s за 24 часа Сеть %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 497372f66e..e7228a19a8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -112,6 +112,7 @@ Next No No address + Now OK Primary Card Passphrase @@ -142,6 +143,7 @@ Support Swap terms and conditions + Today Transaction failed Transactions Transfer @@ -319,6 +321,11 @@ Market Select wallet Sort By + Insights + Links + Metrics + Price performance + Security score You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -552,6 +559,8 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name + Active + To unstake your assets, click here. APR APY The annual percentage return you can earn from participating in staking. @@ -577,6 +586,9 @@ Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards Rewards + Stake more + Unstacked + Check unstaked to claim your assets Validator Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet @@ -702,6 +714,7 @@ WalletConnect Sessions Connect to dApps WalletConnect + Connecting may take a few seconds %s Market Price last 24h %s network diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt deleted file mode 100644 index b51d1c0f08..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemTheme - -@Composable -fun BoxWithGradient( - modifier: Modifier = Modifier, - gradient: Brush = BottomGradient, - content: @Composable BoxScope.() -> Unit, -) { - val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current) - - Box(modifier = modifier.fillMaxSize()) { - content() - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(TangemTheme.dimens.size164 + bottomInsetsPx.dp) - .background(gradient), - ) - } -} - -private val BottomGradient: Brush = Brush.verticalGradient( - colors = listOf( - TangemColorPalette.Black.copy(alpha = 0f), - TangemColorPalette.Black.copy(alpha = 0.75f), - TangemColorPalette.Black.copy(alpha = 0.95f), - TangemColorPalette.Black, - ), -) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt similarity index 92% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt index 26b442e62a..5296313d28 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.common +package com.tangem.core.ui.components.containers import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column @@ -18,7 +18,7 @@ import com.tangem.core.ui.res.TangemTheme * @param content field content */ @Composable -internal fun FooterContainer( +fun FooterContainer( modifier: Modifier = Modifier, footer: String? = null, footerTopPadding: Dp = TangemTheme.dimens.spacing8, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 4222d238c0..3b1e657dda 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -4,12 +4,16 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -20,8 +24,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -64,7 +68,7 @@ fun InputRowDefault( ) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) Text( @@ -80,6 +84,7 @@ fun InputRowDefault( contentDescription = null, tint = iconTint, modifier = Modifier + .align(CenterVertically) .padding( top = TangemTheme.dimens.spacing10, bottom = TangemTheme.dimens.spacing10, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 51f7b29ac6..871dccfd94 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -24,6 +24,7 @@ internal fun InputRowImageBase( ) { Row( verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, ) { InputRowAsyncImage( @@ -32,11 +33,7 @@ internal fun InputRowImageBase( .size(TangemTheme.dimens.spacing36) .padding(vertical = TangemTheme.dimens.size1), ) - Column( - modifier = Modifier - .weight(1f) - .padding(start = TangemTheme.dimens.spacing12), - ) { + Column { Text( text = subtitle.resolveReference(), style = TangemTheme.typography.subtitle2, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt index 9c8fcff521..83fd0ccbf0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt @@ -1,13 +1,20 @@ package com.tangem.core.ui.components.inputrow -import androidx.compose.material.Icon +import android.content.res.Configuration +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Input row component with selector @@ -37,10 +44,36 @@ fun InputRowImageChevron( subtitleColor = subtitleColor, captionColor = captionColor, ) { + SpacerWMax() Icon( painter = painterResource(id = R.drawable.ic_chevron_right_24), tint = TangemTheme.colors.icon.informative, contentDescription = null, ) } -} \ No newline at end of file +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageChevron_Preview() { + TangemThemePreview { + InputRowImageChevron( + subtitle = stringReference("Binance"), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(SpanStyle(TangemTheme.colors.text.accent)) { + stringReference("3,54%") + } + }, + ), + ), + imageUrl = "", + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt new file mode 100644 index 0000000000..81a7e01218 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -0,0 +1,114 @@ +package com.tangem.core.ui.components.inputrow + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.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.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2841-1589&t=u6pOF6lsdpvWLELb-4) + * + * @param title title text + * @param subtitle subtitle text + * @param caption caption text + * @param infoTitle info title text + * @param infoSubtitle info subtitle text + * @param modifier modifier + * @param imageUrl icon to load + * @param subtitleColor subtitle text color + * @param captionColor caption text color + */ +@Suppress("LongParameterList") +@Composable +fun InputRowImageInfo( + title: TextReference?, + subtitle: TextReference, + caption: TextReference, + infoTitle: TextReference, + infoSubtitle: TextReference, + imageUrl: String, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, +) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + modifier = modifier + .padding(TangemTheme.dimens.spacing12), + ) { + if (title != null) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + InputRowImageBase( + subtitle = subtitle, + caption = caption, + imageUrl = imageUrl, + subtitleColor = subtitleColor, + captionColor = captionColor, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + horizontalAlignment = Alignment.End, + modifier = Modifier.weight(1f), + ) { + EllipsisText( + text = infoTitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + EllipsisText( + text = infoSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageInfo_Preview() { + TangemThemePreview { + InputRowImageInfo( + title = stringReference("Active"), + subtitle = stringReference("Binance"), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(SpanStyle(TangemTheme.colors.text.accent)) { + stringReference("3,54%") + } + }, + ), + ), + infoTitle = stringReference("5431231231231231231231232 USD"), + infoSubtitle = stringReference("5 SOL"), + imageUrl = "", + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt index 408e9130ee..b8b11c058f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -53,4 +53,6 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): } } return this -} \ No newline at end of file +} + +fun AnnotatedString.Builder.appendSpace() = append(" ") \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 52f0aef09c..edb32a111b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -3,9 +3,8 @@ package com.tangem.data.staking import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId -import com.tangem.data.staking.converters.StakingNetworkTypeConverter -import com.tangem.data.staking.converters.TokenConverter -import com.tangem.data.staking.converters.YieldConverter +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.converters.* import com.tangem.data.staking.converters.action.ActionStatusConverter import com.tangem.data.staking.converters.action.EnterActionResponseConverter import com.tangem.data.staking.converters.action.StakingActionTypeConverter @@ -18,21 +17,15 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.Address import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody import com.tangem.datasource.api.stakekit.models.request.EnterActionRequestBody -import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Token -import com.tangem.domain.staking.model.Yield -import com.tangem.domain.staking.model.action.EnterAction -import com.tangem.domain.staking.model.transaction.StakingTransaction -import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.staking.converters.* import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.StakingBalanceStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -40,10 +33,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.toFormattedString -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.cancellable -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index 0ab969880e..d0abdee20e 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -21,6 +21,7 @@ internal class YieldBalanceConverter : Converter, + ) : InnerYieldBalanceState() + + data object Empty : InnerYieldBalanceState() +} + +data class BalanceGroupedState( + val items: List, + val type: BalanceType, + val footer: TextReference?, + val title: TextReference, +) + +data class BalanceState( + val validator: Yield.Validator, + val cryptoAmount: TextReference, + val fiatAmount: TextReference, + val rawCurrencyId: String?, +) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index e0112121e4..b1758a8d42 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -53,6 +53,7 @@ internal sealed class StakingStates { val warmupPeriod: String, val rewardSchedule: String, val onInfoClick: (InfoType) -> Unit, + val yieldBalance: InnerYieldBalanceState, ) : InitialInfoState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt new file mode 100644 index 0000000000..f49e7535d0 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -0,0 +1,59 @@ +package com.tangem.features.staking.impl.presentation.state.previewdata + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates + +internal object InitialStakingStatePreview { + val defaultState = StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = true, + available = "15 SOL", + onStake = "0 SOL", + aprRange = stringReference("2.54-5.12%"), + unbondingPeriod = "3d", + minimumRequirement = "12 SOL", + rewardClaiming = "Auto", + warmupPeriod = "Days", + rewardSchedule = "Block", + onInfoClick = {}, + yieldBalance = InnerYieldBalanceState.Empty, + ) + + val stateWithYield = defaultState.copy( + yieldBalance = InnerYieldBalanceState.Data( + rewardsFiat = "100 $", + rewardsCrypto = "100 SOL", + isRewardsToClaim = false, + balance = listOf( + BalanceGroupedState( + type = BalanceType.STAKED, + title = stringReference("Staked"), + footer = null, + items = listOf( + BalanceState( + cryptoAmount = stringReference("100 SOL"), + fiatAmount = stringReference("100 $"), + rawCurrencyId = null, + validator = Yield.Validator( + address = "address", + status = "status", + name = "Binance", + image = null, + website = null, + apr = "5".toBigDecimal(), + commission = null, + stakedBalance = null, + votingPower = null, + preferred = false, + ), + ), + ), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 6282c12227..0438982aff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -9,17 +9,19 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceItem import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingStep -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -55,15 +57,20 @@ internal class SetInitialDataStateTransformer( private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = true, available = BigDecimalFormatter.formatCryptoAmount( - cryptoCurrencyStatus.value.amount, + cryptoAmount = cryptoCurrencyStatus.value.amount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + onStake = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), cryptoCurrency = cryptoCurrencyStatus.currency.symbol, decimals = cryptoCurrencyStatus.currency.decimals, ), - onStake = "0 SOL", // TODO staking add after adding /balances request aprRange = getAprRange(), unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(), minimumRequirement = yield.metadata.minimumStake.toString(), @@ -71,6 +78,7 @@ internal class SetInitialDataStateTransformer( warmupPeriod = yield.metadata.warmupPeriod.days.toString(), rewardSchedule = yield.metadata.rewardSchedule, onInfoClick = clickIntents::onInfoClick, + yieldBalance = getStakedBalances(cryptoCurrencyStatus), ) } @@ -108,6 +116,96 @@ internal class SetInitialDataStateTransformer( return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr)) } + private fun getStakedBalances(cryptoCurrencyStatus: CryptoCurrencyStatus): InnerYieldBalanceState { + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + return if (yieldBalance is YieldBalance.Data) { + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() + val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) + val groupedBalances = getGroupedBalance(yieldBalance.balance, cryptoCurrencyStatus, appCurrency) + + InnerYieldBalanceState.Data( + rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoRewardsValue, + cryptoCurrency = cryptoCurrency, + ), + rewardsFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRewardsValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), + balance = groupedBalances, + ) + } else { + InnerYieldBalanceState.Empty + } + } + + private fun getGroupedBalance( + balance: YieldBalanceItem, + cryptoCurrencyStatus: CryptoCurrencyStatus, + appCurrency: AppCurrency, + ) = balance.items + .sortedByDescending { it.type } + .groupBy { it.type } + .mapNotNull { item -> + val (title, footer) = getValidatorBalanceInto(item.key) + + val balances = item.value.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + balance.validatorAddress?.contains(it.address, ignoreCase = true) == true + } + val cryptoAmount = balance.amount * balance.pricePerShare + val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) + + validator?.let { + BalanceState( + validator = validator, + cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = balance.amount, + cryptoCurrency = cryptoCurrencyStatus.currency, + ), + ), + fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + rawCurrencyId = balance.rawCurrencyId, + ) + } + } + + title?.let { + BalanceGroupedState( + items = balances, + type = item.key, + footer = footer, + title = it, + ) + } + } + + private fun getValidatorBalanceInto(type: BalanceType) = when (type) { + BalanceType.PREPARING, + BalanceType.STAKED, + -> resourceReference(R.string.staking_active) to resourceReference(R.string.staking_active_footer) + BalanceType.UNSTAKING, + BalanceType.UNLOCKING, + BalanceType.UNSTAKED, + -> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer) + BalanceType.AVAILABLE, + BalanceType.LOCKED, + BalanceType.UNKNOWN, + BalanceType.REWARDS, + -> null to null + } + companion object { private val EQUALITY_THRESHOLD = BigDecimal(1E-10) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 558dc71308..81730a1913 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -11,36 +13,64 @@ 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.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.rows.CornersToRound import com.tangem.core.ui.components.rows.RoundableCornersRow -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.utils.Strings.DOT +import com.tangem.utils.extensions.orZero @Composable internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { if (state !is StakingStates.InitialInfoState.Data) return Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = Modifier // Do not put fillMaxSize() in here .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing16) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) .verticalScroll(rememberScrollState()), ) { - MetricsBlock(state) - Spacer(modifier = Modifier.height(8.dp)) + AnimatedVisibility(state.yieldBalance == InnerYieldBalanceState.Empty) { + MetricsBlock(state) + } StakingDetailsRows(state) - Spacer(modifier = Modifier.height(8.dp)) + AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { + if (it is InnerYieldBalanceState.Data) { + StakingRewardBlock( + rewardCrypto = it.rewardsCrypto, + rewardFiat = it.rewardsFiat, + isRewardsToClaim = it.isRewardsToClaim, + ) + } + } + AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { + if (it is InnerYieldBalanceState.Data) { + ActiveStakingBlock(it.balance) + } + } } } @@ -52,7 +82,7 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { color = TangemTheme.colors.background.primary, shape = RoundedCornerShape(TangemTheme.dimens.radius12), ) - .padding(16.dp) + .padding(TangemTheme.dimens.spacing12) .fillMaxWidth(), ) { Text( @@ -60,7 +90,7 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(TangemTheme.dimens.size8)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -108,53 +138,117 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { @Composable internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_available), - endText = state.available, - cornersToRound = CornersToRound.TOP_2, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_on_stake), - endText = state.onStake, - cornersToRound = CornersToRound.ZERO, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_apy), - endText = state.aprRange.resolveReference(), - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.APY) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_unbonding_period), - endText = state.unbondingPeriod, - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_minimum_requirement), - endText = state.minimumRequirement, - cornersToRound = CornersToRound.ZERO, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_reward_claiming), - endText = state.rewardClaiming, - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_warmup_period), - endText = state.warmupPeriod, - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_reward_schedule), - endText = state.rewardSchedule, - cornersToRound = CornersToRound.BOTTOM_2, - iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) }, + Column { + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_available), + endText = state.available, + cornersToRound = CornersToRound.TOP_2, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_on_stake), + endText = state.onStake, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_apy), + endText = state.aprRange.resolveReference(), + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.APY) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_unbonding_period), + endText = state.unbondingPeriod, + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_minimum_requirement), + endText = state.minimumRequirement, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_claiming), + endText = state.rewardClaiming, + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_warmup_period), + endText = state.warmupPeriod, + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_schedule), + endText = state.rewardSchedule, + cornersToRound = CornersToRound.BOTTOM_2, + iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) }, + ) + } +} + +@Composable +private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewardsToClaim: Boolean) { + val (text, textColor) = if (isRewardsToClaim) { + annotatedReference( + buildAnnotatedString { + append("+") + appendSpace() + append(rewardFiat) + appendSpace() + append(DOT) + appendSpace() + append(rewardCrypto) + }, + ) to TangemTheme.colors.text.primary1 + } else { + resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary + } + InputRowDefault( + title = resourceReference(R.string.staking_rewards), + text = text, + iconRes = R.drawable.ic_chevron_right_24, + textColor = textColor, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), ) } +@Composable +private fun ActiveStakingBlock(groupes: List) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + groupes.forEach { group -> + FooterContainer( + footer = group.footer?.resolveReference(), + modifier = Modifier, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + group.items.forEachIndexed { index, balance -> + InputRowImageInfo( + title = group.title.takeIf { index == 0 }, + subtitle = stringReference(balance.validator.name), + caption = stringReference( + BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), + ), + infoTitle = balance.fiatAmount, + infoSubtitle = balance.cryptoAmount, + imageUrl = balance.validator.image.orEmpty(), + ) + } + } + } + } + } +} + @Composable private fun InitialInfoContentRow( startText: String, @@ -191,18 +285,8 @@ private fun StakingInitialInfoContent_Preview( private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - StakingStates.InitialInfoState.Data( - isPrimaryButtonEnabled = true, - available = "15 SOL", - onStake = "0 SOL", - aprRange = stringReference("2.54-5.12%"), - unbondingPeriod = "3d", - minimumRequirement = "12 SOL", - rewardClaiming = "Auto", - warmupPeriod = "Days", - rewardSchedule = "Block", - onInfoClick = {}, - ), + InitialStakingStatePreview.defaultState, + InitialStakingStatePreview.stateWithYield, ) } // endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 025e254aab..e9c29e3cb1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState @@ -49,12 +50,9 @@ internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifie @Composable private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) { val hapticFeedback = LocalHapticFeedback.current - val confirmState = uiState.confirmStakingState - val isSuccess = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isSuccess ?: false - val isStaking = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isStaking ?: false - val isButtonsVisible = uiState.currentStep != StakingStep.Success - val isStakingState = uiState.currentStep == StakingStep.Success && !isSuccess && !isStaking + val isButtonsVisible = isPrevButtonVisible(uiState.currentStep) + val isStakingState = uiState.currentStep == StakingStep.Confirm val (buttonTextId, buttonClick) = getButtonData( currentState = uiState, @@ -103,15 +101,32 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier private fun getButtonData(currentState: StakingUiState): Pair Unit> { return when (currentState.currentStep) { - StakingStep.InitialInfo, + StakingStep.InitialInfo -> { + val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data + if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { + R.string.staking_stake_more to currentState.clickIntents::onNextClick + } else { + R.string.common_next to currentState.clickIntents::onNextClick + } + } StakingStep.Amount, - StakingStep.Confirm, - StakingStep.Success, -> R.string.common_next to currentState.clickIntents::onNextClick + StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick + StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick } } +private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { + StakingStep.InitialInfo, + StakingStep.Confirm, + StakingStep.Success, + -> false + StakingStep.Amount, + StakingStep.Validators, + -> true +} + private fun isButtonEnabled(uiState: StakingUiState): Boolean { return when (uiState.currentStep) { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled From cf6ee347b85788b52f6104fdea941c79f365ef19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 15:48:07 +0100 Subject: [PATCH 097/223] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 8 ++ .../tap/proxy/TransactionManagerImpl.kt | 134 ------------------ .../com/tangem/tap/proxy/di/ProxyModule.kt | 10 +- data/transaction/build.gradle.kts | 2 + .../DefaultTransactionRepository.kt | 40 ++++++ domain/transaction/build.gradle.kts | 1 + .../transaction/models/TransactionType.kt | 5 + .../transaction/TransactionRepository.kt | 10 ++ .../CreateTransactionDataExtrasUseCase.kt | 30 ++++ features/swap/domain/build.gradle.kts | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 100 +++++++------ .../swap/domain/di/SwapDomainModule.kt | 7 +- gradle/dependencies.toml | 2 +- .../tangem/lib/crypto/TransactionManager.kt | 8 -- 14 files changed, 161 insertions(+), 197 deletions(-) create mode 100644 domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 849209811f..8ce98f292d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -64,6 +64,14 @@ internal object TransactionDomainModule { return CreateTransactionUseCase(transactionRepository) } + @Provides + @Singleton + fun provideCreateTransactionExtrasUseCase( + transactionRepository: TransactionRepository, + ): CreateTransactionDataExtrasUseCase { + return CreateTransactionDataExtrasUseCase(transactionRepository) + } + @Provides @Singleton fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase { 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 587924aa8a..c199b92e7b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -1,28 +1,17 @@ package com.tangem.tap.proxy -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.Message -import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.externallinkprovider.TxExploreState -import com.tangem.blockchain.network.ResultChecker import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.hexToBytes -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* -import com.tangem.lib.crypto.models.transactions.SendTxResult -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TangemSigner import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -30,59 +19,10 @@ import java.math.RoundingMode @Suppress("LargeClass") class TransactionManagerImpl( - private val appStateHolder: AppStateHolder, - private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, private val userWalletsListManager: UserWalletsListManager, ) : TransactionManager { - override suspend fun sendApproveTransaction( - txData: ApproveTxData, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult { - val blockchain = requireNotNull(Blockchain.fromNetworkId(txData.networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - walletManager.update() - val amount = Amount(value = BigDecimal.ZERO, blockchain = blockchain) - return sendTransactionInternal( - walletManager = walletManager, - amount = amount, - blockchain = blockchain, - feeAmount = txData.feeAmount, - gasLimit = txData.gasLimit, - destinationAddress = txData.destinationAddress, - dataToSign = txData.dataToSign, - ) - } - - @Suppress("LongParameterList") - private suspend fun sendTransactionInternal( - walletManager: WalletManager, - amount: Amount, - blockchain: Blockchain, - feeAmount: BigDecimal, - gasLimit: Int, - destinationAddress: String, - dataToSign: String, - ): SendTxResult { - val txData = walletManager.createTransaction( - amount = amount, - fee = Fee.Common(Amount(value = feeAmount, blockchain = blockchain)), - destination = destinationAddress, - ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) - - val signer = transactionSigner(walletManager) - - val sendResult = try { - (walletManager as? TransactionSender)?.send(txData, signer) ?: error("Cannot cast to TransactionSender") - } catch (ex: Exception) { - FirebaseCrashlytics.getInstance().recordException(ex) - return SendTxResult.UnknownError(ex) - } - return handleSendResult(result = sendResult) - } - override fun getExplorerTransactionLink(networkId: String, txAddress: String): String { val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found") return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) { @@ -301,60 +241,6 @@ class TransactionManagerImpl( } } - private fun handleSendResult(result: Result): SendTxResult { - when (result) { - is Result.Success -> { - return SendTxResult.Success - } - is Result.Failure -> { - if (ResultChecker.isNetworkError(result)) return SendTxResult.NetworkError(result.error) - val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError() - when (error) { - is BlockchainSdkError.WrappedTangemError -> { - val errorByCode = mapErrorByCode(error) - if (errorByCode != null) { - return errorByCode - } - val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTxResult.UnknownError() - if (tangemSdkError is TangemSdkError.UserCancelled) return SendTxResult.UserCancelledError - return SendTxResult.TangemSdkError(tangemSdkError.code, tangemSdkError.cause) - } - else -> { - return SendTxResult.TangemSdkError(error.code, error.cause) - } - } - } - } - } - - private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTxResult? { - return when (error.code) { - USER_CANCELLED_ERROR_CODE -> { - return SendTxResult.UserCancelledError - } - else -> { - null - } - } - } - - private fun transactionSigner(walletManager: WalletManager): TransactionSigner { - val actualCard = requireNotNull(appStateHolder.getActualCard()) { "no card found" } - return TangemSigner( - card = actualCard, - tangemSdk = cardSdkConfigRepository.sdk, - initialMessage = Message(), - ) { signResponse -> - appStateHolder.mainStore?.dispatch( - GlobalAction.UpdateWalletSignedHashes( - walletSignedHashes = signResponse.totalSignedHashes, - walletPublicKey = walletManager.wallet.publicKey.seedKey, - remainingSignatures = signResponse.remainingSignatures, - ), - ) - } - } - private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { val selectedUserWallet = requireNotNull( userWalletsListManager.selectedUserWalletSync, @@ -368,24 +254,6 @@ class TransactionManagerImpl( return requireNotNull(walletManager) { "no wallet manager found" } } - private fun createExtras( - walletManager: WalletManager, - gasLimit: Int, - transactionHash: String, - ): TransactionExtras? { - return when (walletManager) { - is EthereumWalletManager -> { - return EthereumTransactionExtras( - data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(), - gasLimit = gasLimit.toBigInteger(), - ) - } - else -> { - null - } - } - } - /** * Create proxy fees * @@ -483,8 +351,6 @@ class TransactionManagerImpl( } companion object { - private const val HEX_PREFIX = "0x" - private const val USER_CANCELLED_ERROR_CODE = 50002 private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50% private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50% } 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 37fc1060b4..5c9c990ba5 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 @@ -1,16 +1,16 @@ package com.tangem.tap.proxy.di -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.tap.proxy.* +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.TransactionManagerImpl +import com.tangem.tap.proxy.UserWalletManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.flow.* import javax.inject.Singleton @Module @@ -38,14 +38,10 @@ internal object ProxyModule { @Provides @Singleton fun provideTransactionManager( - appStateHolder: AppStateHolder, - cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, ): TransactionManager { return TransactionManagerImpl( - appStateHolder = appStateHolder, - cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, ) diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 615758dcf6..41baa4a4ba 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { /** Tangem SDKs */ implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) /** Core */ implementation(projects.core.datasource) @@ -25,6 +26,7 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) /** DI */ implementation(deps.hilt.android) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index a69275891f..caffe5aca9 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -4,22 +4,29 @@ import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras +import com.tangem.blockchain.blockchains.tron.TronTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.common.extensions.hexToBytes import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal +import java.math.BigInteger +import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, @@ -111,6 +118,39 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + override fun createTransactionDataExtras( + data: String, + network: Network, + transactionType: TransactionType, + nonce: BigInteger?, + gasLimit: BigInteger?, + ): TransactionExtras { + val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + ?: error("Blockchain not found") + return when { + blockchain.isEvm() -> { + EthereumTransactionExtras( + data = data.hexToBytes(), + gasLimit = gasLimit, + nonce = nonce, + ) + } + blockchain == Blockchain.Tron -> { + TronTransactionExtras( + data = data.hexToBytes(), + txType = convertToSdkTransactionType(transactionType), + ) + } + else -> error("Data extras not supported for $blockchain") + } + } + + private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType { + return when (transactionType) { + TransactionType.APPROVE -> SdkTransactionType.APPROVE + } + } + @Suppress("LongParameterList") private fun WalletManager.createTransactionDataInternal( amount: Amount, diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index d4014c6f47..b54aeca082 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.demo) implementation(projects.domain.card) } \ No newline at end of file diff --git a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt new file mode 100644 index 0000000000..fdd9a7ec17 --- /dev/null +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.transaction.models + +enum class TransactionType { + APPROVE, +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 02240d9dc1..2228ec19e5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -4,7 +4,9 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigInteger interface TransactionRepository { @@ -40,4 +42,12 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): com.tangem.blockchain.extensions.Result + + fun createTransactionDataExtras( + data: String, + network: Network, + transactionType: TransactionType, + nonce: BigInteger?, + gasLimit: BigInteger?, + ): TransactionExtras } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt new file mode 100644 index 0000000000..52d686e403 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.models.TransactionType +import java.math.BigInteger + +class CreateTransactionDataExtrasUseCase( + private val transactionRepository: TransactionRepository, +) { + + operator fun invoke( + data: String, + network: Network, + transactionType: TransactionType, + gasLimit: BigInteger? = null, + nonce: BigInteger? = null, + ) = Either.catch { + requireNotNull( + transactionRepository.createTransactionDataExtras( + data = data, + network = network, + transactionType = transactionType, + nonce = nonce, + gasLimit = gasLimit, + ), + ) { "Failed to create transaction" } + } +} \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index c40cbbcc83..55f5e6eea3 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction.models) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) implementation(projects.domain.staking) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 87c2e5d142..68521fe254 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,12 +2,12 @@ package com.tangem.feature.swap.domain import arrow.core.Either import arrow.core.getOrElse -import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.minimalAmount -import com.tangem.common.extensions.hexToBytes import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -15,20 +15,16 @@ import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.EstimateFeeUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.transaction.models.TransactionType +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -42,8 +38,9 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.* -import com.tangem.lib.crypto.models.transactions.SendTxResult +import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.ProxyFee +import com.tangem.lib.crypto.models.ProxyFees import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -62,6 +59,7 @@ internal class SwapInteractorImpl @Inject constructor( private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, + private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, private val quotesRepository: QuotesRepository, private val swapTransactionRepository: SwapTransactionRepository, private val currencyChecksRepository: CurrencyChecksRepository, @@ -214,35 +212,51 @@ internal class SwapInteractorImpl @Inject constructor( } else { permissionOptions.approveData.approveData } - val result = transactionManager.sendApproveTransaction( - txData = ApproveTxData( - networkId = networkId, - feeAmount = permissionOptions.txFee.feeValue, - gasLimit = permissionOptions.txFee.gasLimit, - destinationAddress = getTokenAddress(permissionOptions.fromToken), - dataToSign = dataToSign, + val approveTransaction = createTransactionUseCase( + amount = BigDecimal.ZERO.convertToAmount(permissionOptions.fromToken), + fee = getFeeForTransaction( + fee = permissionOptions.txFee, + blockchain = Blockchain.fromId(permissionOptions.fromToken.network.id.value), ), - derivationPath = derivationPath, - analyticsData = AnalyticsData( - feeType = permissionOptions.txFee.feeType.getNameForAnalytics(), - tokenSymbol = permissionOptions.fromToken.symbol, - permissionType = permissionOptions.approveType.getNameForAnalytics(), + memo = null, + destination = getTokenAddress(permissionOptions.fromToken), + network = permissionOptions.fromToken.network, + userWalletId = requireNotNull(getSelectedWallet()).walletId, + txExtras = createDexTxExtras( + dataToSign, + permissionOptions.fromToken.network, + permissionOptions.txFee.gasLimit, ), + isSwap = false, + ).getOrElse { + Timber.e(it, "Failed to create approveTransaction") + return SwapTransactionState.UnknownError + } + + val result = sendTransactionUseCase( + txData = approveTransaction, + userWallet = requireNotNull(getSelectedWallet()), + network = permissionOptions.fromToken.network, ) - return when (result) { - is SendTxResult.Success -> { + return result.fold( + ifRight = { hash -> allowPermissionsHandler.addAddressToInProgress(permissionOptions.forTokenContractAddress) SwapTransactionState.TxSent( - txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + txHash = hash, timestamp = System.currentTimeMillis(), ) - } - SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTxResult.NetworkError -> SwapTransactionState.NetworkError - is SendTxResult.UnknownError -> SwapTransactionState.UnknownError - } + }, + ifLeft = { + when (it) { + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode + else -> SwapTransactionState.UnknownError + } + }, + ) } override suspend fun findBestQuote( @@ -647,11 +661,11 @@ internal class SwapInteractorImpl @Inject constructor( destination = swapData.transaction.txTo, userWalletId = userWalletId, network = currencyToSendStatus.currency.network, - txExtras = createDexTxExtras(fee.gasLimit, dataToSign), + txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit), hash = dataToSign, isSwap = true, ).getOrElse { - Timber.e(it) + Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.UnknownError } @@ -699,13 +713,13 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private fun createDexTxExtras(gasLimit: Int, data: String): TransactionExtras { - // for now we support only Ethereum like DEX - // need to be extended if we support other blockchains in DEX - return EthereumTransactionExtras( - gasLimit = gasLimit.toBigInteger(), - data = data.removePrefix(HEX_PREFIX).hexToBytes(), - ) + private fun createDexTxExtras(data: String, network: Network, gasLimit: Int?): TransactionExtras { + return createTransactionExtrasUseCase( + data = data, + network = network, + transactionType = TransactionType.APPROVE, + gasLimit = gasLimit?.toBigInteger(), + ).getOrNull() ?: error("failed to create extras") } @Suppress("LongMethod") @@ -750,7 +764,7 @@ internal class SwapInteractorImpl @Inject constructor( userWalletId = userWalletId, network = currencyToSend.currency.network, ).getOrElse { - Timber.e(it) + Timber.e(it, "Failed to create swap CEX tx data") return SwapTransactionState.UnknownError } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 76f5998502..3e42fef158 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -12,10 +12,7 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.EstimateFeeUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -45,6 +42,7 @@ class SwapDomainModule { getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, @SwapScope createTransactionUseCase: CreateTransactionUseCase, + createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase, quotesRepository: QuotesRepository, swapTransactionRepository: SwapTransactionRepository, appCurrencyRepository: AppCurrencyRepository, @@ -63,6 +61,7 @@ class SwapDomainModule { getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, sendTransactionUseCase = sendTransactionUseCase, createTransactionUseCase = createTransactionUseCase, + createTransactionExtrasUseCase = createTransactionDataExtrasUseCase, quotesRepository = quotesRepository, swapTransactionRepository = swapTransactionRepository, appCurrencyRepository = appCurrencyRepository, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 9836ee2b61..8cecca280d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-688" +tangemBlockchainSdk = "develop-690" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-366" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 6a3ab8f374..f114465769 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,18 +1,10 @@ package com.tangem.lib.crypto import com.tangem.lib.crypto.models.* -import com.tangem.lib.crypto.models.transactions.SendTxResult import java.math.BigDecimal interface TransactionManager { - @Throws(IllegalStateException::class) - suspend fun sendApproveTransaction( - txData: ApproveTxData, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult - /** * Get fee * From 658fdfdb1506445af355b0f600fce5eda04438e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 18:46:08 +0500 Subject: [PATCH 098/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 5 -- .../tap/proxy/redux/DaggerGraphAction.kt | 2 - .../tap/proxy/redux/DaggerGraphReducer.kt | 1 - .../tap/proxy/redux/DaggerGraphState.kt | 2 - .../tangem/tap/routing/utils/ChildFactory.kt | 10 ++-- features/disclaimer/api/build.gradle.kts | 5 +- .../disclaimer/api/DisclaimerRouter.kt | 8 --- .../api/components/DisclaimerComponent.kt | 12 +++++ features/disclaimer/impl/build.gradle.kts | 5 +- .../disclaimer/impl/DisclaimerFragment.kt | 52 ------------------- .../impl/DefaultDisclaimerComponent.kt | 48 +++++++++++++++++ .../disclaimer/impl/di/ComponentModule.kt | 18 +++++++ .../impl/di/DisclaimerRouterModule.kt | 24 --------- .../disclaimer/impl/di/ModelModule.kt | 20 +++++++ .../DisclaimerUM.kt} | 8 +-- .../disclaimer/impl/model/DisclaimerModel.kt | 49 +++++++++++++++++ .../navigation/DefaultDisclaimerRouter.kt | 22 -------- .../impl/navigation/InnerDisclaimerRouter.kt | 10 ---- .../viewmodel/DisclaimerClickIntents.kt | 6 --- .../viewmodel/DisclaimerViewModel.kt | 41 --------------- .../{presentation => }/ui/DisclaimerScreen.kt | 16 +++--- .../ui/DisclaimerWebViewClient.kt | 2 +- 22 files changed, 173 insertions(+), 193 deletions(-) delete mode 100644 features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt create mode 100644 features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt delete mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt delete mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt rename features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/{presentation/state/DisclaimerState.kt => entity/DisclaimerUM.kt} (59%) create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt delete mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt delete mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt delete mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt delete mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt rename features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/{presentation => }/ui/DisclaimerScreen.kt (91%) rename features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/{presentation => }/ui/DisclaimerWebViewClient.kt (97%) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 1a6869a4a5..62a0089954 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -49,7 +49,6 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -175,9 +174,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject internal lateinit var routingComponentFactory: RoutingComponent.Factory - @Inject - lateinit var disclaimerRouter: DisclaimerRouter - @Inject lateinit var pushNotificationsRouter: PushNotificationsRouter @@ -298,7 +294,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac qrScanningRouter = qrScanningRouter, emailSender = emailSender, stakingRouter = stakingRouter, - disclaimerRouter = disclaimerRouter, pushNotificationsRouter = pushNotificationsRouter, ), ) 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 a141560335..0f3c13fbf9 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 @@ -4,7 +4,6 @@ import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -27,7 +26,6 @@ sealed interface DaggerGraphAction : Action { val qrScanningRouter: QrScanningRouter, val emailSender: EmailSender, val stakingRouter: StakingRouter, - val disclaimerRouter: DisclaimerRouter, val pushNotificationsRouter: PushNotificationsRouter, ) : 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 77f2897541..2a20bab5b6 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 @@ -23,7 +23,6 @@ object DaggerGraphReducer { qrScanningRouter = action.qrScanningRouter, emailSender = action.emailSender, stakingRouter = action.stakingRouter, - disclaimerRouter = action.disclaimerRouter, pushNotificationsRouter = action.pushNotificationsRouter, ) } 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 80779536e5..ee435bc6ec 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 @@ -30,7 +30,6 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.disclaimer.api.DisclaimerRouter import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles @@ -88,7 +87,6 @@ data class DaggerGraphState( val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, - val disclaimerRouter: DisclaimerRouter? = null, val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles? = null, val pushNotificationsRouter: PushNotificationsRouter? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 48df79739b..8b3c22a7f9 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -8,7 +8,7 @@ import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.features.details.DetailsFeatureToggles import com.tangem.features.details.component.DetailsComponent -import com.tangem.features.disclaimer.api.DisclaimerRouter +import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter @@ -46,6 +46,7 @@ import javax.inject.Inject internal class ChildFactory @Inject constructor( private val detailsComponentFactory: DetailsComponent.Factory, private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, + private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -55,7 +56,6 @@ internal class ChildFactory @Inject constructor( private val detailsFeatureToggles: DetailsFeatureToggles, private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, private val pushNotificationRouter: PushNotificationsRouter, - private val disclaimerRouter: DisclaimerRouter, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -106,7 +106,11 @@ internal class ChildFactory @Inject constructor( } is AppRoute.Disclaimer -> { if (pushNotificationsFeatureToggles.isPushNotificationsEnabled) { - route.asFragmentChild(Provider { disclaimerRouter.entryFragment() }) + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DisclaimerComponent.Params(route.isTosAccepted), + componentFactory = disclaimerComponentFactory, + ) } else { route.asFragmentChild(Provider { DisclaimerFragment() }) } diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts index fcd117da1b..7b97fa25c1 100644 --- a/features/disclaimer/api/build.gradle.kts +++ b/features/disclaimer/api/build.gradle.kts @@ -10,6 +10,7 @@ android { dependencies { - /** AndroidX */ - implementation(deps.androidx.fragment.ktx) + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) } \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt deleted file mode 100644 index e990639fc8..0000000000 --- a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/DisclaimerRouter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.features.disclaimer.api - -import androidx.fragment.app.Fragment - -interface DisclaimerRouter { - - fun entryFragment(): Fragment -} \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt new file mode 100644 index 0000000000..df556bb776 --- /dev/null +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.disclaimer.api.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface DisclaimerComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params( + val isTosAccepted: Boolean, + ) +} \ No newline at end of file diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index b9bb0c5263..bb7ee06e94 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -11,9 +11,7 @@ android { } dependencies { - /** AndroidX */ - implementation(deps.androidx.fragment.ktx) - implementation(deps.androidx.appCompat) + /* AndroidX */ implementation(deps.lifecycle.compose) implementation(deps.androidx.activity.compose) @@ -31,6 +29,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.core.decompose) implementation(projects.common.routing) /** Domain modules */ diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt deleted file mode 100644 index b53e0353d2..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/DisclaimerFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.features.disclaimer.impl - -import android.os.Bundle -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.fragment.app.viewModels -import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY -import com.tangem.common.routing.AppRouter -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen -import com.tangem.features.disclaimer.impl.presentation.viewmodel.DisclaimerViewModel -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -internal class DisclaimerFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - @Inject - lateinit var appRouter: AppRouter - - private val viewModel by viewModels() - - private val isTosAccepted: Boolean - get() = arguments?.getBoolean(IS_TOS_ACCEPTED_KEY) ?: false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - lifecycle.addObserver(viewModel) - } - - @Composable - override fun ScreenContent(modifier: Modifier) { - BackHandler { - if (isTosAccepted) { - appRouter.pop() - } else { - requireActivity().finish() - } - } - DisclaimerScreen(viewModel.state, appRouter::pop) - } - - companion object { - /** Create disclaimer fragment instance */ - fun create(): DisclaimerFragment = DisclaimerFragment() - } -} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt new file mode 100644 index 0000000000..f281084c55 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.disclaimer.impl.component.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.disclaimer.impl.model.DisclaimerModel +import com.tangem.features.disclaimer.impl.ui.DisclaimerScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultDisclaimerComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: DisclaimerComponent.Params, + private val appFinisher: AppFinisher, +) : DisclaimerComponent, AppComponentContext by context { + + private val model: DisclaimerModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + BackHandler { + if (params.isTosAccepted) { + state.popBack() + } else { + appFinisher.finish() + } + } + DisclaimerScreen(state = state) + } + + @AssistedFactory + interface Factory : DisclaimerComponent.Factory { + + override fun create( + context: AppComponentContext, + params: DisclaimerComponent.Params, + ): DefaultDisclaimerComponent + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..1605d6bbd6 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.disclaimer.impl.component.impl.DefaultDisclaimerComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindDisclaimerComponentFactory(factory: DefaultDisclaimerComponent.Factory): DisclaimerComponent.Factory +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt deleted file mode 100644 index cdac031d49..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/DisclaimerRouterModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.disclaimer.impl.di - -import com.tangem.common.routing.AppRouter -import com.tangem.features.disclaimer.api.DisclaimerRouter -import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped - -/** - * DI module provides implementation of [DisclaimerRouter] - */ -@Module -@InstallIn(ActivityComponent::class) -object DisclaimerRouterModule { - - @Provides - @ActivityScoped - fun provideDisclaimerRouter(appRouter: AppRouter): DisclaimerRouter { - return DefaultDisclaimerRouter(appRouter) - } -} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt new file mode 100644 index 0000000000..a589336c78 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.disclaimer.impl.model.DisclaimerModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(DisclaimerModel::class) + fun provideDisclaimerModel(model: DisclaimerModel): Model +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/entity/DisclaimerUM.kt similarity index 59% rename from features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt rename to features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/entity/DisclaimerUM.kt index 01d0a398a6..155a91269b 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/state/DisclaimerState.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/entity/DisclaimerUM.kt @@ -1,16 +1,18 @@ -package com.tangem.features.disclaimer.impl.presentation.state +package com.tangem.features.disclaimer.impl.entity -internal data class DisclaimerState( +internal data class DisclaimerUM( val url: String, val isTosAccepted: Boolean, val onAccept: (Boolean) -> Unit, + val popBack: () -> Unit, ) internal object DummyDisclaimer { - val state = DisclaimerState( + val state = DisclaimerUM( url = "https://tangem.com/tangem_tos.html", isTosAccepted = false, onAccept = {}, + popBack = {}, ) } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt new file mode 100644 index 0000000000..e3007b5a5a --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -0,0 +1,49 @@ +package com.tangem.features.disclaimer.impl.model + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.card.repository.CardRepository +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.disclaimer.impl.entity.DisclaimerUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class DisclaimerModel @Inject constructor( + private val cardRepository: CardRepository, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: DisclaimerComponent.Params = paramsContainer.require() + + val state: MutableStateFlow = MutableStateFlow( + DisclaimerUM( + onAccept = ::onAccept, + url = DISCLAIMER_URL, + isTosAccepted = params.isTosAccepted, + popBack = router::pop, + ), + ) + + private fun onAccept(shouldAskPushPermission: Boolean) { + modelScope.launch { + cardRepository.acceptTangemTOS() + if (shouldAskPushPermission) { + router.push(AppRoute.PushNotification) + } else { + router.push(AppRoute.Home) + } + } + } + + private companion object { + const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt deleted file mode 100644 index 2897635472..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/DefaultDisclaimerRouter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.disclaimer.impl.navigation - -import androidx.fragment.app.Fragment -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.features.disclaimer.impl.DisclaimerFragment -import javax.inject.Inject - -internal class DefaultDisclaimerRouter @Inject constructor( - private val appRouter: AppRouter, -) : InnerDisclaimerRouter { - - override fun entryFragment(): Fragment = DisclaimerFragment.create() - - override fun openPushNotificationPermission() { - appRouter.push(AppRoute.PushNotification) - } - - override fun openHome() { - appRouter.push(AppRoute.Home) - } -} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt deleted file mode 100644 index 5077db0901..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/navigation/InnerDisclaimerRouter.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.disclaimer.impl.navigation - -import com.tangem.features.disclaimer.api.DisclaimerRouter - -internal interface InnerDisclaimerRouter : DisclaimerRouter { - - fun openPushNotificationPermission() - - fun openHome() -} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt deleted file mode 100644 index d80a1236ed..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerClickIntents.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.disclaimer.impl.presentation.viewmodel - -internal interface DisclaimerClickIntents { - - fun onAccept(shouldAskPushPermission: Boolean) -} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt deleted file mode 100644 index 610278cd04..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/viewmodel/DisclaimerViewModel.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.features.disclaimer.impl.presentation.viewmodel - -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY -import com.tangem.domain.card.repository.CardRepository -import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter -import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -internal class DisclaimerViewModel @Inject constructor( - private val cardRepository: CardRepository, - private val disclaimerRouter: DefaultDisclaimerRouter, - savedStateHandle: SavedStateHandle, -) : ViewModel(), DefaultLifecycleObserver, DisclaimerClickIntents { - - private val isTosAccepted: Boolean = savedStateHandle[IS_TOS_ACCEPTED_KEY] ?: false - - val state: DisclaimerState - get() = DisclaimerState( - onAccept = ::onAccept, - url = DISCLAIMER_URL, - isTosAccepted = isTosAccepted, - ) - - override fun onAccept(shouldAskPushPermission: Boolean) { - viewModelScope.launch { - cardRepository.acceptTangemTOS() - disclaimerRouter.openPushNotificationPermission() - } - } - - private companion object { - const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" - } -} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt similarity index 91% rename from features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt rename to features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 516eab4ef4..7567197f2d 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -1,4 +1,4 @@ -package com.tangem.features.disclaimer.impl.presentation.ui +package com.tangem.features.disclaimer.impl.ui import android.annotation.SuppressLint import android.content.res.Configuration @@ -33,12 +33,12 @@ import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.disclaimer.impl.R -import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState -import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer +import com.tangem.features.disclaimer.impl.entity.DisclaimerUM +import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull @Composable -internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { +internal fun DisclaimerScreen(state: DisclaimerUM) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val bottomPadding = if (state.isTosAccepted) { bottomBarHeight + TangemTheme.dimens.size16 @@ -61,7 +61,7 @@ internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) { text = resourceReference(R.string.disclaimer_title), startButton = AdditionalButton( iconRes = R.drawable.ic_back_24, - onIconClicked = onBackClick, + onIconClicked = state.popBack, ).takeIf { state.isTosAccepted }, textColor = textColor, iconColor = iconColor, @@ -134,12 +134,12 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { @OptIn(ExperimentalPermissionsApi::class) @Composable private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { - val shouldAskPushPermission = getPushPermissionOrNull()?.let { permission -> + val isPermissionGranted = getPushPermissionOrNull()?.let { permission -> rememberPermissionState(permission = permission).status.isGranted } ?: true PrimaryButton( text = stringResource(id = R.string.common_accept), - onClick = { onAccept(shouldAskPushPermission) }, + onClick = { onAccept(!isPermissionGranted) }, colors = TangemButtonColors( backgroundColor = TangemColorPalette.Light4, contentColor = TangemColorPalette.Dark6, @@ -164,7 +164,7 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { @Composable private fun DisclaimerScreen_Preview() { TangemThemePreview { - DisclaimerScreen(state = DummyDisclaimer.state, onBackClick = {}) + DisclaimerScreen(state = DummyDisclaimer.state) } } // endregion \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt similarity index 97% rename from features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt rename to features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt index a0b568833b..5ad8aedb8f 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/presentation/ui/DisclaimerWebViewClient.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -1,4 +1,4 @@ -package com.tangem.features.disclaimer.impl.presentation.ui +package com.tangem.features.disclaimer.impl.ui import android.graphics.Bitmap import android.webkit.* From d521742473128a1bbdd73620915a13345025c0a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 2 Jul 2024 15:34:46 +0500 Subject: [PATCH 099/223] Updated on 2026-08-14 --- .../GiveTxPermisssionBottomSheet.kt | 340 ++++++++++++++++++ .../GiveTxPermissionBottomSheetConfig.kt | 8 + .../state/GiveTxPermissionState.kt | 20 +- core/res/src/main/res/values-ru/strings.xml | 17 +- .../src/main/res/values-zh-rTW/strings.xml | 4 - core/res/src/main/res/values/strings.xml | 19 +- .../src/main/res/drawable/ic_infinity_24.xml | 5 + features/swap/presentation/build.gradle.kts | 1 + .../feature/swap/analytics/SwapEvents.kt | 2 +- .../feature/swap/models/SwapStateHolder.kt | 3 +- .../tangem/feature/swap/models/UiActions.kt | 1 + .../states/GivePermissionBottomSheetConfig.kt | 9 - .../tangem/feature/swap/ui/StateBuilder.kt | 27 +- .../swap/ui/SwapPermissionBottomSheet.kt | 317 ---------------- .../com/tangem/feature/swap/ui/SwapScreen.kt | 7 +- .../feature/swap/ui/SwapScreenContent.kt | 5 +- .../swap/viewmodels/SwapProcessDataState.kt | 2 +- .../feature/swap/viewmodels/SwapViewModel.kt | 10 +- 18 files changed, 414 insertions(+), 383 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt rename features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt => common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt (62%) create mode 100644 core/ui/src/main/res/drawable/ic_infinity_24.xml delete mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt delete mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt new file mode 100644 index 0000000000..e65b7b86f1 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt @@ -0,0 +1,340 @@ +package com.tangem.common.ui.bottomsheets + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.PopupProperties +import com.tangem.common.ui.R +import com.tangem.common.ui.bottomsheets.state.* +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.secondary, + ) { content: GiveTxPermissionBottomSheetConfig -> + GiveTxPermissionBottomSheetContent(content = content) + } +} + +@Composable +private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSheetConfig) { + var isPermissionAlertShow by remember { mutableStateOf(false) } + val data = content.data + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithAdditionalButtons( + text = resourceReference(id = R.string.give_permission_title), + iconColor = TangemTheme.colors.text.tertiary, + endButton = AdditionalButton( + iconRes = R.drawable.ic_information_24, + onIconClicked = { isPermissionAlertShow = true }, + ), + ) + Text( + text = stringResource( + id = R.string.give_permission_subtitle, + data.currency, + ), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24), + ) + + SpacerH16() + + ApprovalBottomSheetInfo(data) + + SpacerH(height = TangemTheme.dimens.spacing20) + + PrimaryButtonIconEnd( + text = stringResource(id = R.string.common_approve), + iconResId = R.drawable.ic_tangem_24, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = data.approveButton.onClick, + ) + + SpacerH12() + + SecondaryButton( + text = stringResource(id = R.string.common_cancel), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = content.onCancel, + ) + + SpacerH16() + + // region dialog + if (isPermissionAlertShow) { + BasicDialog( + message = stringResource(id = R.string.swapping_approve_information_text), + title = stringResource(id = R.string.swapping_approve_information_title), + confirmButton = DialogButton { isPermissionAlertShow = false }, + onDismissDialog = {}, + ) + } + } +} + +@Composable +private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) { + FooterContainer( + footer = stringResource(id = R.string.give_permission_policy_type_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + AmountItem( + currency = data.currency, + approveType = data.approveType, + onChangeApproveType = data.onChangeApproveType, + approveItems = data.approveItems, + ) + } + SpacerH16() + FooterContainer( + footer = stringResource(id = R.string.give_permission_fee_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + FeeItem(fee = data.fee) + } +} + +@Composable +private fun FeeItem(fee: TextReference) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing16, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.common_network_fee_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + EllipsisText( + text = fee.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + ) + } +} + +@Composable +private fun AmountItem( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, +) { + var isExpandSelector by remember { mutableStateOf(false) } + var amountSize by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = { isExpandSelector = true }, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { amountSize = it } + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing16, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.give_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + when (approveType) { + ApproveType.LIMITED -> { + Text( + text = stringResource(id = R.string.give_permission_current_transaction), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + } + ApproveType.UNLIMITED -> { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_infinity_24), + ), + tint = TangemTheme.colors.text.tertiary, + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size20), + ) + } + } + } + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { approveType -> + isExpandSelector = false + onChangeApproveType.invoke(approveType) + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun DropdownSelector( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, + selectedType: ApproveType, + amountSize: IntSize, +) { + var dropDownWidth by remember { mutableStateOf(IntSize.Zero) } + val offsetY = amountSize.height.times(-1) + val offsetX = amountSize.width - dropDownWidth.width + + // Workaround to set color and shape of dropdown menu + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action), + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)), + ) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + properties = PopupProperties(clippingEnabled = false), + offset = with(LocalDensity.current) { + DpOffset(x = offsetX.toDp(), y = offsetY.toDp()) + }, + modifier = Modifier + .wrapContentSize() + .background(TangemTheme.colors.background.action) + .onSizeChanged { dropDownWidth = it }, + ) { + items.forEach { item -> + val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent + + DropdownMenuItem( + modifier = Modifier.fillMaxWidth(), + text = { + Row { + Text( + text = when (item) { + ApproveType.LIMITED -> stringResource( + id = R.string.give_permission_current_transaction, + ) + ApproveType.UNLIMITED -> stringResource(id = R.string.give_permission_unlimited) + }, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + SpacerWMax() + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_check_24), + ), + tint = color, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.size20), + ) + } + }, + onClick = { + onItemClick.invoke(item) + }, + ) + } + } + } +} + +// region preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_AgreementBottomSheet() { + TangemThemePreview { + GiveTxPermissionBottomSheetContent(content = previewData) + } +} + +private val previewData = GiveTxPermissionBottomSheetConfig( + data = GiveTxPermissionState.ReadyForRequest( + currency = "DAI", + amount = "1", + walletAddress = "", + spenderAddress = "", + fee = TextReference.Str("2,14$"), + approveType = ApproveType.UNLIMITED, + approveButton = ApprovePermissionButton(true) {}, + cancelButton = CancelPermissionButton(true), + onChangeApproveType = { ApproveType.UNLIMITED }, + ), + onCancel = {}, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt new file mode 100644 index 0000000000..ad5b970ea9 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt @@ -0,0 +1,8 @@ +package com.tangem.common.ui.bottomsheets.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +data class GiveTxPermissionBottomSheetConfig( + val data: GiveTxPermissionState.ReadyForRequest, + val onCancel: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt similarity index 62% rename from features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt index 4192106336..9b07504849 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt @@ -1,15 +1,14 @@ -package com.tangem.feature.swap.models +package com.tangem.common.ui.bottomsheets.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.feature.swap.domain.models.domain.SwapApproveType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -sealed class SwapPermissionState { +sealed class GiveTxPermissionState { - object InProgress : SwapPermissionState() + data object InProgress : GiveTxPermissionState() - object Empty : SwapPermissionState() + data object Empty : GiveTxPermissionState() data class ReadyForRequest( val currency: String, @@ -18,24 +17,17 @@ sealed class SwapPermissionState { val spenderAddress: String, val fee: TextReference, val approveType: ApproveType, - val approveItems: ImmutableList = ApproveType.values().toList().toImmutableList(), + val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), val approveButton: ApprovePermissionButton, val cancelButton: CancelPermissionButton, val onChangeApproveType: (ApproveType) -> Unit, - ) : SwapPermissionState() + ) : GiveTxPermissionState() } enum class ApproveType { LIMITED, UNLIMITED } -fun ApproveType.toDomainApproveType(): SwapApproveType { - return when (this) { - ApproveType.LIMITED -> SwapApproveType.LIMITED - ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED - } -} - data class ApprovePermissionButton( val enabled: Boolean, val loading: Boolean = false, diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 053247f58d..c7913d2da6 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -70,6 +70,7 @@ Доступ запрещен Применить Одобрение + Подтвердить Внимание Баланс: %s Баланс @@ -261,6 +262,13 @@ Обращение в поддержку Обращение в поддержку Tangem Не могу отправить транзакцию + Текущая транзакция + Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. + Укажите лимит доступа к выбранному токену + Количество %s + Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Дать разрешение + Безлимитно Купить Сканировать Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции @@ -388,6 +396,7 @@ Активация карты Резервная карта #%d Нет резервных карт + Уведомления Добавлена ​​одна резервная карта Подготовьте свою карту Добавлены две резервные карты @@ -581,17 +590,9 @@ Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить Вы отправляете - Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств - Подтвердить - Текущая транзакция - Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Дать разрешение - Укажите лимит доступа к выбранному токену - Количество %s - Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s - Безлимитно В процессе Обменять Вы получите diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index e0b8c69381..28f111ab8b 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -277,13 +277,9 @@ 兼容Web3.0 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 批准 - 賦予權限 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 - 允許 賦予權限 - 數量 %s - 要繼續,您需要允許 1inch 智能合約使用您的 %s 進行中 交易 選擇代幣 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index e7228a19a8..28d7f3aefc 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -71,6 +71,7 @@ Allow Apply Approval + Approve Attention Balance: %s Balance @@ -264,6 +265,13 @@ Feedback Tangem feedback Can\'t send a transaction + Current transaction + The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. + Specify the approve limit for the selected token + Amount %s + To continue, grant 1inch smart contracts permission to use your %s + Give Permission + Unlimited Order card Scan card To change the access code tap the card as shown above and do not remove until the end of the operation @@ -397,6 +405,7 @@ Activating card Backup card #%d No backup cards + Notifications One backup card added Prepare your card Two backup cards added @@ -607,17 +616,9 @@ All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve You swap - Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds - Approve - Current transaction - The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Give Permission - Specify the approve limit for the selected token - Amount %s - To continue, grant 1inch smart contracts permission to use your %s - Unlimited In progress Swap You receive @@ -671,7 +672,7 @@ Use %s or scan a card to have an access to your wallet Stay up to date with the latest features and news Be able to get important market notifications - Would you like to use
Push-notifications? + Would you like to use\nPush-notifications? Add new wallet Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in diff --git a/core/ui/src/main/res/drawable/ic_infinity_24.xml b/core/ui/src/main/res/drawable/ic_infinity_24.xml new file mode 100644 index 0000000000..cf0d1b2c54 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_infinity_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index f82d33b863..c41603a025 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.ui) implementation(projects.common.routing) + implementation(projects.common.ui) /** Domain modules **/ implementation(projects.domain.appCurrency) 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 2fd8fa08ac..2c8c11fb70 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,9 +1,9 @@ package com.tangem.feature.swap.analytics +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.FeeType -import com.tangem.feature.swap.models.ApproveType private const val SWAP_CATEGORY = "Swap" diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index cd22de2a3b..e3d7c3d5c8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig @@ -22,7 +23,7 @@ data class SwapStateHolder( val providerState: ProviderState, val fee: FeeItemState = FeeItemState.Empty, - val permissionState: SwapPermissionState = SwapPermissionState.Empty, + val permissionState: GiveTxPermissionState = GiveTxPermissionState.Empty, val priceImpact: PriceImpact, val successState: SwapSuccessStateHolder? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 6bbee12717..c0c890750e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.models +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt deleted file mode 100644 index 9a8bd7a519..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.feature.swap.models.states - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.feature.swap.models.SwapPermissionState - -data class GivePermissionBottomSheetConfig( - val data: SwapPermissionState.ReadyForRequest, - val onCancel: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c902406819..02a25af845 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.bottomsheets.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig @@ -649,7 +650,7 @@ internal class StateBuilder( ), receiveCardData = receiveCardData, warnings = warnings, - permissionState = SwapPermissionState.Empty, + permissionState = GiveTxPermissionState.Empty, fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, @@ -906,7 +907,7 @@ internal class StateBuilder( } fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder { - val config = uiState.bottomSheetConfig?.content as? GivePermissionBottomSheetConfig + val config = uiState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig return if (config != null) { uiState.copy( bottomSheetConfig = uiState.bottomSheetConfig.copy( @@ -980,7 +981,7 @@ internal class StateBuilder( ), ) return uiState.copy( - permissionState = SwapPermissionState.InProgress, + permissionState = GiveTxPermissionState.InProgress, warnings = warnings, ) } @@ -1140,27 +1141,27 @@ internal class StateBuilder( } private fun convertPermissionState( - lastPermissionState: SwapPermissionState, + lastPermissionState: GiveTxPermissionState, permissionDataState: PermissionDataState, onGivePermissionClick: () -> Unit, onChangeApproveType: (ApproveType) -> Unit, - ): SwapPermissionState { - val approveType = if (lastPermissionState is SwapPermissionState.ReadyForRequest) { + ): GiveTxPermissionState { + val approveType = if (lastPermissionState is GiveTxPermissionState.ReadyForRequest) { lastPermissionState.approveType } else { ApproveType.UNLIMITED } return when (permissionDataState) { - PermissionDataState.Empty -> SwapPermissionState.Empty - PermissionDataState.PermissionFailed -> SwapPermissionState.Empty - PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress + PermissionDataState.Empty -> GiveTxPermissionState.Empty + PermissionDataState.PermissionFailed -> GiveTxPermissionState.Empty + PermissionDataState.PermissionLoading -> GiveTxPermissionState.InProgress is PermissionDataState.PermissionReadyForRequest -> { val permissionFee = when (val fee = permissionDataState.requestApproveData.fee) { TxFeeState.Empty -> error("Fee shouldn't be empty") is TxFeeState.MultipleFeeState -> fee.priorityFee is TxFeeState.SingleFeeState -> fee.fee } - SwapPermissionState.ReadyForRequest( + GiveTxPermissionState.ReadyForRequest( currency = permissionDataState.currency, amount = permissionDataState.amount, approveType = approveType, @@ -1193,8 +1194,8 @@ internal class StateBuilder( fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { val permissionState = uiState.permissionState - if (permissionState is SwapPermissionState.ReadyForRequest) { - val config = GivePermissionBottomSheetConfig( + if (permissionState is GiveTxPermissionState.ReadyForRequest) { + val config = GiveTxPermissionBottomSheetConfig( data = permissionState, onCancel = onDismiss, ) @@ -1402,7 +1403,7 @@ internal class StateBuilder( return NotificationConfig( title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( - id = R.string.swapping_permission_subheader, + id = R.string.give_permission_subtitle, formatArgs = wrappedList(fromTokenSymbol), ), iconResId = R.drawable.ic_locked_24, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt deleted file mode 100644 index 8ac175588a..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt +++ /dev/null @@ -1,317 +0,0 @@ -package com.tangem.feature.swap.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.swap.models.ApprovePermissionButton -import com.tangem.feature.swap.models.ApproveType -import com.tangem.feature.swap.models.CancelPermissionButton -import com.tangem.feature.swap.models.SwapPermissionState -import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig -import com.tangem.feature.swap.presentation.R -import kotlinx.collections.immutable.ImmutableList - -@Composable -fun SwapPermissionBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: GivePermissionBottomSheetConfig -> - SwapPermissionBottomSheetContent(content = content) - } -} - -@Composable -private fun SwapPermissionBottomSheetContent(content: GivePermissionBottomSheetConfig) { - var isPermissionAlertShow by remember { mutableStateOf(false) } - val data = content.data - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = stringResource(id = R.string.swapping_permission_header), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - ) - IconButton( - modifier = Modifier.align(Alignment.CenterEnd), - onClick = { isPermissionAlertShow = true }, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_question_24), - contentDescription = null, - ) - } - } - - SpacerH10() - - Text( - text = stringResource( - id = R.string.swapping_permission_subheader, - data.currency, - ), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), - ) - - SpacerH16() - - ApprovalBottomSheetInfo(data) - - SpacerH28() - - PrimaryButtonIconEnd( - text = stringResource(id = R.string.swapping_permission_buttons_approve), - iconResId = R.drawable.ic_tangem_24, - modifier = Modifier.fillMaxWidth(), - onClick = data.approveButton.onClick, - ) - - SpacerH12() - - SecondaryButton( - text = stringResource(id = R.string.common_cancel), - modifier = Modifier.fillMaxWidth(), - onClick = { - content.onCancel() - }, - ) - - SpacerH16() - - // region dialog - if (isPermissionAlertShow) { - BasicDialog( - message = stringResource(id = R.string.swapping_approve_information_text), - title = stringResource(id = R.string.swapping_approve_information_title), - confirmButton = DialogButton { isPermissionAlertShow = false }, - onDismissDialog = {}, - ) - } - } -} - -@Composable -private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AmountItem( - currency = data.currency, - approveType = data.approveType, - onChangeApproveType = data.onChangeApproveType, - approveItems = data.approveItems, - ) - SubtitleItem( - subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), - modifier = Modifier.fillMaxWidth(), - ) - SpacerH24() - DividerBottomSheet() - FeeItem(fee = data.fee.resolveReference()) - SubtitleItem( - subtitle = stringResource(id = R.string.swapping_permission_fee_footer), - modifier = Modifier.fillMaxWidth(), - ) - } -} - -@Composable -private fun DividerBottomSheet() { - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - ) -} - -@Composable -private fun InformationItem(subtitle: String, value: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = subtitle, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - maxLines = 1, - ) - - MiddleEllipsisText( - text = value, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), - ) - } -} - -@Composable -private fun AmountItem( - currency: String, - approveType: ApproveType, - approveItems: ImmutableList, - onChangeApproveType: (ApproveType) -> Unit, -) { - var isExpandSelector by remember { - mutableStateOf(false) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.swapping_permission_rows_amount, currency), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - maxLines = 1, - ) - Box { - SelectorItem( - getTitleForApproveType(approveType = approveType), - ) { - isExpandSelector = true - } - DropdownSelector( - isExpanded = isExpandSelector, - onDismiss = { isExpandSelector = false }, - onItemClick = { approveType -> - isExpandSelector = false - onChangeApproveType.invoke(approveType) - }, - items = approveItems, - ) - } - } -} - -@Composable -private fun SelectorItem(title: String, onClick: () -> Unit) { - Row( - modifier = Modifier.clickable { onClick() }, - ) { - Text( - text = title, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - Icon( - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } -} - -@Composable -private fun DropdownSelector( - isExpanded: Boolean, - onDismiss: () -> Unit, - onItemClick: (ApproveType) -> Unit, - items: ImmutableList, -) { - DropdownMenu( - expanded = isExpanded, - onDismissRequest = onDismiss, - modifier = Modifier - .wrapContentSize() - .background(TangemTheme.colors.background.secondary), - ) { - items.forEach { item -> - DropdownMenuItem( - onClick = { - onItemClick.invoke(item) - }, - ) { - Text( - text = getTitleForApproveType(approveType = item), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - } - } - } -} - -@Composable -private fun FeeItem(fee: String) { - InformationItem( - subtitle = stringResource(id = R.string.common_network_fee_title), - value = fee, - ) -} - -@Composable -private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = subtitle, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { - ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) - ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) -} - -// region preview - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AgreementBottomSheet() { - TangemThemePreview { - SwapPermissionBottomSheetContent(content = previewData) - } -} - -private val previewData = GivePermissionBottomSheetConfig( - data = SwapPermissionState.ReadyForRequest( - currency = "DAI", - amount = "∞", - walletAddress = "", - spenderAddress = "", - fee = TextReference.Str("2,14$"), - approveType = ApproveType.UNLIMITED, - approveButton = ApprovePermissionButton(true) {}, - cancelButton = CancelPermissionButton(true), - onChangeApproveType = { ApproveType.UNLIMITED }, - ), - onCancel = {}, -) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index ff34b02557..9ae4b97bca 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -6,13 +6,14 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.bottomsheets.GiveTxPermissionBottomSheet +import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionBottomSheetConfig import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig -import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig import com.tangem.feature.swap.presentation.R @@ -40,8 +41,8 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { stateHolder.bottomSheetConfig?.let { config -> when (config.content) { - is GivePermissionBottomSheetConfig -> { - SwapPermissionBottomSheet(config = config) + is GiveTxPermissionBottomSheetConfig -> { + GiveTxPermissionBottomSheet(config = config) } is ChooseProviderBottomSheetConfig -> { ChooseProviderBottomSheet(config = config) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index a0c5919e82..582ba1f779 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout +import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig @@ -414,7 +415,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U state.warnings.any { it is SwapWarning.PermissionNeeded } -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.swapping_give_permission), + text = stringResource(id = R.string.give_permission_title), enabled = true, onClick = onPermissionWarningClick, ) @@ -495,7 +496,7 @@ private val state = SwapStateHolder( onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {}, - permissionState = SwapPermissionState.InProgress, + permissionState = GiveTxPermissionState.InProgress, blockchainId = "POLYGON", providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index aada4b7d2b..969fadc27a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.viewmodels +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider @@ -7,7 +8,6 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee -import com.tangem.feature.swap.models.ApproveType data class SwapProcessDataState( // Initial network id diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 692cb6ea7a..398a248ec5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -9,6 +9,7 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -73,7 +74,7 @@ internal class SwapViewModel @Inject constructor( ) : ViewModel(), DefaultLifecycleObserver { private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Swap.CURRENCY_BUNDLE_KEY) - ?.let { it.unbundle(CryptoCurrency.serializer()) } + ?.unbundle(CryptoCurrency.serializer()) ?: error("no expected parameter CryptoCurrency found`") private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus @@ -1173,6 +1174,13 @@ internal class SwapViewModel @Inject constructor( ) } + private fun ApproveType.toDomainApproveType(): SwapApproveType { + return when (this) { + ApproveType.LIMITED -> SwapApproveType.LIMITED + ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED + } + } + private companion object { const val loggingTag = "SwapViewModel" const val INITIAL_AMOUNT = "" From 10d959b0de0783e11be19cfeb62979da6606a91a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 18:14:36 +0500 Subject: [PATCH 100/223] Updated on 2026-08-14 --- .../ui/components/inputrow/InputRowDefault.kt | 1 + .../components/inputrow/InputRowImageInfo.kt | 2 +- .../src/main/java/com/tangem/utils/Strings.kt | 1 + .../state/InnerYieldBalanceState.kt | 14 +- .../state/StakingStateController.kt | 1 + .../presentation/state/StakingStateRouter.kt | 5 + .../impl/presentation/state/StakingUiState.kt | 14 ++ .../RewardsValidatorStateConverter.kt | 100 +++++++++++++ .../converters/YieldBalancesConverter.kt | 138 ++++++++++++++++++ .../previewdata/InitialStakingStatePreview.kt | 6 +- .../state/stub/StakingClickIntentsStub.kt | 4 + .../SetInitialDataStateTransformer.kt | 112 +++----------- .../ui/StakingClaimRewardsValidatorContent.kt | 73 +++++++++ .../ui/StakingInitialInfoContent.kt | 73 +++++---- .../ui/StakingNavigationButtons.kt | 40 +++-- .../impl/presentation/ui/StakingScreen.kt | 12 +- .../viewmodel/StakingClickIntents.kt | 4 + .../viewmodel/StakingViewModel.kt | 29 ++-- 18 files changed, 467 insertions(+), 162 deletions(-) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 3b1e657dda..1eff068d9f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -90,6 +90,7 @@ fun InputRowDefault( bottom = TangemTheme.dimens.spacing10, ) .clickable( + enabled = onIconClick != null, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), ) { onIconClick?.invoke() }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index 81a7e01218..c4fb398c6e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -36,13 +36,13 @@ import com.tangem.core.ui.res.TangemThemePreview @Suppress("LongParameterList") @Composable fun InputRowImageInfo( - title: TextReference?, subtitle: TextReference, caption: TextReference, infoTitle: TextReference, infoSubtitle: TextReference, imageUrl: String, modifier: Modifier = Modifier, + title: TextReference? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, ) { diff --git a/core/utils/src/main/java/com/tangem/utils/Strings.kt b/core/utils/src/main/java/com/tangem/utils/Strings.kt index f6cc72a41a..b40470e852 100644 --- a/core/utils/src/main/java/com/tangem/utils/Strings.kt +++ b/core/utils/src/main/java/com/tangem/utils/Strings.kt @@ -4,4 +4,5 @@ object Strings { const val STARS = "\u2217\u2217\u2217" const val DOT = "•" + const val PLUS = "+" } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index f50e8ec598..61015e2357 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -1,8 +1,8 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield +import kotlinx.collections.immutable.ImmutableList sealed class InnerYieldBalanceState { data class Data( @@ -16,15 +16,21 @@ sealed class InnerYieldBalanceState { } data class BalanceGroupedState( - val items: List, - val type: BalanceType, + val items: ImmutableList, val footer: TextReference?, val title: TextReference, ) data class BalanceState( val validator: Yield.Validator, + val cryptoValue: String, val cryptoAmount: TextReference, val fiatAmount: TextReference, val rawCurrencyId: String?, -) \ No newline at end of file +) + +enum class BalanceGroupType { + ACTIVE, + UNSTAKED, + UNKNOWN, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index bef915818c..d62526b9a8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -39,6 +39,7 @@ internal class StakingStateController @Inject constructor() { currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), + rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmStakingState = StakingStates.ConfirmStakingState.Empty(), isBalanceHidden = false, event = consumedEvent(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index eef49189ea..921430000b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -28,6 +28,7 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.uiState.value.currentStep) { StakingStep.InitialInfo -> showAmount() + StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Amount, -> showConfirm() @@ -50,6 +51,10 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } + fun showRewardsValidators() { + stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } + } + fun showAmount() { stateController.update { it.copy(currentStep = StakingStep.Amount) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index b1758a8d42..8d952f02e4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -19,6 +19,7 @@ internal data class StakingUiState( val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, + val rewardsValidatorsState: StakingStates.RewardsValidatorsState, val confirmStakingState: StakingStates.ConfirmStakingState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, @@ -61,6 +62,18 @@ internal sealed class StakingStates { ) : InitialInfoState() } + /** Select validator to claim rewards state */ + sealed class RewardsValidatorsState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val rewards: ImmutableList, + ) : RewardsValidatorsState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : RewardsValidatorsState() + } + /** Confirm state */ sealed class ConfirmStakingState : StakingStates() { data class Data( @@ -81,6 +94,7 @@ internal sealed class StakingStates { enum class StakingStep { InitialInfo, + RewardsValidators, Amount, Confirm, Validators, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt new file mode 100644 index 0000000000..7ebb9058e7 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -0,0 +1,100 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.BalanceItem +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.utils.Provider +import com.tangem.utils.Strings.PLUS +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addOrReplace +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class RewardsValidatorStateConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: Unit): StakingStates.RewardsValidatorsState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + return if (yieldBalance is YieldBalance.Data) { + val balances = yieldBalance.balance.items + StakingStates.RewardsValidatorsState.Data( + isPrimaryButtonEnabled = true, + rewards = balances + // todo remove when real data is available + .addOrReplace( + item = balances.first().copy( + type = BalanceType.REWARDS, + ), + predicate = { true }, + ) + .filter { it.type == BalanceType.REWARDS } + .mapRewardBalances(cryptoCurrencyStatus) + .toPersistentList(), + ) + } else { + StakingStates.RewardsValidatorsState.Empty() + } + } + + private fun List.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) = + this.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true) + } + val cryptoValue = balance.amount.times(balance.pricePerShare) + val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) + + validator?.toBalanceState( + cryptoCurrencyStatus = cryptoCurrencyStatus, + cryptoValue = cryptoValue, + fiatValue = fiatValue, + ) + } + + private fun Yield.Validator.toBalanceState( + cryptoCurrencyStatus: CryptoCurrencyStatus, + cryptoValue: BigDecimal, + fiatValue: BigDecimal?, + ): BalanceState { + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + val cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoValue, + cryptoCurrency = cryptoCurrency, + ), + ) + val fiatAmount = combinedReference( + stringReference(PLUS), + stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + ) + + return BalanceState( + validator = this, + cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), + cryptoAmount = cryptoAmount, + fiatAmount = fiatAmount, + rawCurrencyId = cryptoCurrency.id.rawCurrencyId, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt new file mode 100644 index 0000000000..28ede76079 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -0,0 +1,138 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.* +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.BalanceGroupType +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.toPersistentList + +internal class YieldBalancesConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: Unit): InnerYieldBalanceState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + + val cryptoCurrency = cryptoCurrencyStatus.currency + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + + return if (yieldBalance is YieldBalance.Data) { + val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() + val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) + val groupedBalances = getGroupedBalance(yieldBalance.balance) + + InnerYieldBalanceState.Data( + rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoRewardsValue, + cryptoCurrency = cryptoCurrency, + ), + rewardsFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRewardsValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), + balance = groupedBalances, + ) + } else { + InnerYieldBalanceState.Empty + } + } + + private fun getGroupedBalance(balance: YieldBalanceItem) = balance.items + // todo remove when real data is available + .addOrReplace( + item = balance.items.first().copy( + type = BalanceType.REWARDS, + ), + predicate = { true }, + ) + .groupBy { it.type.toGroup() } + .mapNotNull { item -> + val (title, footer) = getGroupTitle(item.key) + title?.let { + BalanceGroupedState( + items = item.value.mapBalances().toPersistentList(), + footer = footer, + title = it, + ) + } + } + + private fun List.mapBalances(): List { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + return this.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + balance.validatorAddress?.contains(it.address, ignoreCase = true) == true + } + val cryptoAmount = balance.amount * balance.pricePerShare + val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) + + validator?.let { + BalanceState( + validator = validator, + cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), + cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + rawCurrencyId = balance.rawCurrencyId, + ) + } + } + } + + private fun BalanceType.toGroup() = when (this) { + BalanceType.PREPARING, + BalanceType.STAKED, + BalanceType.REWARDS, + BalanceType.AVAILABLE, + BalanceType.LOCKED, + -> BalanceGroupType.ACTIVE + BalanceType.UNSTAKING, + BalanceType.UNLOCKING, + BalanceType.UNSTAKED, + -> BalanceGroupType.UNSTAKED + BalanceType.UNKNOWN, + -> BalanceGroupType.UNKNOWN + } + + private fun getGroupTitle(type: BalanceGroupType) = when (type) { + BalanceGroupType.ACTIVE -> resourceReference( + R.string.staking_active, + ) to resourceReference( + R.string.staking_active_footer, + ) + BalanceGroupType.UNSTAKED -> resourceReference( + R.string.staking_unstaked, + ) to resourceReference( + R.string.staking_unstaked_footer, + ) + BalanceGroupType.UNKNOWN -> null to null + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index f49e7535d0..4eadd4916a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -1,12 +1,12 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates +import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { val defaultState = StakingStates.InitialInfoState.Data( @@ -30,11 +30,11 @@ internal object InitialStakingStatePreview { isRewardsToClaim = false, balance = listOf( BalanceGroupedState( - type = BalanceType.STAKED, title = stringReference("Staked"), footer = null, - items = listOf( + items = persistentListOf( BalanceState( + cryptoValue = "100", cryptoAmount = stringReference("100 SOL"), fiatAmount = stringReference("100 $"), rawCurrencyId = null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 8a120d3f9a..6320dce75c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -27,4 +27,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun openValidators() {} override fun onValidatorSelect(validator: Yield.Validator) {} + + override fun openRewardsValidators() {} + + override fun selectRewardValidator(rewardValue: String) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 0438982aff..9e1341b01c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -9,19 +9,21 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.YieldBalance -import com.tangem.domain.staking.model.YieldBalanceItem import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter +import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero -import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import java.math.BigDecimal @@ -45,6 +47,14 @@ internal class SetInitialDataStateTransformer( ) } + private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { + RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + + private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { + YieldBalancesConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( clickIntents = clickIntents, @@ -52,6 +62,8 @@ internal class SetInitialDataStateTransformer( initialInfoState = createInitialInfoState(), amountState = createInitialAmountState(), confirmStakingState = createInitialConfirmationState(), + rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit), + bottomSheetConfig = null, ) } @@ -78,7 +90,7 @@ internal class SetInitialDataStateTransformer( warmupPeriod = yield.metadata.warmupPeriod.days.toString(), rewardSchedule = yield.metadata.rewardSchedule, onInfoClick = clickIntents::onInfoClick, - yieldBalance = getStakedBalances(cryptoCurrencyStatus), + yieldBalance = yieldBalancesConverter.convert(Unit), ) } @@ -116,96 +128,6 @@ internal class SetInitialDataStateTransformer( return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr)) } - private fun getStakedBalances(cryptoCurrencyStatus: CryptoCurrencyStatus): InnerYieldBalanceState { - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance - return if (yieldBalance is YieldBalance.Data) { - val appCurrency = appCurrencyProvider() - val cryptoCurrency = cryptoCurrencyStatus.currency - val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() - val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) - val groupedBalances = getGroupedBalance(yieldBalance.balance, cryptoCurrencyStatus, appCurrency) - - InnerYieldBalanceState.Data( - rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoRewardsValue, - cryptoCurrency = cryptoCurrency, - ), - rewardsFiat = BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatRewardsValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), - balance = groupedBalances, - ) - } else { - InnerYieldBalanceState.Empty - } - } - - private fun getGroupedBalance( - balance: YieldBalanceItem, - cryptoCurrencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ) = balance.items - .sortedByDescending { it.type } - .groupBy { it.type } - .mapNotNull { item -> - val (title, footer) = getValidatorBalanceInto(item.key) - - val balances = item.value.mapNotNull { balance -> - val validator = yield.validators.firstOrNull { - balance.validatorAddress?.contains(it.address, ignoreCase = true) == true - } - val cryptoAmount = balance.amount * balance.pricePerShare - val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - - validator?.let { - BalanceState( - validator = validator, - cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = balance.amount, - cryptoCurrency = cryptoCurrencyStatus.currency, - ), - ), - fiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), - ), - rawCurrencyId = balance.rawCurrencyId, - ) - } - } - - title?.let { - BalanceGroupedState( - items = balances, - type = item.key, - footer = footer, - title = it, - ) - } - } - - private fun getValidatorBalanceInto(type: BalanceType) = when (type) { - BalanceType.PREPARING, - BalanceType.STAKED, - -> resourceReference(R.string.staking_active) to resourceReference(R.string.staking_active_footer) - BalanceType.UNSTAKING, - BalanceType.UNLOCKING, - BalanceType.UNSTAKED, - -> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer) - BalanceType.AVAILABLE, - BalanceType.LOCKED, - BalanceType.UNKNOWN, - BalanceType.REWARDS, - -> null to null - } - companion object { private val EQUALITY_THRESHOLD = BigDecimal(1E-10) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt new file mode 100644 index 0000000000..64a2f51b56 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -0,0 +1,73 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.inputrow.InputRowImageInfo +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.extensions.orZero + +@Composable +internal fun StakingClaimRewardsValidatorContent( + state: StakingStates.RewardsValidatorsState, + clickIntents: StakingClickIntents, + modifier: Modifier = Modifier, +) { + if (state !is StakingStates.RewardsValidatorsState.Data) return + Column( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing12) + .verticalScroll(rememberScrollState()), + ) { + state.rewards.forEachIndexed { index, item -> + key(item.validator.address) { + InputRowImageInfo( + subtitle = stringReference(item.validator.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + appendSpace() + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent( + percent = item.validator.apr.orZero(), + useAbsoluteValue = true, + ), + ) + } + }, + ), + ), + infoTitle = item.fiatAmount, + infoSubtitle = item.cryptoAmount, + imageUrl = item.validator.image.orEmpty(), + modifier = modifier + .roundedShapeItemDecoration(index, state.rewards.lastIndex, false) + .background(TangemTheme.colors.background.action) + .clickable( + onClick = { + clickIntents.selectRewardValidator(item.cryptoValue) + }, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 81730a1913..5ca7d71e8e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -4,13 +4,18 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -34,12 +39,14 @@ import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Strings.DOT import com.tangem.utils.extensions.orZero @Composable -internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { +internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) { if (state !is StakingStates.InitialInfoState.Data) return Column( @@ -63,6 +70,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) { rewardCrypto = it.rewardsCrypto, rewardFiat = it.rewardsFiat, isRewardsToClaim = it.isRewardsToClaim, + onRewardsClick = clickIntents::openRewardsValidators, ) } } @@ -188,7 +196,12 @@ internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { } @Composable -private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewardsToClaim: Boolean) { +private fun StakingRewardBlock( + rewardCrypto: String, + rewardFiat: String, + isRewardsToClaim: Boolean, + onRewardsClick: () -> Unit, +) { val (text, textColor) = if (isRewardsToClaim) { annotatedReference( buildAnnotatedString { @@ -211,37 +224,46 @@ private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewar textColor = textColor, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action), + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onRewardsClick, + ), ) } @Composable -private fun ActiveStakingBlock(groupes: List) { +private fun ActiveStakingBlock(groups: List) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - groupes.forEach { group -> - FooterContainer( - footer = group.footer?.resolveReference(), - modifier = Modifier, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action), + groups.forEach { group -> + key(group.title) { + FooterContainer( + footer = group.footer?.resolveReference(), + modifier = Modifier, ) { - group.items.forEachIndexed { index, balance -> - InputRowImageInfo( - title = group.title.takeIf { index == 0 }, - subtitle = stringReference(balance.validator.name), - caption = stringReference( - BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), - ), - infoTitle = balance.fiatAmount, - infoSubtitle = balance.cryptoAmount, - imageUrl = balance.validator.image.orEmpty(), - ) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + group.items.forEachIndexed { index, balance -> + key(balance.validator.address) { + InputRowImageInfo( + title = group.title.takeIf { index == 0 }, + subtitle = stringReference(balance.validator.name), + caption = stringReference( + BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), + ), + infoTitle = balance.fiatAmount, + infoSubtitle = balance.cryptoAmount, + imageUrl = balance.validator.image.orEmpty(), + ) + } + } } } } @@ -278,6 +300,7 @@ private fun StakingInitialInfoContent_Preview( TangemThemePreview { StakingInitialInfoContent( state = feeState, + clickIntents = StakingClickIntentsStub, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index e9c29e3cb1..1ee7aca2ac 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -1,8 +1,6 @@ package com.tangem.features.staking.impl.presentation.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -84,22 +82,29 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier SpacerW12() } } - TangemButton( - text = stringResource(buttonTextId), - icon = buttonIcon, - enabled = isButtonEnabled, - onClick = { - if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - buttonClick() - }, - showProgress = false, - modifier = Modifier.fillMaxWidth(), - colors = TangemButtonsDefaults.primaryButtonColors, - ) + + AnimatedVisibility( + visible = buttonClick != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled && buttonClick != null, + onClick = { + if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + if (buttonClick != null) buttonClick() + }, + showProgress = false, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } } } -private fun getButtonData(currentState: StakingUiState): Pair Unit> { +private fun getButtonData(currentState: StakingUiState): Pair Unit)?> { return when (currentState.currentStep) { StakingStep.InitialInfo -> { val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data @@ -114,11 +119,13 @@ private fun getButtonData(currentState: StakingUiState): Pair Unit> { StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick + else -> R.string.common_next to null } } private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { StakingStep.InitialInfo, + StakingStep.RewardsValidators, StakingStep.Confirm, StakingStep.Success, -> false @@ -132,6 +139,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled StakingStep.Success -> true StakingStep.Validators -> true } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index d53e68d30a..1b92f7fcea 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -65,6 +65,7 @@ private fun SendAppBar(uiState: StakingUiState) { val titleRes = when (uiState.currentStep) { StakingStep.Amount -> stringResource(id = R.string.send_amount_label) StakingStep.InitialInfo, + StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Confirm, -> stringResource(id = R.string.common_stake) @@ -78,7 +79,9 @@ private fun SendAppBar(uiState: StakingUiState) { -> { R.drawable.ic_close_24 } - StakingStep.InitialInfo -> { + StakingStep.RewardsValidators, + StakingStep.InitialInfo, + -> { R.drawable.ic_back_24 } } @@ -137,7 +140,14 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M when (state) { StakingStep.InitialInfo -> StakingInitialInfoContent( state = uiState.initialInfoState, + clickIntents = uiState.clickIntents, ) + StakingStep.RewardsValidators -> { + StakingClaimRewardsValidatorContent( + state = uiState.rewardsValidatorsState, + clickIntents = uiState.clickIntents, + ) + } StakingStep.Amount -> AmountScreenContent( amountState = uiState.amountState, isBalanceHiding = uiState.isBalanceHidden, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index d89142ec38..62f5fc6c5c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -19,4 +19,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun openValidators() fun onValidatorSelect(validator: Yield.Validator) + + fun openRewardsValidators() + + fun selectRewardValidator(rewardValue: String) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index ef08dd78bb..23bdae1705 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -21,9 +21,9 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter -import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer @@ -89,22 +89,8 @@ internal class StakingViewModel @Inject constructor( override fun onNextClick() { stakingStateRouter.onNextClick() - when (value.currentStep) { - StakingStep.Confirm -> { - initStaking() - } - StakingStep.InitialInfo -> { - // TODO staking - } - StakingStep.Amount -> { - // TODO staking - } - StakingStep.Success -> { - // TODO staking - } - StakingStep.Validators -> { - // TODO staking - } + if (value.currentStep == StakingStep.Confirm) { + initStaking() } } @@ -175,6 +161,15 @@ internal class StakingViewModel @Inject constructor( stateController.update(ValidatorSelectChangeTransformer(validator)) } + override fun openRewardsValidators() { + stakingStateRouter.showRewardsValidators() + } + + override fun selectRewardValidator(rewardValue: String) { + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue)) + stakingStateRouter.onNextClick() + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter From 679469c91f9759e66fc6cc789576629bf8acdd46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 15:14:19 +0300 Subject: [PATCH 101/223] Updated on 2026-08-14 --- .../presentation/state/StakingStateRouter.kt | 3 +- .../impl/presentation/state/StakingUiState.kt | 12 +++- .../ConfirmStakingStatePreviewData.kt | 3 +- ...t => SetConfirmStateConfirmTransformer.kt} | 4 +- .../SetConfirmStateInProgressTransformer.kt | 25 +++++++ ...t => SetConfirmStateLoadingTransformer.kt} | 7 +- .../viewmodel/StakingViewModel.kt | 70 ++++++++++++++++++- 7 files changed, 110 insertions(+), 14 deletions(-) rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/{SetConfirmStateDataStateTransformer.kt => SetConfirmStateConfirmTransformer.kt} (91%) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/{SetConfirmLoadingStateTransformer.kt => SetConfirmStateLoadingTransformer.kt} (89%) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 921430000b..5f277ce4a4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -68,6 +68,7 @@ internal class StakingStateRouter( } fun showSuccess() { - stateController.update { it.copy(currentStep = StakingStep.Success) } + // stateController.update { it.copy(currentStep = StakingStep.Success) } + // TODO staking [REDACTED_TASK_KEY] } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 8d952f02e4..65fe669de8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -82,9 +82,15 @@ internal sealed class StakingStates { val validatorState: ValidatorState, val notifications: ImmutableList, val footerText: String, - val isSuccess: Boolean, - val isStaking: Boolean, - ) : ConfirmStakingState() + val innerState: InnerConfirmStakingState, + ) : ConfirmStakingState() { + + enum class InnerConfirmStakingState { + CONFIRM, + IN_PROGRESS, + SUCCESS, + } + } data class Empty( override val isPrimaryButtonEnabled: Boolean = false, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt index 161f39ee33..34916ee421 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt @@ -82,7 +82,6 @@ internal object ConfirmStakingStatePreviewData { days = 2, ), ), - isStaking = false, - isSuccess = false, + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt similarity index 91% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt index d37ad3406b..f058a772c3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.utils.transformer.Transformer import com.tangem.blockchain.common.Amount @Suppress("UnusedPrivateMember") -internal class SetConfirmStateDataStateTransformer( +internal class SetConfirmStateConfirmTransformer( private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val stakingGasEstimate: StakingGasEstimate, @@ -42,6 +42,8 @@ internal class SetConfirmStateDataStateTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), + isPrimaryButtonEnabled = true, + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, ) } else { return this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt new file mode 100644 index 0000000000..edad01bfea --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class SetConfirmStateInProgressTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmStakingState = prevState.confirmStakingState.copyWrapped(), + ) + } + + private fun StakingStates.ConfirmStakingState.copyWrapped(): StakingStates.ConfirmStakingState { + return if (this is StakingStates.ConfirmStakingState.Data) { + copy( + isPrimaryButtonEnabled = false, + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.IN_PROGRESS, + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt similarity index 89% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt index 76ff266521..838a5db5cd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmLoadingStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt @@ -10,14 +10,14 @@ import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf @Suppress("UnusedPrivateMember") -internal class SetConfirmLoadingStateTransformer( +internal class SetConfirmStateLoadingTransformer( private val yield: Yield, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val possibleConfirmStakingState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data val possibleValidatorState = possibleConfirmStakingState?.validatorState as? ValidatorState.Content - val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0] // TODO staking add sorting + val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0] return prevState.copy( confirmStakingState = StakingStates.ConfirmStakingState.Data( @@ -34,8 +34,7 @@ internal class SetConfirmLoadingStateTransformer( ), ), footerText = "", - isSuccess = false, - isStaking = false, // TODO staking move to enum + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 23bdae1705..7b76ca2694 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionData +import com.tangem.common.extensions.hexToBytes import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.models.AmountState @@ -14,9 +16,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.InitializeStakingProcessUseCase import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -24,6 +28,7 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer @@ -36,6 +41,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates @@ -49,6 +55,7 @@ internal class StakingViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -58,6 +65,8 @@ internal class StakingViewModel @Inject constructor( var stakingStateRouter: StakingStateRouter by Delegates.notNull() private set + private var stakingTransaction: StakingTransaction? = null + private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle.get(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY) ?.unbundle(CryptoCurrency.ID.serializer()) @@ -88,16 +97,30 @@ internal class StakingViewModel @Inject constructor( } override fun onNextClick() { + handleOnNextConfirmClick() stakingStateRouter.onNextClick() if (value.currentStep == StakingStep.Confirm) { initStaking() } } + private fun handleOnNextConfirmClick() { + if (value.currentStep == StakingStep.Confirm && + (value.confirmStakingState as? StakingStates.ConfirmStakingState.Data)?.innerState == + StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM + ) { + viewModelScope.launch { + stakingTransaction?.unsignedTransaction?.let { + sendStakingTransaction(TransactionData.Compiled(value = it.hexToBytes())) + } ?: error("No unsigned transaction available") + } + } + } + private fun initStaking() { viewModelScope.launch { stateController.update( - SetConfirmLoadingStateTransformer( + SetConfirmStateLoadingTransformer( yield = yield, ), ) @@ -111,14 +134,17 @@ internal class StakingViewModel @Inject constructor( validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), token = yield.token, ) + val (enterAction, stakingTransaction) = actionWithTransaction.getOrElse { - error("Can't retrieve action and transaction") + error(it) } + this@StakingViewModel.stakingTransaction = stakingTransaction + val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info") stateController.update( - SetConfirmStateDataStateTransformer( + SetConfirmStateConfirmTransformer( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = stakingGasEstimate, @@ -226,4 +252,42 @@ internal class StakingViewModel @Inject constructor( .flowOn(dispatchers.main) .launchIn(viewModelScope) } + + private suspend fun sendStakingTransaction(txData: TransactionData) { + stateController.update(SetConfirmStateInProgressTransformer()) + + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrencyStatus.currency.network, + ).fold( + ifLeft = { error -> + Timber.e(error.toString()) + val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold + stateController.update( + SetConfirmStateConfirmTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = gasEstimate, + ), + ) + // uiState = eventStateFactory.getSendTransactionErrorState( + // error = error, + // onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + // ) + // analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + }, + ifRight = { + val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold + stateController.update( + SetConfirmStateConfirmTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = gasEstimate, + ), + ) + // sendScreenAnalyticSender.sendTransaction() + }, + ) + } } \ No newline at end of file From 8737ebd2911323e8b5d8ea89cf006ecc911fbab9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 14:09:16 +0300 Subject: [PATCH 102/223] Updated on 2026-08-14 --- core/pagination/.gitignore | 1 + core/pagination/build.gradle.kts | 10 + .../main/java/com/tangem/pagination/Batch.kt | 13 + .../java/com/tangem/pagination/BatchAction.kt | 61 ++++ .../com/tangem/pagination/BatchFetchResult.kt | 37 ++ .../pagination/BatchFetchUpdateResult.kt | 36 ++ .../com/tangem/pagination/BatchFetcher.kt | 21 ++ .../com/tangem/pagination/BatchListSource.kt | 343 ++++++++++++++++++ .../com/tangem/pagination/BatchListState.kt | 18 + .../com/tangem/pagination/BatchRequest.kt | 18 + .../tangem/pagination/BatchUpdateFetcher.kt | 25 ++ .../com/tangem/pagination/BatchingConfig.kt | 12 + .../com/tangem/pagination/BatchingContext.kt | 22 ++ .../com/tangem/pagination/PaginationStatus.kt | 56 +++ settings.gradle.kts | 1 + 15 files changed, 674 insertions(+) create mode 100644 core/pagination/.gitignore create mode 100644 core/pagination/build.gradle.kts create mode 100644 core/pagination/src/main/java/com/tangem/pagination/Batch.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt diff --git a/core/pagination/.gitignore b/core/pagination/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/pagination/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/pagination/build.gradle.kts b/core/pagination/build.gradle.kts new file mode 100644 index 0000000000..c7b5d3d97a --- /dev/null +++ b/core/pagination/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + // region Coroutines + implementation(deps.kotlin.coroutines) + // endregion +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt new file mode 100644 index 0000000000..b4b2482eaa --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt @@ -0,0 +1,13 @@ +package com.tangem.pagination + +/** + * Represents a batch of data with a key. + * Used in [BatchListState]. + * + * @param K type of the key. + * @param T type of the data. + */ +data class Batch( + val key: K, + val data: T, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt new file mode 100644 index 0000000000..710e191dd7 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -0,0 +1,61 @@ +package com.tangem.pagination + +/** + * Action that can be dispatched to [BatchListSource]. + * + * @param R type of the request to load batches. + * @param K type of the key of the batch. + * @param U type of the update request. + */ +sealed class BatchAction { + + /** + * Action to load the first batch. + * + * @param request request to load the first batch. + */ + data class Reload( + val request: R, + ) : BatchAction() + + /** + * Action to load the next batch. + * + * @param request request to load the next batch with new request. + * If null, the last request will be used. + * Will be saved in the state and used for future LoadMore actions with request = null. + */ + data class LoadMore( + val request: R? = null, + ) : BatchAction() + + /** + * Action to update the batch. + * + * @param keys keys of the batches to update. + * @param request request to update the batches. + */ + class UpdateBatches( + val keys: Set, + val request: U, + ) : BatchAction() + + /** + * Action to cancel the current batch loading. + */ + data object CancelBatchLoading : BatchAction() + + /** + * Action to cancel all update requests. + */ + data object CancelAllUpdates : BatchAction() + + /** + * Action to cancel update requests that satisfy the predicate. + * + * @param predicate predicate to check if the update request should be cancelled. + */ + class CancelUpdates( + val predicate: (UpdateBatches) -> Boolean, + ) : BatchAction() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt new file mode 100644 index 0000000000..d4ea1d4ae7 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -0,0 +1,37 @@ +package com.tangem.pagination + +/** + * Represents a result of a batch fetch request. + * Used in [BatchListState]. + * + * @param T type of the data. + * @param E type of the error. + */ +sealed class BatchFetchResult { + + /** + * Represents a successful result of a batch fetch request. + * + * @param data fetched data. + * @param last indicates if this is the last batch for the request. + */ + data class Success( + val data: T, + val last: Boolean = false, + ) : BatchFetchResult() + + /** + * Represents an error result of a batch fetch request. + * + * @param error error that occurred during the request. + */ + data class Error(val error: E) : BatchFetchResult() + + /** + * Represents an unknown error result of a batch fetch request. + * Used for unexpected exceptions that occurred in fetch method in [BatchFetcher]. + * + * @param throwable throwable that occurred during the request. + */ + class UnknownError(val throwable: Throwable) : BatchFetchResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt new file mode 100644 index 0000000000..7d47ed8142 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt @@ -0,0 +1,36 @@ +package com.tangem.pagination + +/** + * Represents the result of a batch fetch operation. + * Used in [BatchListState] and [BatchUpdateFetcher]. + * + * @param TKey type of the key. + * @param TData type of the data. + * @param TError type of the error. + */ +sealed class BatchFetchUpdateResult { + + /** + * Represents a successful result of a batch update operation. + * + * @param data fetched data. + */ + data class Success( + val data: List>, + ) : BatchFetchUpdateResult() + + /** + * Represents an error result of a batch update operation. + * + * @param error error that occurred during the operation. + */ + data class Error(val error: TError) : BatchFetchUpdateResult() + + /** + * Represents an unknown error result of a batch update operation. + * Used for unexpected exceptions that occurred in `fetchUpdate` method in [BatchUpdateFetcher]. + * + * @param throwable throwable that occurred during the operation. + */ + class UnknownError(val throwable: Throwable) : BatchFetchUpdateResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt new file mode 100644 index 0000000000..aea9c1bd37 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt @@ -0,0 +1,21 @@ +package com.tangem.pagination + +/** + * Interface for fetching a batch of data. Used in [BatchListState]. + * + * @param TRequest type of the request. + * @param TData type of the data. + * @param TError type of the error. + * + * @see BatchListState + */ +interface BatchFetcher { + + /** + * Fetches a batch of data. + * + * @param request request to fetch the data. + * @return result of the fetch operation. + */ + suspend fun fetch(request: BatchRequest): BatchFetchResult +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt new file mode 100644 index 0000000000..a3e529d029 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -0,0 +1,343 @@ +package com.tangem.pagination + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* + +/** + * Source for paginated data. The starting point for pagination. + * + * @param TKey Type of the key that identifies a batch. + * @param TData Type of the data in the batch. Generally, it' a list of items. + * @param TUpdate Type of the update request. + * @param TError Type of the error that can occur during fetching or updating. + * @property state State of the paginated data. + * @property updateResults Flow of results of update requests. + */ +interface BatchListSource { + val state: StateFlow> + val updateResults: SharedFlow>> +} + +/** + * Creates a new [BatchListSource] with the provided configuration. + * + * @param config Configuration for batching. + * @param context Context for batching. + * @param generateNewKey Function to generate a new key for a batch. + * @param batchFetcher Function to fetch a batch of data. + * + * @return New instance of [BatchListSource]. + */ +@Suppress("FunctionNaming") +fun BatchListSource( + config: BatchingConfig, + context: BatchingContext, + generateNewKey: suspend (List) -> TKey, + batchFetcher: BatchFetcher, +): BatchListSource = + BatchListSourceImpl(config, context, generateNewKey, batchFetcher, null) + +/** + * Creates a new [BatchListSource] with the provided configuration. + * + * @param config Configuration for batching. + * @param context Context for batching. + * @param generateNewKey Function to generate a new key for a batch. + * @param batchFetcher Function to fetch a batch of data. + * @param updateFetcher Function to fetch updates for batches. + * + * @return New instance of [BatchListSource]. + */ +@Suppress("FunctionNaming") +fun BatchListSource( + config: BatchingConfig, + context: BatchingContext, + generateNewKey: suspend (List) -> TKey, + batchFetcher: BatchFetcher, + updateFetcher: BatchUpdateFetcher, +): BatchListSource = + BatchListSourceImpl(config, context, generateNewKey, batchFetcher, updateFetcher) + +private class BatchListSourceImpl( + private val config: BatchingConfig, + private val context: BatchingContext, + private val generateNewKey: suspend (List) -> TKey, + private val batchFetcher: BatchFetcher, + private val updateFetcher: BatchUpdateFetcher? = null, +) : BatchListSource { + + override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) + override val updateResults = MutableSharedFlow>>( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private val scope = context.coroutineScope + private val updateJobs = MutableStateFlow, Job>>>(emptyList()) + private val waitingUpdateJobs = + MutableStateFlow, Job>>>(emptyList()) + + private val lastRequest = MutableStateFlow?>(null) + private var reloadActionJob: Job? = null + private var loadMoreActionJob: Job? = null + + init { + scope.launch { + try { + awaitCancellation() + } finally { + withContext(NonCancellable) { + lastRequest.value = null + loadMoreActionJob = null + loadMoreActionJob = null + lastRequest.value = null + stopAllUpdates() + state.value = BatchListState(emptyList(), PaginationStatus.None) + } + } + } + + scope.launch { + context.actionsFlow.collect { action -> + collectActions(action) + } + } + } + + private fun collectActions(action: BatchAction) { + when (action) { + is BatchAction.Reload -> { + // Stop all tasks + loadMoreActionJob?.cancel() + reloadActionJob?.cancel() + stopAllUpdates() + reloadActionJob = scope.launch(Dispatchers.IO) { + reloadTask(action) + } + } + is BatchAction.LoadMore -> { + if (loadMoreActionJob?.isActive == true) { + return + } + + loadMoreActionJob = scope.launch(Dispatchers.IO) { + reloadActionJob?.join() + loadMoreTask(action) + } + } + is BatchAction.UpdateBatches -> { + if (updateFetcher == null) return + + scope.launch(Dispatchers.IO) { + val job = launch(start = CoroutineStart.LAZY) { + updateBatchesTask(action) + } + + val actionJob = action to job + + waitingUpdateJobs.update { it + actionJob } + + updateJobs.first { workingJobs -> + action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() + } + + waitingUpdateJobs.update { it - actionJob } + + val started = job.start() + + if (started) { + updateJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + if (cause !is CancellationException) { + updateJobs.update { it - actionJob } + } + } + } + } + } + BatchAction.CancelAllUpdates -> { + if (updateFetcher == null) return + stopAllUpdates() + } + is BatchAction.CancelUpdates -> { + if (updateFetcher == null) return + stopUpdates(action.predicate) + } + BatchAction.CancelBatchLoading -> { + loadMoreActionJob?.cancel() + reloadActionJob?.cancel() + } + } + } + + private suspend fun reloadTask(action: BatchAction.Reload) { + state.value = BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoading, + ) + + val requestData = action.request + val request = BatchRequest(offset = 0, limit = config.batchSize, requestData) + + val res = runCatching { + batchFetcher.fetch(request) + }.getOrElse { BatchFetchResult.UnknownError(it) } + + state.value = if (res is BatchFetchResult.Success) { + val key = generateNewKey(listOf()) + val batch = Batch( + key = key, + data = res.data, + ) + BatchListState( + data = listOf(batch), + status = if (res.last) { + PaginationStatus.EndOfPagination + } else { + PaginationStatus.Paginating(res) + }, + ) + } else { + BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoadingError( + error = (res as? BatchFetchResult.Error)?.error, + ), + ) + } + + lastRequest.value = request + } + + private suspend fun loadMoreTask(action: BatchAction.LoadMore) { + val status = state.value.status + val lastReq = lastRequest.value + + val request: BatchRequest = when { + // try to continue pagination with new request + status is PaginationStatus.EndOfPagination && action.request != null && action.request != lastReq -> { + requireNotNull(lastReq) + + BatchRequest( + offset = lastReq.offset + lastReq.limit, + limit = config.batchSize, + data = action.request, + ) + } + // continue pagination + status is PaginationStatus.Paginating -> { + requireNotNull(lastReq) + + if (status.lastResult is BatchFetchResult.Success) { + BatchRequest( + offset = lastReq.offset + lastReq.limit, + limit = config.batchSize, + data = action.request ?: lastReq.data, + ) + } else { + BatchRequest( + offset = lastReq.offset, + limit = lastReq.limit, + data = action.request ?: lastReq.data, + ) + } + } + else -> return + } + + state.update { it.copy(status = PaginationStatus.NextBatchLoading) } + + val res = runCatching { + batchFetcher.fetch(request) + }.getOrElse { BatchFetchResult.UnknownError(it) } + + lastRequest.value = request + + state.update { currentState -> + if (res is BatchFetchResult.Success) { + val newBatch = Batch( + key = generateNewKey(currentState.data.map { it.key }), + data = res.data, + ) + + currentState.copy( + data = currentState.data + newBatch, + status = if (res.last) { + PaginationStatus.EndOfPagination + } else { + PaginationStatus.Paginating(res) + }, + ) + } else { + currentState.copy( + status = PaginationStatus.Paginating(res), + ) + } + } + } + + private suspend fun updateBatchesTask(action: BatchAction.UpdateBatches) { + if (updateFetcher == null) return + + val batches = state.value.data + val batchesToUpdate = batches.filter { action.keys.contains(it.key) } + + val result = updateFetcher.fetchUpdate( + toUpdate = batchesToUpdate, + updateRequest = action.request, + ) + + if (result is BatchFetchUpdateResult.Success) { + state.update { currentState -> + val resMap = result.data.associateBy { it.key } + currentState.copy( + data = currentState.data.map { + resMap[it.key] ?: it + }, + ) + } + } + + updateResults.emit(action.request to result) + } + + private fun stopAllUpdates() { + updateJobs.update { actionJobs -> + waitingUpdateJobs.update { waitingActionJobs -> + waitingActionJobs.forEach { + it.second.cancel() + } + emptyList() + } + actionJobs.forEach { + it.second.cancel() + } + emptyList() + } + } + + private fun stopUpdates(predicate: (BatchAction.UpdateBatches) -> Boolean) { + updateJobs.update { actionJobs -> + waitingUpdateJobs.update { waitingActionJobs -> + waitingActionJobs.mapNotNull { + if (predicate(it.first)) { + it.second.cancel() + null + } else { + it + } + } + } + actionJobs.mapNotNull { + if (predicate(it.first)) { + it.second.cancel() + null + } else { + it + } + } + } + } +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt new file mode 100644 index 0000000000..adce142dab --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -0,0 +1,18 @@ +package com.tangem.pagination + +/** + * State that is used for listening the current state of a pagination. + * + * @param K type of the key of the batch. + * @param T type of the data. + * @param E type of the error. + * + * @property data list of loaded batches. + * @property status current status of the pagination. + * + * @see BatchListSource + */ +data class BatchListState( + val data: List>, + val status: PaginationStatus, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt new file mode 100644 index 0000000000..6710c62702 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt @@ -0,0 +1,18 @@ +package com.tangem.pagination + +/** + * Request to fetch a batch of data. + * + * @param R type of the request. + * + * @property offset offset for the request. + * @property limit limit for the request. + * @property data body of the request. + * + * @see BatchFetcher + */ +data class BatchRequest( + val offset: Int, + val limit: Int, + val data: R, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt new file mode 100644 index 0000000000..71ddd43c76 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -0,0 +1,25 @@ +package com.tangem.pagination + +/** + * Interface for fetching updates for a batch of data. + * Used in [BatchListState]. + * + * @param TKey type of the key. + * @param TData type of the data. + * @param TError type of the error. + * @param TUpdate type of the update request. + */ +interface BatchUpdateFetcher { + + /** + * Fetches updates for a batch of data. + * + * @param toUpdate list of batches to update. + * @param updateRequest request to update the data. + * @return result of the update operation. + */ + suspend fun fetchUpdate( + toUpdate: List>, + updateRequest: TUpdate, + ): BatchFetchUpdateResult +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt new file mode 100644 index 0000000000..8789d11405 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.pagination + +/** + * Configuration for batching. + * + * @param batchSize size of the batch. + * @param prefetchDistance number of items to fetch for the first batch. + */ +data class BatchingConfig( + val batchSize: Int, + val prefetchDistance: Int = batchSize, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt new file mode 100644 index 0000000000..95f10045c4 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -0,0 +1,22 @@ +package com.tangem.pagination + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow + +/** + * Context for working with [BatchListSource]. + * + * @param R type of the request. + * @param K type of the key. + * @param U type of the update request. + * + * @property actionsFlow flow of [BatchAction]s that would be dispatched to [BatchListSource]. + * @property coroutineScope scope for the [BatchListSource] to launch coroutines. When it is cancelled, + * all the operations and requests launched in the [BatchListSource] would be cancelled and all data would be cleared. + * + * @see BatchListSource + */ +class BatchingContext( + val actionsFlow: Flow>, + val coroutineScope: CoroutineScope, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt new file mode 100644 index 0000000000..054178ce19 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -0,0 +1,56 @@ +package com.tangem.pagination + +/** + * Status of the pagination. + * + * @param T type of the data. + * @param E type of the error. + * + * @see BatchListState + */ +sealed class PaginationStatus { + + /** + * Represents that there is no data. Used when the list of batches is empty. + * The initial state of the pagination. + */ + data object None : PaginationStatus() + + /** + * Represents that the batch is loading for the first time. + * Used when the pagination is empty and the first batch is being loaded. + */ + data object InitialLoading : PaginationStatus() + + /** + * Represents that the first batch was loaded with an error. + * + * @param error error that occurred during the initial loading. + */ + data class InitialLoadingError( + val error: E?, + ) : PaginationStatus() + + /** + * Represents that the last batch was loaded and + * the source is ready to load the next one or reload previous if [lastResult] is an error. + * For the first batch, [lastResult] is always [BatchFetchResult.Success] + * + * @param lastResult result of the last batch fetch. + */ + data class Paginating( + val lastResult: BatchFetchResult, + ) : PaginationStatus() + + /** + * Represents that the next batch is loading. + * Used when the next batch is being loaded. + */ + data object NextBatchLoading : PaginationStatus() + + /** + * Represents that the source has no more batches to load. + * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.request] = null will be ignored. + */ + data object EndOfPagination : PaginationStatus() +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3637dd3cc9..6d1f411939 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,6 +127,7 @@ include(":core:utils") include(":core:deep-links") include(":core:deep-links:global") include(":core:decompose") +include(":core:pagination") // endregion Core modules // region Common modules From 5c51d8c3f16d3c500dbacc1b98564f876d44416e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 14:33:31 +0300 Subject: [PATCH 103/223] Updated on 2026-08-14 --- .../src/main/java/com/tangem/pagination/Batch.kt | 10 +++++----- .../main/java/com/tangem/pagination/BatchAction.kt | 8 ++++---- .../java/com/tangem/pagination/BatchFetchResult.kt | 14 +++++++------- .../java/com/tangem/pagination/BatchListState.kt | 12 ++++++------ .../java/com/tangem/pagination/BatchRequest.kt | 6 +++--- .../java/com/tangem/pagination/BatchingContext.kt | 10 +++++----- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt index b4b2482eaa..0e262776a2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt @@ -4,10 +4,10 @@ package com.tangem.pagination * Represents a batch of data with a key. * Used in [BatchListState]. * - * @param K type of the key. - * @param T type of the data. + * @param TKey type of the key. + * @param TData type of the data. */ -data class Batch( - val key: K, - val data: T, +data class Batch( + val key: TKey, + val data: TData, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 710e191dd7..4a4cfd7d71 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -3,11 +3,11 @@ package com.tangem.pagination /** * Action that can be dispatched to [BatchListSource]. * - * @param R type of the request to load batches. - * @param K type of the key of the batch. - * @param U type of the update request. + * @param TRequest type of the request to load batches. + * @param TKey type of the key of the batch. + * @param TUpdate type of the update request. */ -sealed class BatchAction { +sealed class BatchAction { /** * Action to load the first batch. diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index d4ea1d4ae7..03f87be764 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -4,10 +4,10 @@ package com.tangem.pagination * Represents a result of a batch fetch request. * Used in [BatchListState]. * - * @param T type of the data. - * @param E type of the error. + * @param TData type of the data. + * @param TError type of the error. */ -sealed class BatchFetchResult { +sealed class BatchFetchResult { /** * Represents a successful result of a batch fetch request. @@ -15,17 +15,17 @@ sealed class BatchFetchResult { * @param data fetched data. * @param last indicates if this is the last batch for the request. */ - data class Success( - val data: T, + data class Success( + val data: TData, val last: Boolean = false, - ) : BatchFetchResult() + ) : BatchFetchResult() /** * Represents an error result of a batch fetch request. * * @param error error that occurred during the request. */ - data class Error(val error: E) : BatchFetchResult() + data class Error(val error: TError) : BatchFetchResult() /** * Represents an unknown error result of a batch fetch request. diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt index adce142dab..525458362f 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -3,16 +3,16 @@ package com.tangem.pagination /** * State that is used for listening the current state of a pagination. * - * @param K type of the key of the batch. - * @param T type of the data. - * @param E type of the error. + * @param TKey type of the key of the batch. + * @param TData type of the data. + * @param TError type of the error. * * @property data list of loaded batches. * @property status current status of the pagination. * * @see BatchListSource */ -data class BatchListState( - val data: List>, - val status: PaginationStatus, +data class BatchListState( + val data: List>, + val status: PaginationStatus, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt index 6710c62702..b6c9545527 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt @@ -3,7 +3,7 @@ package com.tangem.pagination /** * Request to fetch a batch of data. * - * @param R type of the request. + * @param TRequest type of the request. * * @property offset offset for the request. * @property limit limit for the request. @@ -11,8 +11,8 @@ package com.tangem.pagination * * @see BatchFetcher */ -data class BatchRequest( +data class BatchRequest( val offset: Int, val limit: Int, - val data: R, + val data: TRequest, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt index 95f10045c4..37b6bd2c73 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -6,9 +6,9 @@ import kotlinx.coroutines.flow.Flow /** * Context for working with [BatchListSource]. * - * @param R type of the request. - * @param K type of the key. - * @param U type of the update request. + * @param TRequest type of the request. + * @param TKey type of the key. + * @param TUpdate type of the update request. * * @property actionsFlow flow of [BatchAction]s that would be dispatched to [BatchListSource]. * @property coroutineScope scope for the [BatchListSource] to launch coroutines. When it is cancelled, @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.Flow * * @see BatchListSource */ -class BatchingContext( - val actionsFlow: Flow>, +class BatchingContext( + val actionsFlow: Flow>, val coroutineScope: CoroutineScope, ) \ No newline at end of file From dc97dbd91fa4c77020843d38fc77f846d259a816 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 19:09:34 +0300 Subject: [PATCH 104/223] Updated on 2026-08-14 --- .../java/com/tangem/pagination/BatchAction.kt | 26 +++---- .../com/tangem/pagination/BatchFetchResult.kt | 3 +- .../com/tangem/pagination/BatchFetcher.kt | 21 ------ .../com/tangem/pagination/BatchListSource.kt | 65 ++++------------- .../com/tangem/pagination/BatchRequest.kt | 18 ----- .../exception/EndOfPaginationException.kt | 6 ++ .../tangem/pagination/fetcher/BatchFetcher.kt | 37 ++++++++++ .../fetcher/LimitOffsetBatchFetcher.kt | 69 +++++++++++++++++++ 8 files changed, 142 insertions(+), 103 deletions(-) delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 4a4cfd7d71..813cbe6ba9 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -14,9 +14,9 @@ sealed class BatchAction { * * @param request request to load the first batch. */ - data class Reload( - val request: R, - ) : BatchAction() + data class Reload( + val request: TRequest, + ) : BatchAction() /** * Action to load the next batch. @@ -25,9 +25,9 @@ sealed class BatchAction { * If null, the last request will be used. * Will be saved in the state and used for future LoadMore actions with request = null. */ - data class LoadMore( - val request: R? = null, - ) : BatchAction() + data class LoadMore( + val request: TRequest? = null, + ) : BatchAction() /** * Action to update the batch. @@ -35,10 +35,10 @@ sealed class BatchAction { * @param keys keys of the batches to update. * @param request request to update the batches. */ - class UpdateBatches( - val keys: Set, - val request: U, - ) : BatchAction() + class UpdateBatches( + val keys: Set, + val request: TUpdate, + ) : BatchAction() /** * Action to cancel the current batch loading. @@ -55,7 +55,7 @@ sealed class BatchAction { * * @param predicate predicate to check if the update request should be cancelled. */ - class CancelUpdates( - val predicate: (UpdateBatches) -> Boolean, - ) : BatchAction() + class CancelUpdates( + val predicate: (UpdateBatches) -> Boolean, + ) : BatchAction() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index 03f87be764..c3b8b03ee2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -29,9 +29,10 @@ sealed class BatchFetchResult { /** * Represents an unknown error result of a batch fetch request. - * Used for unexpected exceptions that occurred in fetch method in [BatchFetcher]. + * Used for unexpected exceptions that occurred in fetch method in BatchFetcher. * * @param throwable throwable that occurred during the request. + * @see com.tangem.pagination.fetcher.BatchFetcher */ class UnknownError(val throwable: Throwable) : BatchFetchResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt deleted file mode 100644 index aea9c1bd37..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetcher.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.pagination - -/** - * Interface for fetching a batch of data. Used in [BatchListState]. - * - * @param TRequest type of the request. - * @param TData type of the data. - * @param TError type of the error. - * - * @see BatchListState - */ -interface BatchFetcher { - - /** - * Fetches a batch of data. - * - * @param request request to fetch the data. - * @return result of the fetch operation. - */ - suspend fun fetch(request: BatchRequest): BatchFetchResult -} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index a3e529d029..7021d474c3 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -1,5 +1,6 @@ package com.tangem.pagination +import com.tangem.pagination.fetcher.BatchFetcher import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* @@ -30,13 +31,11 @@ interface BatchListSource { * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - config: BatchingConfig, +fun BatchListSource( context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, -): BatchListSource = - BatchListSourceImpl(config, context, generateNewKey, batchFetcher, null) +): BatchListSource = BatchListSourceImpl(context, generateNewKey, batchFetcher, null) /** * Creates a new [BatchListSource] with the provided configuration. @@ -50,17 +49,15 @@ fun BatchListSource( * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - config: BatchingConfig, +fun BatchListSource( context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, updateFetcher: BatchUpdateFetcher, ): BatchListSource = - BatchListSourceImpl(config, context, generateNewKey, batchFetcher, updateFetcher) + BatchListSourceImpl(context, generateNewKey, batchFetcher, updateFetcher) -private class BatchListSourceImpl( - private val config: BatchingConfig, +private class BatchListSourceImpl( private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, private val batchFetcher: BatchFetcher, @@ -78,7 +75,7 @@ private class BatchListSourceImpl( private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) - private val lastRequest = MutableStateFlow?>(null) + private val lastRequestResult = MutableStateFlow?>(null) private var reloadActionJob: Job? = null private var loadMoreActionJob: Job? = null @@ -88,10 +85,9 @@ private class BatchListSourceImpl( awaitCancellation() } finally { withContext(NonCancellable) { - lastRequest.value = null loadMoreActionJob = null loadMoreActionJob = null - lastRequest.value = null + lastRequestResult.value = null stopAllUpdates() state.value = BatchListState(emptyList(), PaginationStatus.None) } @@ -178,11 +174,8 @@ private class BatchListSourceImpl( status = PaginationStatus.InitialLoading, ) - val requestData = action.request - val request = BatchRequest(offset = 0, limit = config.batchSize, requestData) - val res = runCatching { - batchFetcher.fetch(request) + batchFetcher.fetchFirst(action.request) }.getOrElse { BatchFetchResult.UnknownError(it) } state.value = if (res is BatchFetchResult.Success) { @@ -208,52 +201,24 @@ private class BatchListSourceImpl( ) } - lastRequest.value = request + lastRequestResult.value = res } private suspend fun loadMoreTask(action: BatchAction.LoadMore) { val status = state.value.status - val lastReq = lastRequest.value - val request: BatchRequest = when { - // try to continue pagination with new request - status is PaginationStatus.EndOfPagination && action.request != null && action.request != lastReq -> { - requireNotNull(lastReq) + if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return + if (status is PaginationStatus.EndOfPagination && action.request == null) return - BatchRequest( - offset = lastReq.offset + lastReq.limit, - limit = config.batchSize, - data = action.request, - ) - } - // continue pagination - status is PaginationStatus.Paginating -> { - requireNotNull(lastReq) - - if (status.lastResult is BatchFetchResult.Success) { - BatchRequest( - offset = lastReq.offset + lastReq.limit, - limit = config.batchSize, - data = action.request ?: lastReq.data, - ) - } else { - BatchRequest( - offset = lastReq.offset, - limit = lastReq.limit, - data = action.request ?: lastReq.data, - ) - } - } - else -> return - } + val lastResult = lastRequestResult.value ?: return state.update { it.copy(status = PaginationStatus.NextBatchLoading) } val res = runCatching { - batchFetcher.fetch(request) + batchFetcher.fetchNext(action.request, lastResult) }.getOrElse { BatchFetchResult.UnknownError(it) } - lastRequest.value = request + lastRequestResult.value = lastResult state.update { currentState -> if (res is BatchFetchResult.Success) { diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt deleted file mode 100644 index b6c9545527..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchRequest.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.pagination - -/** - * Request to fetch a batch of data. - * - * @param TRequest type of the request. - * - * @property offset offset for the request. - * @property limit limit for the request. - * @property data body of the request. - * - * @see BatchFetcher - */ -data class BatchRequest( - val offset: Int, - val limit: Int, - val data: TRequest, -) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt b/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt new file mode 100644 index 0000000000..f2cc69767a --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt @@ -0,0 +1,6 @@ +package com.tangem.pagination.exception + +/** + * Exception that is thrown when there are no more items to fetch. + */ +class EndOfPaginationException : IllegalStateException() \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt new file mode 100644 index 0000000000..901d83f32b --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -0,0 +1,37 @@ +package com.tangem.pagination.fetcher + +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.BatchListState + +/** + * Interface for fetching a batch of data. Used in [BatchListState]. + * + * @param TRequest type of the request. + * @param TData type of the data. + * @param TError type of the error. + * + * @see BatchListState + */ +interface BatchFetcher { + + /** + * Fetches the first batch of data. + * + * @param request initial request. Will be saved to be used in [fetchNext] requests. + * @return result of the fetch operation. + */ + suspend fun fetchFirst(request: TRequest): BatchFetchResult + + /** + * Fetches the next batch of data. + * + * @param overrideRequest overrides current remembered request, even if that fetch fails. + * If null, the last request should be used. + * @param lastResult result of the last fetch operation. + * @return result of the fetch operation. + */ + suspend fun fetchNext( + overrideRequest: TRequest?, + lastResult: BatchFetchResult, + ): BatchFetchResult +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt new file mode 100644 index 0000000000..11af45665d --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -0,0 +1,69 @@ +package com.tangem.pagination.fetcher + +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.exception.EndOfPaginationException +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Fetcher that uses limit and offset to fetch data. + * + * @param TRequest type of the request. + * @param TData type of the data. + * @param TError type of the error. + * + * @property prefetchDistance number of items to fetch for the first batch. + * @property batchSize size of the batch. + * @property fetch function that fetches the data. + */ +class LimitOffsetBatchFetcher( + private val prefetchDistance: Int, + private val batchSize: Int, + private val fetch: (request: Request) -> BatchFetchResult, +) : BatchFetcher { + + data class Request( + val limit: Int, + val offset: Int, + val request: TRequest, + ) + + private val lastRequest = MutableStateFlow?>(null) + + override suspend fun fetchFirst(request: TRequest): BatchFetchResult { + val req = Request( + offset = 0, + limit = prefetchDistance, + request = request, + ) + + val res = fetch(req) + lastRequest.value = req + return res + } + + override suspend fun fetchNext( + overrideRequest: TRequest?, + lastResult: BatchFetchResult, + ): BatchFetchResult { + val last = lastRequest.value + requireNotNull(last) + + val req = if (lastResult is BatchFetchResult.Success) { + if (lastResult.last && overrideRequest == null) { + return BatchFetchResult.UnknownError(EndOfPaginationException()) + } + + Request( + offset = last.offset + last.limit, + limit = batchSize, + request = overrideRequest ?: last.request, + ) + } else { + last + } + + val res = fetch(req) + lastRequest.value = req + return res + } +} \ No newline at end of file From dd43f3dbf5063f10f4623345e2df266500491a45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 19:27:25 +0300 Subject: [PATCH 105/223] Updated on 2026-08-14 --- .../com/tangem/pagination/BatchListSource.kt | 36 ++++++++++--------- .../tangem/pagination/BatchUpdateFetcher.kt | 2 +- .../{BatchFetchResult.kt => FetchResult.kt} | 8 ++--- ...chUpdateResult.kt => FetchUpdateResult.kt} | 8 ++--- .../com/tangem/pagination/PaginationStatus.kt | 4 +-- .../tangem/pagination/fetcher/BatchFetcher.kt | 8 ++--- .../fetcher/LimitOffsetBatchFetcher.kt | 14 ++++---- 7 files changed, 42 insertions(+), 38 deletions(-) rename core/pagination/src/main/java/com/tangem/pagination/{BatchFetchResult.kt => FetchResult.kt} (77%) rename core/pagination/src/main/java/com/tangem/pagination/{BatchFetchUpdateResult.kt => FetchUpdateResult.kt} (72%) diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index 7021d474c3..cfedfbcbf2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -17,13 +17,13 @@ import kotlinx.coroutines.flow.* */ interface BatchListSource { val state: StateFlow> - val updateResults: SharedFlow>> + val updateResults: SharedFlow>> } /** * Creates a new [BatchListSource] with the provided configuration. * - * @param config Configuration for batching. + * @param ioDispatcher Dispatcher for IO operations. * @param context Context for batching. * @param generateNewKey Function to generate a new key for a batch. * @param batchFetcher Function to fetch a batch of data. @@ -32,15 +32,17 @@ interface BatchListSource { */ @Suppress("FunctionNaming") fun BatchListSource( + ioDispatcher : CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, -): BatchListSource = BatchListSourceImpl(context, generateNewKey, batchFetcher, null) +): BatchListSource = + BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, null) /** * Creates a new [BatchListSource] with the provided configuration. * - * @param config Configuration for batching. + * @param ioDispatcher Dispatcher for IO operations. * @param context Context for batching. * @param generateNewKey Function to generate a new key for a batch. * @param batchFetcher Function to fetch a batch of data. @@ -50,14 +52,16 @@ fun BatchListSource( */ @Suppress("FunctionNaming") fun BatchListSource( + ioDispatcher: CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, updateFetcher: BatchUpdateFetcher, ): BatchListSource = - BatchListSourceImpl(context, generateNewKey, batchFetcher, updateFetcher) + BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, updateFetcher) private class BatchListSourceImpl( + private val ioDispatcher: CoroutineDispatcher, private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, private val batchFetcher: BatchFetcher, @@ -65,7 +69,7 @@ private class BatchListSourceImpl( ) : BatchListSource { override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) - override val updateResults = MutableSharedFlow>>( + override val updateResults = MutableSharedFlow>>( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -75,7 +79,7 @@ private class BatchListSourceImpl( private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) - private val lastRequestResult = MutableStateFlow?>(null) + private val lastRequestResult = MutableStateFlow?>(null) private var reloadActionJob: Job? = null private var loadMoreActionJob: Job? = null @@ -108,7 +112,7 @@ private class BatchListSourceImpl( loadMoreActionJob?.cancel() reloadActionJob?.cancel() stopAllUpdates() - reloadActionJob = scope.launch(Dispatchers.IO) { + reloadActionJob = scope.launch(ioDispatcher) { reloadTask(action) } } @@ -117,7 +121,7 @@ private class BatchListSourceImpl( return } - loadMoreActionJob = scope.launch(Dispatchers.IO) { + loadMoreActionJob = scope.launch(ioDispatcher) { reloadActionJob?.join() loadMoreTask(action) } @@ -125,7 +129,7 @@ private class BatchListSourceImpl( is BatchAction.UpdateBatches -> { if (updateFetcher == null) return - scope.launch(Dispatchers.IO) { + scope.launch(ioDispatcher) { val job = launch(start = CoroutineStart.LAZY) { updateBatchesTask(action) } @@ -176,9 +180,9 @@ private class BatchListSourceImpl( val res = runCatching { batchFetcher.fetchFirst(action.request) - }.getOrElse { BatchFetchResult.UnknownError(it) } + }.getOrElse { FetchResult.UnknownError(it) } - state.value = if (res is BatchFetchResult.Success) { + state.value = if (res is FetchResult.Success) { val key = generateNewKey(listOf()) val batch = Batch( key = key, @@ -196,7 +200,7 @@ private class BatchListSourceImpl( BatchListState( data = emptyList(), status = PaginationStatus.InitialLoadingError( - error = (res as? BatchFetchResult.Error)?.error, + error = (res as? FetchResult.Error)?.error, ), ) } @@ -216,12 +220,12 @@ private class BatchListSourceImpl( val res = runCatching { batchFetcher.fetchNext(action.request, lastResult) - }.getOrElse { BatchFetchResult.UnknownError(it) } + }.getOrElse { FetchResult.UnknownError(it) } lastRequestResult.value = lastResult state.update { currentState -> - if (res is BatchFetchResult.Success) { + if (res is FetchResult.Success) { val newBatch = Batch( key = generateNewKey(currentState.data.map { it.key }), data = res.data, @@ -254,7 +258,7 @@ private class BatchListSourceImpl( updateRequest = action.request, ) - if (result is BatchFetchUpdateResult.Success) { + if (result is FetchUpdateResult.Success) { state.update { currentState -> val resMap = result.data.associateBy { it.key } currentState.copy( diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt index 71ddd43c76..1cf7e10a91 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -21,5 +21,5 @@ interface BatchUpdateFetcher { suspend fun fetchUpdate( toUpdate: List>, updateRequest: TUpdate, - ): BatchFetchUpdateResult + ): FetchUpdateResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt similarity index 77% rename from core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt rename to core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt index c3b8b03ee2..d72c42ac1d 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt @@ -7,7 +7,7 @@ package com.tangem.pagination * @param TData type of the data. * @param TError type of the error. */ -sealed class BatchFetchResult { +sealed class FetchResult { /** * Represents a successful result of a batch fetch request. @@ -18,14 +18,14 @@ sealed class BatchFetchResult { data class Success( val data: TData, val last: Boolean = false, - ) : BatchFetchResult() + ) : FetchResult() /** * Represents an error result of a batch fetch request. * * @param error error that occurred during the request. */ - data class Error(val error: TError) : BatchFetchResult() + data class Error(val error: TError) : FetchResult() /** * Represents an unknown error result of a batch fetch request. @@ -34,5 +34,5 @@ sealed class BatchFetchResult { * @param throwable throwable that occurred during the request. * @see com.tangem.pagination.fetcher.BatchFetcher */ - class UnknownError(val throwable: Throwable) : BatchFetchResult() + class UnknownError(val throwable: Throwable) : FetchResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt similarity index 72% rename from core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt rename to core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt index 7d47ed8142..d575044a1f 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchUpdateResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt @@ -8,7 +8,7 @@ package com.tangem.pagination * @param TData type of the data. * @param TError type of the error. */ -sealed class BatchFetchUpdateResult { +sealed class FetchUpdateResult { /** * Represents a successful result of a batch update operation. @@ -17,14 +17,14 @@ sealed class BatchFetchUpdateResult { */ data class Success( val data: List>, - ) : BatchFetchUpdateResult() + ) : FetchUpdateResult() /** * Represents an error result of a batch update operation. * * @param error error that occurred during the operation. */ - data class Error(val error: TError) : BatchFetchUpdateResult() + data class Error(val error: TError) : FetchUpdateResult() /** * Represents an unknown error result of a batch update operation. @@ -32,5 +32,5 @@ sealed class BatchFetchUpdateResult { * * @param throwable throwable that occurred during the operation. */ - class UnknownError(val throwable: Throwable) : BatchFetchUpdateResult() + class UnknownError(val throwable: Throwable) : FetchUpdateResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt index 054178ce19..ec30a2ea1e 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -34,12 +34,12 @@ sealed class PaginationStatus { /** * Represents that the last batch was loaded and * the source is ready to load the next one or reload previous if [lastResult] is an error. - * For the first batch, [lastResult] is always [BatchFetchResult.Success] + * For the first batch, [lastResult] is always [FetchResult.Success] * * @param lastResult result of the last batch fetch. */ data class Paginating( - val lastResult: BatchFetchResult, + val lastResult: FetchResult, ) : PaginationStatus() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt index 901d83f32b..2d9e50832e 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -1,6 +1,6 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.FetchResult import com.tangem.pagination.BatchListState /** @@ -20,7 +20,7 @@ interface BatchFetcher { * @param request initial request. Will be saved to be used in [fetchNext] requests. * @return result of the fetch operation. */ - suspend fun fetchFirst(request: TRequest): BatchFetchResult + suspend fun fetchFirst(request: TRequest): FetchResult /** * Fetches the next batch of data. @@ -32,6 +32,6 @@ interface BatchFetcher { */ suspend fun fetchNext( overrideRequest: TRequest?, - lastResult: BatchFetchResult, - ): BatchFetchResult + lastResult: FetchResult, + ): FetchResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 11af45665d..858411b6a6 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -1,6 +1,6 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.FetchResult import com.tangem.pagination.exception.EndOfPaginationException import kotlinx.coroutines.flow.MutableStateFlow @@ -18,7 +18,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: (request: Request) -> BatchFetchResult, + private val fetch: (request: Request) -> FetchResult, ) : BatchFetcher { data class Request( @@ -29,7 +29,7 @@ class LimitOffsetBatchFetcher( private val lastRequest = MutableStateFlow?>(null) - override suspend fun fetchFirst(request: TRequest): BatchFetchResult { + override suspend fun fetchFirst(request: TRequest): FetchResult { val req = Request( offset = 0, limit = prefetchDistance, @@ -43,14 +43,14 @@ class LimitOffsetBatchFetcher( override suspend fun fetchNext( overrideRequest: TRequest?, - lastResult: BatchFetchResult, - ): BatchFetchResult { + lastResult: FetchResult, + ): FetchResult { val last = lastRequest.value requireNotNull(last) - val req = if (lastResult is BatchFetchResult.Success) { + val req = if (lastResult is FetchResult.Success) { if (lastResult.last && overrideRequest == null) { - return BatchFetchResult.UnknownError(EndOfPaginationException()) + return FetchResult.UnknownError(EndOfPaginationException()) } Request( From d819a156b2736c03284ff0fce31de38ef0677a1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 3 Jul 2024 19:50:38 +0300 Subject: [PATCH 106/223] Updated on 2026-08-14 --- .../src/main/java/com/tangem/pagination/BatchListSource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index cfedfbcbf2..0c6c05bd57 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -32,7 +32,7 @@ interface BatchListSource { */ @Suppress("FunctionNaming") fun BatchListSource( - ioDispatcher : CoroutineDispatcher = Dispatchers.IO, + ioDispatcher: CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, From 16eb18b07e6bb285c0e05105b30ba9ffede36462 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 14:39:28 +0300 Subject: [PATCH 107/223] Updated on 2026-08-14 --- .../java/com/tangem/pagination/BatchAction.kt | 24 ++-- .../{FetchResult.kt => BatchFetchResult.kt} | 16 +-- .../com/tangem/pagination/BatchListSource.kt | 128 +++++++++--------- .../com/tangem/pagination/BatchListState.kt | 5 +- .../tangem/pagination/BatchUpdateFetcher.kt | 10 +- .../tangem/pagination/BatchUpdateResult.kt | 27 ++++ .../com/tangem/pagination/BatchingConfig.kt | 12 -- .../com/tangem/pagination/BatchingContext.kt | 6 +- .../tangem/pagination/FetchUpdateResult.kt | 36 ----- .../com/tangem/pagination/PaginationStatus.kt | 29 ++-- .../tangem/pagination/fetcher/BatchFetcher.kt | 12 +- .../fetcher/LimitOffsetBatchFetcher.kt | 25 ++-- 12 files changed, 147 insertions(+), 183 deletions(-) rename core/pagination/src/main/java/com/tangem/pagination/{FetchResult.kt => BatchFetchResult.kt} (55%) create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt delete mode 100644 core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 813cbe6ba9..20d95b63aa 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -3,41 +3,41 @@ package com.tangem.pagination /** * Action that can be dispatched to [BatchListSource]. * - * @param TRequest type of the request to load batches. + * @param TRequestParams type of the request params to load batches. * @param TKey type of the key of the batch. * @param TUpdate type of the update request. */ -sealed class BatchAction { +sealed class BatchAction { /** * Action to load the first batch. * - * @param request request to load the first batch. + * @param requestParams request params to load the first batch. */ - data class Reload( - val request: TRequest, - ) : BatchAction() + data class Reload( + val requestParams: TRequestParams, + ) : BatchAction() /** * Action to load the next batch. * - * @param request request to load the next batch with new request. + * @param requestParams request params to load the next batch with new request. * If null, the last request will be used. * Will be saved in the state and used for future LoadMore actions with request = null. */ - data class LoadMore( - val request: TRequest? = null, - ) : BatchAction() + data class LoadMore( + val requestParams: TRequestParams? = null, + ) : BatchAction() /** * Action to update the batch. * * @param keys keys of the batches to update. - * @param request request to update the batches. + * @param updateRequest request to update the batches. */ class UpdateBatches( val keys: Set, - val request: TUpdate, + val updateRequest: TUpdate, ) : BatchAction() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt similarity index 55% rename from core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt rename to core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index d72c42ac1d..a247310b9c 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/FetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -5,9 +5,8 @@ package com.tangem.pagination * Used in [BatchListState]. * * @param TData type of the data. - * @param TError type of the error. */ -sealed class FetchResult { +sealed class BatchFetchResult { /** * Represents a successful result of a batch fetch request. @@ -18,21 +17,14 @@ sealed class FetchResult { data class Success( val data: TData, val last: Boolean = false, - ) : FetchResult() + ) : BatchFetchResult() /** * Represents an error result of a batch fetch request. - * - * @param error error that occurred during the request. - */ - data class Error(val error: TError) : FetchResult() - - /** - * Represents an unknown error result of a batch fetch request. - * Used for unexpected exceptions that occurred in fetch method in BatchFetcher. + * Also used for unexpected exceptions that occurred in fetch method in BatchFetcher. * * @param throwable throwable that occurred during the request. * @see com.tangem.pagination.fetcher.BatchFetcher */ - class UnknownError(val throwable: Throwable) : FetchResult() + class Error(val throwable: Throwable) : BatchFetchResult() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index 0c6c05bd57..bd521c6f71 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -11,19 +11,18 @@ import kotlinx.coroutines.flow.* * @param TKey Type of the key that identifies a batch. * @param TData Type of the data in the batch. Generally, it' a list of items. * @param TUpdate Type of the update request. - * @param TError Type of the error that can occur during fetching or updating. * @property state State of the paginated data. * @property updateResults Flow of results of update requests. */ -interface BatchListSource { - val state: StateFlow> - val updateResults: SharedFlow>> +interface BatchListSource { + val state: StateFlow> + val updateResults: SharedFlow>> } /** * Creates a new [BatchListSource] with the provided configuration. * - * @param ioDispatcher Dispatcher for IO operations. + * @param fetchDispatcher Dispatcher for fetch operations. * @param context Context for batching. * @param generateNewKey Function to generate a new key for a batch. * @param batchFetcher Function to fetch a batch of data. @@ -31,18 +30,18 @@ interface BatchListSource { * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - ioDispatcher: CoroutineDispatcher = Dispatchers.IO, +fun BatchListSource( + fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, context: BatchingContext, generateNewKey: suspend (List) -> TKey, - batchFetcher: BatchFetcher, -): BatchListSource = - BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, null) + batchFetcher: BatchFetcher, +): BatchListSource = + DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) /** * Creates a new [BatchListSource] with the provided configuration. * - * @param ioDispatcher Dispatcher for IO operations. + * @param fetchDispatcher Dispatcher for fetch operations. * @param context Context for batching. * @param generateNewKey Function to generate a new key for a batch. * @param batchFetcher Function to fetch a batch of data. @@ -51,25 +50,25 @@ fun BatchListSource( * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( - ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - context: BatchingContext, +fun BatchListSource( + fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, + context: BatchingContext, generateNewKey: suspend (List) -> TKey, - batchFetcher: BatchFetcher, - updateFetcher: BatchUpdateFetcher, -): BatchListSource = - BatchListSourceImpl(ioDispatcher, context, generateNewKey, batchFetcher, updateFetcher) + batchFetcher: BatchFetcher, + updateFetcher: BatchUpdateFetcher, +): BatchListSource = + DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) -private class BatchListSourceImpl( - private val ioDispatcher: CoroutineDispatcher, - private val context: BatchingContext, +private class DefaultBatchListSource( + private val fetchDispatcher: CoroutineDispatcher, + private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, - private val batchFetcher: BatchFetcher, - private val updateFetcher: BatchUpdateFetcher? = null, -) : BatchListSource { + private val batchFetcher: BatchFetcher, + private val updateFetcher: BatchUpdateFetcher? = null, +) : BatchListSource { - override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) - override val updateResults = MutableSharedFlow>>( + override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) + override val updateResults = MutableSharedFlow>>( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -79,7 +78,7 @@ private class BatchListSourceImpl( private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) - private val lastRequestResult = MutableStateFlow?>(null) + private val lastRequestResult = MutableStateFlow?>(null) private var reloadActionJob: Job? = null private var loadMoreActionJob: Job? = null @@ -105,14 +104,14 @@ private class BatchListSourceImpl( } } - private fun collectActions(action: BatchAction) { + private fun collectActions(action: BatchAction) { when (action) { is BatchAction.Reload -> { // Stop all tasks loadMoreActionJob?.cancel() reloadActionJob?.cancel() stopAllUpdates() - reloadActionJob = scope.launch(ioDispatcher) { + reloadActionJob = scope.launch(fetchDispatcher) { reloadTask(action) } } @@ -121,7 +120,7 @@ private class BatchListSourceImpl( return } - loadMoreActionJob = scope.launch(ioDispatcher) { + loadMoreActionJob = scope.launch(fetchDispatcher) { reloadActionJob?.join() loadMoreTask(action) } @@ -129,7 +128,7 @@ private class BatchListSourceImpl( is BatchAction.UpdateBatches -> { if (updateFetcher == null) return - scope.launch(ioDispatcher) { + scope.launch(fetchDispatcher) { val job = launch(start = CoroutineStart.LAZY) { updateBatchesTask(action) } @@ -172,60 +171,63 @@ private class BatchListSourceImpl( } } - private suspend fun reloadTask(action: BatchAction.Reload) { + private suspend fun reloadTask(action: BatchAction.Reload) { state.value = BatchListState( data = emptyList(), status = PaginationStatus.InitialLoading, ) val res = runCatching { - batchFetcher.fetchFirst(action.request) - }.getOrElse { FetchResult.UnknownError(it) } + batchFetcher.fetchFirst(action.requestParams) + }.getOrElse { BatchFetchResult.Error(it) } - state.value = if (res is FetchResult.Success) { - val key = generateNewKey(listOf()) - val batch = Batch( - key = key, - data = res.data, - ) - BatchListState( - data = listOf(batch), - status = if (res.last) { - PaginationStatus.EndOfPagination - } else { - PaginationStatus.Paginating(res) - }, - ) - } else { - BatchListState( - data = emptyList(), - status = PaginationStatus.InitialLoadingError( - error = (res as? FetchResult.Error)?.error, - ), - ) + state.value = when (res) { + is BatchFetchResult.Success -> { + val key = generateNewKey(listOf()) + val batch = Batch( + key = key, + data = res.data, + ) + BatchListState( + data = listOf(batch), + status = if (res.last) { + PaginationStatus.EndOfPagination + } else { + PaginationStatus.Paginating(res) + }, + ) + } + is BatchFetchResult.Error -> { + BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoadingError( + throwable = res.throwable, + ), + ) + } } lastRequestResult.value = res } - private suspend fun loadMoreTask(action: BatchAction.LoadMore) { + private suspend fun loadMoreTask(action: BatchAction.LoadMore) { val status = state.value.status if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return - if (status is PaginationStatus.EndOfPagination && action.request == null) return + if (status is PaginationStatus.EndOfPagination && action.requestParams == null) return val lastResult = lastRequestResult.value ?: return state.update { it.copy(status = PaginationStatus.NextBatchLoading) } val res = runCatching { - batchFetcher.fetchNext(action.request, lastResult) - }.getOrElse { FetchResult.UnknownError(it) } + batchFetcher.fetchNext(action.requestParams, lastResult) + }.getOrElse { BatchFetchResult.Error(it) } lastRequestResult.value = lastResult state.update { currentState -> - if (res is FetchResult.Success) { + if (res is BatchFetchResult.Success) { val newBatch = Batch( key = generateNewKey(currentState.data.map { it.key }), data = res.data, @@ -255,10 +257,10 @@ private class BatchListSourceImpl( val result = updateFetcher.fetchUpdate( toUpdate = batchesToUpdate, - updateRequest = action.request, + updateRequest = action.updateRequest, ) - if (result is FetchUpdateResult.Success) { + if (result is BatchUpdateResult.Success) { state.update { currentState -> val resMap = result.data.associateBy { it.key } currentState.copy( @@ -269,7 +271,7 @@ private class BatchListSourceImpl( } } - updateResults.emit(action.request to result) + updateResults.emit(action.updateRequest to result) } private fun stopAllUpdates() { diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt index 525458362f..b53ba23dc0 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -5,14 +5,13 @@ package com.tangem.pagination * * @param TKey type of the key of the batch. * @param TData type of the data. - * @param TError type of the error. * * @property data list of loaded batches. * @property status current status of the pagination. * * @see BatchListSource */ -data class BatchListState( +data class BatchListState( val data: List>, - val status: PaginationStatus, + val status: PaginationStatus, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt index 1cf7e10a91..704c416316 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -6,20 +6,18 @@ package com.tangem.pagination * * @param TKey type of the key. * @param TData type of the data. - * @param TError type of the error. * @param TUpdate type of the update request. */ -interface BatchUpdateFetcher { +fun interface BatchUpdateFetcher { /** * Fetches updates for a batch of data. + * Note that the result batch key as a result of executing the method must be presented in the [toUpdate] list, + * otherwise, updates will not be performed * * @param toUpdate list of batches to update. * @param updateRequest request to update the data. * @return result of the update operation. */ - suspend fun fetchUpdate( - toUpdate: List>, - updateRequest: TUpdate, - ): FetchUpdateResult + suspend fun fetchUpdate(toUpdate: List>, updateRequest: TUpdate): BatchUpdateResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt new file mode 100644 index 0000000000..c2e00fad9c --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt @@ -0,0 +1,27 @@ +package com.tangem.pagination + +/** + * Represents the result of a batch fetch operation. + * Used in [BatchListState] and [BatchUpdateFetcher]. + * + * @param TKey type of the key. + * @param TData type of the data. + */ +sealed class BatchUpdateResult { + + /** + * Represents a successful result of a batch update operation. + * + * @param data fetched data. + */ + data class Success( + val data: List>, + ) : BatchUpdateResult() + + /** + * Represents an error result of a batch update operation. + * + * @param error error that occurred during the operation. + */ + class Error(val throwable: Throwable) : BatchUpdateResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt deleted file mode 100644 index 8789d11405..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingConfig.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.pagination - -/** - * Configuration for batching. - * - * @param batchSize size of the batch. - * @param prefetchDistance number of items to fetch for the first batch. - */ -data class BatchingConfig( - val batchSize: Int, - val prefetchDistance: Int = batchSize, -) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt index 37b6bd2c73..fb9672d365 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.flow.Flow /** * Context for working with [BatchListSource]. * - * @param TRequest type of the request. + * @param TRequestParams type of the request. * @param TKey type of the key. * @param TUpdate type of the update request. * @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.Flow * * @see BatchListSource */ -class BatchingContext( - val actionsFlow: Flow>, +class BatchingContext( + val actionsFlow: Flow>, val coroutineScope: CoroutineScope, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt deleted file mode 100644 index d575044a1f..0000000000 --- a/core/pagination/src/main/java/com/tangem/pagination/FetchUpdateResult.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.pagination - -/** - * Represents the result of a batch fetch operation. - * Used in [BatchListState] and [BatchUpdateFetcher]. - * - * @param TKey type of the key. - * @param TData type of the data. - * @param TError type of the error. - */ -sealed class FetchUpdateResult { - - /** - * Represents a successful result of a batch update operation. - * - * @param data fetched data. - */ - data class Success( - val data: List>, - ) : FetchUpdateResult() - - /** - * Represents an error result of a batch update operation. - * - * @param error error that occurred during the operation. - */ - data class Error(val error: TError) : FetchUpdateResult() - - /** - * Represents an unknown error result of a batch update operation. - * Used for unexpected exceptions that occurred in `fetchUpdate` method in [BatchUpdateFetcher]. - * - * @param throwable throwable that occurred during the operation. - */ - class UnknownError(val throwable: Throwable) : FetchUpdateResult() -} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt index ec30a2ea1e..5e30875c6a 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -3,54 +3,53 @@ package com.tangem.pagination /** * Status of the pagination. * - * @param T type of the data. - * @param E type of the error. + * @param TData type of the data. * * @see BatchListState */ -sealed class PaginationStatus { +sealed class PaginationStatus { /** * Represents that there is no data. Used when the list of batches is empty. * The initial state of the pagination. */ - data object None : PaginationStatus() + data object None : PaginationStatus() /** * Represents that the batch is loading for the first time. * Used when the pagination is empty and the first batch is being loaded. */ - data object InitialLoading : PaginationStatus() + data object InitialLoading : PaginationStatus() /** * Represents that the first batch was loaded with an error. * * @param error error that occurred during the initial loading. */ - data class InitialLoadingError( - val error: E?, - ) : PaginationStatus() + data class InitialLoadingError( + val throwable: Throwable, + ) : PaginationStatus() /** * Represents that the last batch was loaded and * the source is ready to load the next one or reload previous if [lastResult] is an error. - * For the first batch, [lastResult] is always [FetchResult.Success] + * For the first batch, [lastResult] is always [BatchFetchResult.Success] * * @param lastResult result of the last batch fetch. */ - data class Paginating( - val lastResult: FetchResult, - ) : PaginationStatus() + data class Paginating( + val lastResult: BatchFetchResult, + ) : PaginationStatus() /** * Represents that the next batch is loading. * Used when the next batch is being loaded. */ - data object NextBatchLoading : PaginationStatus() + data object NextBatchLoading : PaginationStatus() /** * Represents that the source has no more batches to load. - * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.request] = null will be ignored. + * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.requestParams] = null will be ignored. */ - data object EndOfPagination : PaginationStatus() + data object EndOfPagination : PaginationStatus() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt index 2d9e50832e..98358257b5 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -1,6 +1,6 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.FetchResult +import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListState /** @@ -8,11 +8,10 @@ import com.tangem.pagination.BatchListState * * @param TRequest type of the request. * @param TData type of the data. - * @param TError type of the error. * * @see BatchListState */ -interface BatchFetcher { +interface BatchFetcher { /** * Fetches the first batch of data. @@ -20,7 +19,7 @@ interface BatchFetcher { * @param request initial request. Will be saved to be used in [fetchNext] requests. * @return result of the fetch operation. */ - suspend fun fetchFirst(request: TRequest): FetchResult + suspend fun fetchFirst(request: TRequest): BatchFetchResult /** * Fetches the next batch of data. @@ -30,8 +29,5 @@ interface BatchFetcher { * @param lastResult result of the last fetch operation. * @return result of the fetch operation. */ - suspend fun fetchNext( - overrideRequest: TRequest?, - lastResult: FetchResult, - ): FetchResult + suspend fun fetchNext(overrideRequest: TRequest?, lastResult: BatchFetchResult): BatchFetchResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 858411b6a6..6a321a61e9 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -1,25 +1,24 @@ package com.tangem.pagination.fetcher -import com.tangem.pagination.FetchResult +import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.exception.EndOfPaginationException import kotlinx.coroutines.flow.MutableStateFlow /** * Fetcher that uses limit and offset to fetch data. * - * @param TRequest type of the request. + * @param TRequestParams type of the request params. * @param TData type of the data. - * @param TError type of the error. * * @property prefetchDistance number of items to fetch for the first batch. * @property batchSize size of the batch. * @property fetch function that fetches the data. */ -class LimitOffsetBatchFetcher( +class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: (request: Request) -> FetchResult, -) : BatchFetcher { + private val fetch: (request: Request) -> BatchFetchResult, +) : BatchFetcher { data class Request( val limit: Int, @@ -27,9 +26,9 @@ class LimitOffsetBatchFetcher( val request: TRequest, ) - private val lastRequest = MutableStateFlow?>(null) + private val lastRequest = MutableStateFlow?>(null) - override suspend fun fetchFirst(request: TRequest): FetchResult { + override suspend fun fetchFirst(request: TRequestParams): BatchFetchResult { val req = Request( offset = 0, limit = prefetchDistance, @@ -42,15 +41,15 @@ class LimitOffsetBatchFetcher( } override suspend fun fetchNext( - overrideRequest: TRequest?, - lastResult: FetchResult, - ): FetchResult { + overrideRequest: TRequestParams?, + lastResult: BatchFetchResult, + ): BatchFetchResult { val last = lastRequest.value requireNotNull(last) - val req = if (lastResult is FetchResult.Success) { + val req = if (lastResult is BatchFetchResult.Success) { if (lastResult.last && overrideRequest == null) { - return FetchResult.UnknownError(EndOfPaginationException()) + return BatchFetchResult.Error(EndOfPaginationException()) } Request( From 7dd5e87cfd3c7f0615792d8e407aae8babfa8295 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 17:10:03 +0300 Subject: [PATCH 108/223] Updated on 2026-08-14 --- .../java/com/tangem/pagination/BatchAction.kt | 10 +-- .../com/tangem/pagination/BatchListSource.kt | 64 +++++++++++-------- .../com/tangem/pagination/BatchingContext.kt | 4 +- .../tangem/pagination/fetcher/BatchFetcher.kt | 15 +++-- .../fetcher/LimitOffsetBatchFetcher.kt | 12 ++-- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 20d95b63aa..691941752a 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -7,7 +7,7 @@ package com.tangem.pagination * @param TKey type of the key of the batch. * @param TUpdate type of the update request. */ -sealed class BatchAction { +sealed class BatchAction { /** * Action to load the first batch. @@ -16,7 +16,7 @@ sealed class BatchAction { */ data class Reload( val requestParams: TRequestParams, - ) : BatchAction() + ) : BatchAction() /** * Action to load the next batch. @@ -27,7 +27,7 @@ sealed class BatchAction { */ data class LoadMore( val requestParams: TRequestParams? = null, - ) : BatchAction() + ) : BatchAction() /** * Action to update the batch. @@ -38,7 +38,7 @@ sealed class BatchAction { class UpdateBatches( val keys: Set, val updateRequest: TUpdate, - ) : BatchAction() + ) : BatchAction() /** * Action to cancel the current batch loading. @@ -57,5 +57,5 @@ sealed class BatchAction { */ class CancelUpdates( val predicate: (UpdateBatches) -> Boolean, - ) : BatchAction() + ) : BatchAction() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index bd521c6f71..fc1d0bd1c2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -30,11 +30,11 @@ interface BatchListSource { * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( +fun BatchListSource( fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, - context: BatchingContext, + context: BatchingContext, generateNewKey: suspend (List) -> TKey, - batchFetcher: BatchFetcher, + batchFetcher: BatchFetcher, ): BatchListSource = DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) @@ -50,18 +50,18 @@ fun BatchListSource( * @return New instance of [BatchListSource]. */ @Suppress("FunctionNaming") -fun BatchListSource( +fun BatchListSource( fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, - context: BatchingContext, + context: BatchingContext, generateNewKey: suspend (List) -> TKey, batchFetcher: BatchFetcher, updateFetcher: BatchUpdateFetcher, ): BatchListSource = DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) -private class DefaultBatchListSource( +private class DefaultBatchListSource( private val fetchDispatcher: CoroutineDispatcher, - private val context: BatchingContext, + private val context: BatchingContext, private val generateNewKey: suspend (List) -> TKey, private val batchFetcher: BatchFetcher, private val updateFetcher: BatchUpdateFetcher? = null, @@ -104,7 +104,7 @@ private class DefaultBatchListSource } } - private fun collectActions(action: BatchAction) { + private fun collectActions(action: BatchAction) { when (action) { is BatchAction.Reload -> { // Stop all tasks @@ -129,6 +129,8 @@ private class DefaultBatchListSource if (updateFetcher == null) return scope.launch(fetchDispatcher) { + // Lazily start a job so we can avoid batch update collisions + // by waiting for other tasks with the same keys to complete val job = launch(start = CoroutineStart.LAZY) { updateBatchesTask(action) } @@ -137,18 +139,21 @@ private class DefaultBatchListSource waitingUpdateJobs.update { it + actionJob } + // Wait for other update tasks that mutate batches with the same keys updateJobs.first { workingJobs -> action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() } waitingUpdateJobs.update { it - actionJob } + // No other task are mutating batches with the same keys, so we can start a job val started = job.start() if (started) { updateJobs.update { it + actionJob } job.invokeOnCompletion { cause -> + // If the job was cancelled it is up to a canceller to remove job from the updateJobs list if (cause !is CancellationException) { updateJobs.update { it - actionJob } } @@ -213,6 +218,12 @@ private class DefaultBatchListSource private suspend fun loadMoreTask(action: BatchAction.LoadMore) { val status = state.value.status + // Skip the action if the state is not ready to continue pagination. + // Two options are acceptable: + // 1. The Source is ready to load next page with the same or different request params. + // 2. The Source has reached the end of pagination, but there is another request + // that can possibly load the next page and continue the pagination + if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return if (status is PaginationStatus.EndOfPagination && action.requestParams == null) return @@ -227,24 +238,27 @@ private class DefaultBatchListSource lastRequestResult.value = lastResult state.update { currentState -> - if (res is BatchFetchResult.Success) { - val newBatch = Batch( - key = generateNewKey(currentState.data.map { it.key }), - data = res.data, - ) + when (res) { + is BatchFetchResult.Success -> { + val newBatch = Batch( + key = generateNewKey(currentState.data.map { it.key }), + data = res.data, + ) - currentState.copy( - data = currentState.data + newBatch, - status = if (res.last) { - PaginationStatus.EndOfPagination - } else { - PaginationStatus.Paginating(res) - }, - ) - } else { - currentState.copy( - status = PaginationStatus.Paginating(res), - ) + currentState.copy( + data = currentState.data + newBatch, + status = if (res.last) { + PaginationStatus.EndOfPagination + } else { + PaginationStatus.Paginating(res) + }, + ) + } + is BatchFetchResult.Error -> { + currentState.copy( + status = PaginationStatus.Paginating(res), + ) + } } } } diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt index fb9672d365..252d6f1680 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.Flow * * @see BatchListSource */ -class BatchingContext( - val actionsFlow: Flow>, +class BatchingContext( + val actionsFlow: Flow>, val coroutineScope: CoroutineScope, ) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt index 98358257b5..818620dab1 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -6,28 +6,31 @@ import com.tangem.pagination.BatchListState /** * Interface for fetching a batch of data. Used in [BatchListState]. * - * @param TRequest type of the request. + * @param TRequestParams type of the request. * @param TData type of the data. * * @see BatchListState */ -interface BatchFetcher { +interface BatchFetcher { /** * Fetches the first batch of data. * - * @param request initial request. Will be saved to be used in [fetchNext] requests. + * @param requestParams initial request params. Will be saved to be used in [fetchNext] requests. * @return result of the fetch operation. */ - suspend fun fetchFirst(request: TRequest): BatchFetchResult + suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult /** * Fetches the next batch of data. * - * @param overrideRequest overrides current remembered request, even if that fetch fails. + * @param overrideRequestParams overrides current remembered request, even if that fetch fails. * If null, the last request should be used. * @param lastResult result of the last fetch operation. * @return result of the fetch operation. */ - suspend fun fetchNext(overrideRequest: TRequest?, lastResult: BatchFetchResult): BatchFetchResult + suspend fun fetchNext( + overrideRequestParams: TRequestParams?, + lastResult: BatchFetchResult, + ): BatchFetchResult } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 6a321a61e9..01059c4676 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: (request: Request) -> BatchFetchResult, + private val fetch: suspend (request: Request) -> BatchFetchResult, ) : BatchFetcher { data class Request( @@ -28,11 +28,11 @@ class LimitOffsetBatchFetcher( private val lastRequest = MutableStateFlow?>(null) - override suspend fun fetchFirst(request: TRequestParams): BatchFetchResult { + override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult { val req = Request( offset = 0, limit = prefetchDistance, - request = request, + request = requestParams, ) val res = fetch(req) @@ -41,21 +41,21 @@ class LimitOffsetBatchFetcher( } override suspend fun fetchNext( - overrideRequest: TRequestParams?, + overrideRequestParams: TRequestParams?, lastResult: BatchFetchResult, ): BatchFetchResult { val last = lastRequest.value requireNotNull(last) val req = if (lastResult is BatchFetchResult.Success) { - if (lastResult.last && overrideRequest == null) { + if (lastResult.last && overrideRequestParams == null) { return BatchFetchResult.Error(EndOfPaginationException()) } Request( offset = last.offset + last.limit, limit = batchSize, - request = overrideRequest ?: last.request, + request = overrideRequestParams ?: last.request, ) } else { last From 1fbf2debcdbc53a1f8d1733fe124620a658dae4c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 19:09:46 +0400 Subject: [PATCH 109/223] Updated on 2026-08-14 --- .../converters/AmountStateConverter.kt | 2 +- .../models/AmountSegmentedButtonsConfig.kt | 4 +-- .../ui/amountScreen/models/AmountState.kt | 4 +-- .../preview/AmountStatePreviewData.kt | 6 ++-- .../common/ui/amountScreen/ui/AmountBlock.kt | 4 +-- .../ui/amountScreen/ui/AmountButtons.kt | 4 +-- .../amountScreen/ui/AmountFieldContainer.kt | 4 +-- .../currency/DefaultCurrencyIcon.kt | 2 +- .../{tokenicon => icon}/ContentIcon.kt | 14 ++++----- .../TokenIcon.kt => icon/CurrencyIcon.kt} | 30 ++++++++++--------- .../CurrencyIconState.kt} | 30 +++++++++---------- .../currency/{tokenicon => icon}/IconBadge.kt | 6 ++-- .../CryptoCurrencyToIconStateConverter.kt | 28 ++++++++--------- .../ui/components/inputrow/InputRowApprox.kt | 22 +++++++------- .../ui/components/inputrow/InputRowImage.kt | 18 +++++------ .../inputrow/inner/InputRowAsyncImage.kt | 2 +- .../features/markets/ui/MarketsListItem.kt | 18 +++++++---- .../presentation/state/SendStateFactory.kt | 2 +- .../SetInitialDataStateTransformer.kt | 2 +- .../swap/converters/TokensDataConverter.kt | 12 ++++---- .../swap/models/SwapSelectTokenStateHolder.kt | 4 +-- .../swap/models/SwapSuccessStateHolder.kt | 6 ++-- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 17 ++++++----- .../feature/swap/ui/SwapSuccessScreen.kt | 10 +++---- .../state/SwapTransactionsState.kt | 6 ++-- ...enDetailsSwapTransactionsStateConverter.kt | 2 +- .../components/exchange/ExchangeEstimate.kt | 6 ++-- .../exchange/ExchangeStatusItems.kt | 16 +++++----- .../presentation/common/WalletPreviewData.kt | 12 ++++---- .../common/component/TokenItem.kt | 4 +-- .../common/preview/WalletScreenPreviewData.kt | 6 ++-- .../common/state/TokenItemState.kt | 16 +++++----- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- .../converter/TokenItemStateConverter.kt | 2 +- 35 files changed, 169 insertions(+), 156 deletions(-) rename core/ui/src/main/java/com/tangem/core/ui/components/currency/{tokenicon => icon}/ContentIcon.kt (91%) rename core/ui/src/main/java/com/tangem/core/ui/components/currency/{tokenicon/TokenIcon.kt => icon/CurrencyIcon.kt} (77%) rename core/ui/src/main/java/com/tangem/core/ui/components/currency/{tokenicon/TokenIconState.kt => icon/CurrencyIconState.kt} (75%) rename core/ui/src/main/java/com/tangem/core/ui/components/currency/{tokenicon => icon}/IconBadge.kt (92%) rename core/ui/src/main/java/com/tangem/core/ui/components/currency/{tokenicon => icon}/converter/CryptoCurrencyToIconStateConverter.kt (81%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 0fe994f885..ad251378f6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt index 733ae20d2e..23469c4c03 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt @@ -1,7 +1,7 @@ package com.tangem.common.ui.amountScreen.models import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference /** @@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.TextReference @Immutable data class AmountSegmentedButtonsConfig( val title: TextReference, - val iconState: TokenIconState? = null, + val iconState: CurrencyIconState? = null, val iconUrl: String? = null, val isFiat: Boolean, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index 045ec94609..d23a2892bf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -1,7 +1,7 @@ package com.tangem.common.ui.amountScreen.models import androidx.compose.runtime.Stable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.PersistentList @@ -26,7 +26,7 @@ sealed class AmountState { override val isPrimaryButtonEnabled: Boolean, val walletName: String, val walletBalance: TextReference, - val tokenIconState: TokenIconState, + val tokenIconState: CurrencyIconState, val segmentedButtonConfig: PersistentList, val selectedButton: Int, val isSegmentedButtonsEnabled: Boolean, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index c597d024b4..91951b4220 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.model.Amount @@ -19,11 +19,11 @@ object AmountStatePreviewData { isPrimaryButtonEnabled = false, walletName = "Family Wallet", walletBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), - tokenIconState = TokenIconState.Loading, + tokenIconState = CurrencyIconState.Loading, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( title = stringReference("USDT"), - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, isFiat = false, ), AmountSegmentedButtonsConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index 942186b435..b2903b20cc 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.components.ResizableText -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter @@ -54,7 +54,7 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing16), ) { - TokenIcon(state = amountState.tokenIconState) + CurrencyIcon(state = amountState.tokenIconState) ResizableText( text = firstAmount, style = TangemTheme.typography.h2, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index 5b23386ee8..b279e47f6a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -19,7 +19,7 @@ import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.fiaticon.FiatIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.PersistentList @@ -105,7 +105,7 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment modifier = iconModifier, ) } else if (button.iconState != null) { - TokenIcon( + CurrencyIcon( state = button.iconState, shouldDisplayNetwork = false, modifier = iconModifier, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 09b81b4420..46fef9fbb0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.utils.Strings.STARS @@ -55,7 +55,7 @@ internal fun LazyListScope.amountField( .padding(top = TangemTheme.dimens.spacing2), ) } - TokenIcon( + CurrencyIcon( state = amountState.tokenIconState, modifier = Modifier .padding(top = TangemTheme.dimens.spacing32), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt index 005053e2c8..bc9b5f2707 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest -import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon +import com.tangem.core.ui.components.currency.icon.LoadingIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import kotlinx.coroutines.launch diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt similarity index 91% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index 1814d2593d..089d050c7d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -18,20 +18,20 @@ import com.tangem.core.ui.res.TangemTheme @Composable internal fun ContentIcon( - icon: TokenIconState, + icon: CurrencyIconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier, ) { when (icon) { - is TokenIconState.CoinIcon -> CoinIcon( + is CurrencyIconState.CoinIcon -> CoinIcon( modifier = modifier, url = icon.url, fallbackResId = icon.fallbackResId, alpha = alpha, colorFilter = colorFilter, ) - is TokenIconState.TokenIcon -> TokenIcon( + is CurrencyIconState.TokenIcon -> TokenIcon( modifier = modifier, url = icon.url, alpha = alpha, @@ -45,14 +45,14 @@ internal fun ContentIcon( ) }, ) - is TokenIconState.CustomTokenIcon -> CustomTokenIcon( + is CurrencyIconState.CustomTokenIcon -> CustomTokenIcon( modifier = modifier, tint = icon.tint, background = icon.background, alpha = alpha, ) - TokenIconState.Loading, - TokenIconState.Locked, + CurrencyIconState.Loading, + CurrencyIconState.Locked, -> Unit } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt similarity index 77% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 559bc7bde8..ce67d13f6f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -26,23 +26,23 @@ import com.tangem.core.ui.utils.NORMAL_ALPHA * @param shouldDisplayNetwork specifies whether to display network badge */ @Composable -fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { +fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) .size(TangemTheme.dimens.size36) when (state) { - is TokenIconState.Loading -> LoadingIcon(modifier = iconModifier) - is TokenIconState.Locked -> LockedIcon(modifier = iconModifier) - is TokenIconState.CoinIcon, - is TokenIconState.CustomTokenIcon, - is TokenIconState.TokenIcon, + is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) + is CurrencyIconState.Locked -> LockedIcon(modifier = iconModifier) + is CurrencyIconState.CoinIcon, + is CurrencyIconState.CustomTokenIcon, + is CurrencyIconState.TokenIcon, -> { ContentIconContainer( icon = state, modifier = iconModifier, - shouldDisplayNetwork = shouldDisplayNetwork, + shouldShowTopBadge = shouldDisplayNetwork, ) } } @@ -70,9 +70,9 @@ private fun LockedIcon(modifier: Modifier = Modifier) { @Composable private fun BoxScope.ContentIconContainer( - icon: TokenIconState, + icon: CurrencyIconState, + shouldShowTopBadge: Boolean, modifier: Modifier = Modifier, - shouldDisplayNetwork: Boolean = true, ) { val networkBadgeOffset = TangemTheme.dimens.spacing4 val (alpha, colorFilter) = remember(icon.isGrayscale) { @@ -90,19 +90,21 @@ private fun BoxScope.ContentIconContainer( colorFilter = colorFilter, ) - if (icon.networkBadgeIconResId != null && shouldDisplayNetwork) { - NetworkBadge( + if (icon.topBadgeIconResId != null && shouldShowTopBadge) { + TopBadge( modifier = Modifier .offset(x = networkBadgeOffset, y = -networkBadgeOffset) .align(Alignment.TopEnd), - iconResId = requireNotNull(icon.networkBadgeIconResId), + iconResId = requireNotNull(icon.topBadgeIconResId), alpha = alpha, colorFilter = colorFilter, ) } if (icon.showCustomBadge) { - CustomBadge(modifier = Modifier.align(Alignment.BottomEnd)) + BottomBadge( + modifier = Modifier.align(Alignment.BottomEnd), + ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt similarity index 75% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 0f7ff350de..85e49532fc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable @@ -10,11 +10,11 @@ import androidx.compose.ui.graphics.Color * [REDACTED_TODO_COMMENT] */ @Immutable -sealed class TokenIconState { +sealed class CurrencyIconState { abstract val isGrayscale: Boolean abstract val showCustomBadge: Boolean - abstract val networkBadgeIconResId: Int? + abstract val topBadgeIconResId: Int? /** * Represents a coin icon. @@ -29,16 +29,16 @@ sealed class TokenIconState { @DrawableRes val fallbackResId: Int, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, - ) : TokenIconState() { + ) : CurrencyIconState() { - override val networkBadgeIconResId: Int? = null + override val topBadgeIconResId: Int? = null } /** * Represents a token icon. * * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. * @property fallbackTint The color to be used for tinting the fallback icon. @@ -46,39 +46,39 @@ sealed class TokenIconState { */ data class TokenIcon( val url: String?, - @DrawableRes override val networkBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, val fallbackTint: Color, val fallbackBackground: Color, - ) : TokenIconState() + ) : CurrencyIconState() /** * Represents a custom token icon. * * @property tint The color to be used for tinting the icon. * @property background The background color to be used for the icon. - * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. */ data class CustomTokenIcon( val tint: Color, val background: Color, - @DrawableRes override val networkBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int, override val isGrayscale: Boolean, override val showCustomBadge: Boolean = true, - ) : TokenIconState() + ) : CurrencyIconState() - data object Loading : TokenIconState() { + data object Loading : CurrencyIconState() { override val isGrayscale: Boolean = false override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null + override val topBadgeIconResId: Int? = null } - data object Locked : TokenIconState() { + data object Locked : CurrencyIconState() { override val isGrayscale: Boolean = false override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null + override val topBadgeIconResId: Int? = null } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt similarity index 92% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt index e21ce3274c..09e597f7a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.res.TangemTheme @Composable -internal fun NetworkBadge( +internal fun TopBadge( @DrawableRes iconResId: Int, alpha: Float, colorFilter: ColorFilter?, @@ -41,7 +41,7 @@ internal fun NetworkBadge( } @Composable -internal fun CustomBadge(modifier: Modifier = Modifier) { +internal fun BottomBadge(modifier: Modifier = Modifier) { Box( modifier = modifier .size(TangemTheme.dimens.size12) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt similarity index 81% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt index 3e62e98992..c331fc8a1d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt @@ -1,6 +1,6 @@ -package com.tangem.core.ui.components.currency.tokenicon.converter +package com.tangem.core.ui.components.currency.icon.converter -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon @@ -9,11 +9,11 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** - * Converts [CryptoCurrencyStatus] to [TokenIconState] + * Converts [CryptoCurrencyStatus] to [CurrencyIconState] */ -class CryptoCurrencyToIconStateConverter : Converter { +class CryptoCurrencyToIconStateConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): TokenIconState { + override fun convert(value: CryptoCurrencyStatus): CurrencyIconState { return when (val currency = value.currency) { is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError) is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError) @@ -24,7 +24,7 @@ class CryptoCurrencyToIconStateConverter : Converter getIconStateForCoin( coin = currency, @@ -41,7 +41,7 @@ class CryptoCurrencyToIconStateConverter : Converter getIconStateForCoin(currency, isUnreachable = false) is CryptoCurrency.Token -> getIconStateForToken(currency, isErrorStatus = false) @@ -53,8 +53,8 @@ class CryptoCurrencyToIconStateConverter : Converter { - TokenIconState.CoinIcon( + CurrencyIconState.CoinIcon( url = currency.iconUrl, fallbackResId = currency.networkIconResId, isGrayscale = !isAvailable, @@ -90,11 +90,11 @@ class TokensDataConverter( val isGrayscale = currency.network.isTestnet val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) val tint = getTintForTokenIcon(background) - TokenIconState.TokenIcon( + CurrencyIconState.TokenIcon( url = currency.iconUrl, isGrayscale = !isAvailable, showCustomBadge = currency.isCustom, - networkBadgeIconResId = currency.networkIconResId, + topBadgeIconResId = currency.networkIconResId, fallbackTint = tint, fallbackBackground = background, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 585afb1921..d99e967096 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -20,7 +20,7 @@ sealed class TokenToSelectState { val id: String, val name: String, val symbol: String, - val tokenIcon: TokenIconState, + val tokenIcon: CurrencyIconState, val available: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, ) : TokenToSelectState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index cbe749c6a8..5d05e969b8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference data class SwapSuccessStateHolder( @@ -16,8 +16,8 @@ data class SwapSuccessStateHolder( val toTokenAmount: TextReference, val fromTokenFiatAmount: TextReference, val toTokenFiatAmount: TextReference, - val fromTokenIconState: TokenIconState?, - val toTokenIconState: TokenIconState?, + val fromTokenIconState: CurrencyIconState?, + val toTokenIconState: CurrencyIconState?, val onExploreButtonClick: () -> Unit, val onStatusButtonClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 02a25af845..a4a31d2962 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -4,7 +4,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.bottomsheets.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index ff4b879c0e..6b753b705a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -18,16 +18,19 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerW2 import com.tangem.core.ui.components.appbar.ExpandableSearchView -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.SwapSelectTokenStateHolder +import com.tangem.feature.swap.models.TokenBalanceData +import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R import com.tangem.utils.Strings import kotlinx.collections.immutable.ImmutableList @@ -212,7 +215,7 @@ private fun TokenItem( ), verticalAlignment = Alignment.CenterVertically, ) { - TokenIcon( + CurrencyIcon( state = token.tokenIcon, shouldDisplayNetwork = true, ) @@ -280,7 +283,7 @@ private fun TokenItem( } private val token = TokenToSelectState.TokenToSelect( - tokenIcon = TokenIconState.CoinIcon( + tokenIcon = CurrencyIconState.CoinIcon( url = "", fallbackResId = 0, isGrayscale = false, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 11ade5db09..68735250d9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImage @@ -65,7 +65,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad title = TextReference.Res(R.string.swapping_from_title), subtitle = state.fromTokenAmount, caption = state.fromTokenFiatAmount, - tokenIconState = state.fromTokenIconState ?: TokenIconState.Loading, + tokenIconState = state.fromTokenIconState ?: CurrencyIconState.Loading, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), @@ -76,7 +76,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad title = TextReference.Res(R.string.swapping_to_title), subtitle = state.toTokenAmount, caption = state.toTokenFiatAmount, - tokenIconState = state.toTokenIconState ?: TokenIconState.Loading, + tokenIconState = state.toTokenIconState ?: CurrencyIconState.Loading, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), @@ -161,8 +161,8 @@ private val state = SwapSuccessStateHolder( toTokenAmount = TextReference.Str("1 000 MATIC"), fromTokenFiatAmount = TextReference.Str("1 000 $"), toTokenFiatAmount = TextReference.Str("1 000 $"), - fromTokenIconState = TokenIconState.Loading, - toTokenIconState = TokenIconState.Loading, + fromTokenIconState = CurrencyIconState.Loading, + toTokenIconState = CurrencyIconState.Loading, rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), onExploreButtonClick = {}, onStatusButtonClick = {}, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 89f5925433..52f0679e6c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -1,7 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.ExchangeStatus @@ -23,11 +23,11 @@ internal data class SwapTransactionsState( val toCryptoCurrency: CryptoCurrency, val toCryptoAmount: String, val toFiatAmount: String, - val toCurrencyIcon: TokenIconState, + val toCurrencyIcon: CurrencyIconState, val fromCryptoCurrency: CryptoCurrency, val fromCryptoAmount: String, val fromFiatAmount: String, - val fromCurrencyIcon: TokenIconState, + val fromCurrencyIcon: CurrencyIconState, val showProviderLink: Boolean, val onClick: () -> Unit, val onGoToProviderClick: (String) -> Unit, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 55e6cba9b0..5f8bf6995f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt index 6aee05217a..319e6f44fe 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.InputRowApprox import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -19,8 +19,8 @@ import com.tangem.features.tokendetails.impl.R @Composable internal fun ExchangeEstimate( timestamp: TextReference, - fromTokenIconState: TokenIconState, - toTokenIconState: TokenIconState, + fromTokenIconState: CurrencyIconState, + toTokenIconState: CurrencyIconState, fromCryptoAmount: TextReference, fromCryptoSymbol: String, toCryptoAmount: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 1945621d19..4b2a5a1ab4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.constraintlayout.compose.* import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeStatus @@ -72,8 +72,8 @@ internal fun LazyListScope.swapTransactionsItems( @Composable private fun ExchangeStatusItem( providerName: String, - fromTokenIconState: TokenIconState, - toTokenIconState: TokenIconState, + fromTokenIconState: CurrencyIconState, + toTokenIconState: CurrencyIconState, fromAmount: String, fromSymbol: String, toSymbol: String, @@ -102,7 +102,7 @@ private fun ExchangeStatusItem( top.linkTo(parent.top) }, ) - TokenIcon( + CurrencyIcon( state = fromTokenIconState, shouldDisplayNetwork = false, modifier = Modifier @@ -139,7 +139,7 @@ private fun ExchangeStatusItem( bottom.linkTo(parent.bottom) }, ) - TokenIcon( + CurrencyIcon( state = toTokenIconState, shouldDisplayNetwork = false, modifier = Modifier @@ -205,8 +205,8 @@ private fun ExchangeStatusItemPreview( TangemThemePreview { ExchangeStatusItem( providerName = "ChangeNow", - fromTokenIconState = TokenIconState.Loading, - toTokenIconState = TokenIconState.Loading, + fromTokenIconState = CurrencyIconState.Loading, + toTokenIconState = CurrencyIconState.Loading, fromAmount = amount, fromSymbol = "USDT", toSymbol = "USDT", 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 92abf8f92f..e1f6a29f77 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,7 +1,7 @@ package com.tangem.feature.wallet.presentation.common import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference @@ -67,7 +67,7 @@ internal object WalletPreviewData { } val coinIconState - get() = TokenIconState.CoinIcon( + get() = CurrencyIconState.CoinIcon( url = null, fallbackResId = R.drawable.img_polygon_22, isGrayscale = false, @@ -75,9 +75,9 @@ internal object WalletPreviewData { ) private val tokenIconState - get() = TokenIconState.TokenIcon( + get() = CurrencyIconState.TokenIcon( url = null, - networkBadgeIconResId = R.drawable.img_polygon_22, + topBadgeIconResId = R.drawable.img_polygon_22, fallbackTint = TangemColorPalette.Black, fallbackBackground = TangemColorPalette.Meadow, isGrayscale = false, @@ -85,10 +85,10 @@ internal object WalletPreviewData { ) private val customTokenIconState - get() = TokenIconState.CustomTokenIcon( + get() = CurrencyIconState.CustomTokenIcon( tint = TangemColorPalette.Black, background = TangemColorPalette.Meadow, - networkBadgeIconResId = R.drawable.img_polygon_22, + topBadgeIconResId = R.drawable.img_polygon_22, isGrayscale = false, ) 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 eb3f107e08..01c4e11d48 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 @@ -16,7 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Constraints -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme @@ -49,7 +49,7 @@ internal fun TokenItem( .tokenClickable(state = state) .background(color = TangemTheme.colors.background.primary), ) { - TokenIcon( + CurrencyIcon( state = state.iconState, modifier = Modifier .layoutId(layoutId = LayoutId.ICON) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 5397088c1e..635afd0fc9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.common.preview -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference @@ -16,7 +16,7 @@ import kotlinx.collections.immutable.persistentListOf internal object WalletScreenPreviewData { private val tokenItemState = TokenItemState.Content( id = "1", - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"), @@ -56,7 +56,7 @@ internal object WalletScreenPreviewData { WalletTokensListState.TokensListItemState.Token( state = TokenItemState.Unreachable( id = "3", - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, titleState = TokenItemState.TitleState.Content(text = "Polygon"), onItemClick = {}, onItemLongClick = {}, 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 80fdc10cdc..c5bc2e083e 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 @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType /** Token item state */ @@ -10,7 +10,7 @@ internal sealed class TokenItemState { abstract val id: String - abstract val iconState: TokenIconState + abstract val iconState: CurrencyIconState abstract val titleState: TitleState @@ -23,7 +23,7 @@ internal sealed class TokenItemState { /** Loading token state */ data class Loading( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState.Content, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading @@ -33,7 +33,7 @@ internal sealed class TokenItemState { /** Locked token state */ data class Locked(override val id: String) : TokenItemState() { - override val iconState: TokenIconState = TokenIconState.Locked + override val iconState: CurrencyIconState = CurrencyIconState.Locked override val titleState: TitleState = TitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked @@ -51,7 +51,7 @@ internal sealed class TokenItemState { */ data class Content( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, @@ -69,7 +69,7 @@ internal sealed class TokenItemState { */ data class Draggable( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { @@ -88,7 +88,7 @@ internal sealed class TokenItemState { */ data class Unreachable( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, @@ -108,7 +108,7 @@ internal sealed class TokenItemState { */ data class NoAddress( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, val onItemLongClick: () -> Unit, ) : TokenItemState() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 2288a0c763..93f8267b5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index 73198feee8..c16dc3e022 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter From 2be4782ce3f23d3880e2a8a2c45a92008bbcb896 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 14:29:01 +0400 Subject: [PATCH 110/223] Updated on 2026-08-14 --- .../core/ui/components/rows/BlockchainRow.kt | 120 +++++++++++++---- .../core/ui/components/rows/ChainRow.kt | 73 +++++------ .../core/ui/components/rows/RowComponents.kt | 124 ++++++------------ .../components/rows/model/BlockchainRowUM.kt | 21 +-- .../ui/components/rows/model/ChainRowUM.kt | 19 +-- 5 files changed, 177 insertions(+), 180 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index 8d6e4165e3..d0341755a7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -1,20 +1,22 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon 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.tooling.preview.Preview 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.TangemSwitch import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -32,24 +34,84 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni ), icon = { RowIcon( - modifier = Modifier.size(TangemTheme.dimens.size22), - resId = model.icon.resId, - isColored = model.icon.isColored, - showAccentBadge = model.isAccented, + resId = model.iconResId, + isColored = model.isSelected, + showAccentBadge = model.isMainNetwork, ) }, text = { - RowTitleAndSubtitle( - title = model.name, - subtitle = model.type, - accentTitle = model.usePrimaryTextColor, - accentSubtitle = model.isAccented, + RowText( + mainText = model.name, + secondText = model.type, + accentMainText = model.isSelected, + accentSecondText = model.isMainNetwork, ) }, action = action, ) } +@Composable +private fun RowIcon( + @DrawableRes resId: Int, + isColored: Boolean, + showAccentBadge: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.size(TangemTheme.dimens.size24), + ) { + if (isColored) { + Image( + modifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size22), + painter = painterResource(id = resId), + contentDescription = null, + ) + } else { + Icon( + modifier = Modifier + .align(Alignment.Center) + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .size(TangemTheme.dimens.size22), + painter = painterResource(id = resId), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + + if (showAccentBadge) { + Badge(modifier = Modifier.align(Alignment.TopEnd)) + } + } +} + +@Composable +private fun Badge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size8) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size5) + .background( + color = TangemTheme.colors.icon.accent, + shape = CircleShape, + ), + ) + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -74,25 +136,25 @@ private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvid private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider( collection = listOf( BlockchainRowUM( - name = stringReference("BNB BEACON CHAIN"), - type = stringReference("BEP20"), - icon = BlockchainRowUM.Icon(R.drawable.ic_bsc_16, isColored = false), - isAccented = true, - usePrimaryTextColor = false, + name = "BNB BEACON CHAIN", + type = "BEP20", + iconResId = R.drawable.img_bsc_22, + isMainNetwork = true, + isSelected = true, ), BlockchainRowUM( - name = stringReference("1234567890111213141516171819"), - type = stringReference("BEP20"), - icon = BlockchainRowUM.Icon(R.drawable.ic_bsc_16, isColored = false), - isAccented = false, - usePrimaryTextColor = true, + name = "1234567890111213141516171819", + type = "BEP20", + iconResId = R.drawable.ic_bsc_16, + isMainNetwork = true, + isSelected = false, ), BlockchainRowUM( - name = stringReference("BNB BEACON CHAIN"), - type = stringReference("1234567890111213141516171819"), - icon = BlockchainRowUM.Icon(R.drawable.img_bsc_22, isColored = true), - isAccented = false, - usePrimaryTextColor = false, + name = "BNB BEACON CHAIN", + type = "1234567890111213141516171819", + iconResId = R.drawable.ic_bsc_16, + isMainNetwork = false, + isSelected = false, ), ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt index 24645b9c17..175ace75f5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -2,10 +2,7 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable @@ -15,8 +12,10 @@ 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.R +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.model.ChainRowUM -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -32,20 +31,24 @@ fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composab vertical = TangemTheme.dimens.spacing12, horizontal = TangemTheme.dimens.spacing14, ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), icon = { - RowIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - resId = model.icon.resId, - isColored = model.icon.isColored, - showAccentBadge = false, + CurrencyIcon( + state = model.icon, + shouldDisplayNetwork = true, ) }, text = { - RowTitleAndSubtitle( - title = model.name, - subtitle = model.type, - accentTitle = true, - accentSubtitle = false, + RowText( + mainText = model.name, + secondText = model.type, + subtitle = if (model.showCustom) { + resourceReference(R.string.common_custom) + } else { + null + }, + accentMainText = true, + accentSecondText = false, ) }, action = action, @@ -81,36 +84,28 @@ private fun Preview_ChainRow(@PreviewParameter(ChainRowParameterProvider::class) private class ChainRowParameterProvider : CollectionPreviewParameterProvider( collection = listOf( ChainRowUM( - name = stringReference("Cardano"), - type = stringReference("ADA"), - icon = ChainRowUM.Icon( - resId = R.drawable.img_cardano_22, - isColored = true, - ), + name = "Cardano", + type = "ADA", + icon = CurrencyIconState.Locked, + showCustom = true, ), ChainRowUM( - name = stringReference("Binance"), - type = stringReference("BNB"), - icon = ChainRowUM.Icon( - resId = R.drawable.ic_bsc_16, - isColored = false, - ), + name = "Binance", + type = "BNB", + icon = CurrencyIconState.Locked, + showCustom = false, ), ChainRowUM( - name = stringReference("123456789010111213141516"), - type = stringReference("BNB"), - icon = ChainRowUM.Icon( - resId = R.drawable.ic_bsc_16, - isColored = false, - ), + name = "123456789010111213141516", + type = "BNB", + icon = CurrencyIconState.Locked, + showCustom = true, ), ChainRowUM( - name = stringReference("123456789010111213141516"), - type = stringReference("123456789010111213141516"), - icon = ChainRowUM.Icon( - resId = R.drawable.ic_bsc_16, - isColored = false, - ), + name = "123456789010111213141516", + type = "123456789010111213141516", + icon = CurrencyIconState.Locked, + showCustom = false, ), ), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt index e0ac20cdc2..f93f42b41d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -1,18 +1,13 @@ package com.tangem.core.ui.components.rows -import androidx.annotation.DrawableRes -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text 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.TextOverflow import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -22,11 +17,12 @@ internal inline fun RowContentContainer( text: @Composable BoxScope.() -> Unit, action: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { Row( modifier = modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8, Alignment.Start), + horizontalArrangement = horizontalArrangement, ) { Box( contentAlignment = Alignment.Center, @@ -41,7 +37,7 @@ internal inline fun RowContentContainer( ) Box( modifier = Modifier - .requiredWidthIn(max = TangemTheme.dimens.size48) + .requiredWidthIn(max = TangemTheme.dimens.size80) .heightIn(min = TangemTheme.dimens.size24), contentAlignment = Alignment.CenterEnd, content = action, @@ -50,90 +46,50 @@ internal inline fun RowContentContainer( } @Composable -internal fun RowTitleAndSubtitle( - title: TextReference, - subtitle: TextReference, - accentTitle: Boolean, - accentSubtitle: Boolean, +internal fun RowText( + mainText: String, + secondText: String, + accentMainText: Boolean, + accentSecondText: Boolean, modifier: Modifier = Modifier, + subtitle: TextReference? = null, ) { - Row( + Column( modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), ) { - Text( - modifier = Modifier.weight(weight = 10f, fill = false), - text = title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = if (accentTitle) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - - Text( - modifier = Modifier.weight(weight = 4f, fill = false), - text = subtitle.resolveReference(), - style = TangemTheme.typography.body2, - color = if (accentSubtitle) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } -} - -@Composable -internal fun RowIcon( - @DrawableRes resId: Int, - isColored: Boolean, - showAccentBadge: Boolean, - modifier: Modifier = Modifier, -) { - Box(modifier = modifier) { - if (isColored) { - Image( - modifier = Modifier.matchParentSize(), - painter = painterResource(id = resId), - contentDescription = null, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.weight(weight = 10f, fill = false), + text = mainText, + style = TangemTheme.typography.subtitle2, + color = if (accentMainText) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) - } else { - Icon( - modifier = Modifier - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ) - .matchParentSize(), - painter = painterResource(id = resId), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, + + Text( + modifier = Modifier.weight(weight = 4f, fill = false), + text = secondText, + style = TangemTheme.typography.body2, + color = if (accentSecondText) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } - if (showAccentBadge) { - Badge(modifier = Modifier.align(Alignment.TopEnd)) + if (!subtitle.isNullOrEmpty()) { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } -} - -@Composable -private fun Badge(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size6) - .background( - color = TangemTheme.colors.background.primary, - shape = CircleShape, - ), - ) { - Box( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing1) - .matchParentSize() - .background( - color = TangemTheme.colors.icon.accent, - shape = CircleShape, - ), - ) - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt index dbb8ae529d..f8e756a9ae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt @@ -1,21 +1,12 @@ package com.tangem.core.ui.components.rows.model -import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference @Immutable data class BlockchainRowUM( - val name: TextReference, - val type: TextReference, - val icon: Icon, - val isAccented: Boolean, - val usePrimaryTextColor: Boolean, -) { - - @Immutable - data class Icon( - @DrawableRes val resId: Int, - val isColored: Boolean, - ) -} \ No newline at end of file + val name: String, + val type: String, + val iconResId: Int, + val isMainNetwork: Boolean, + val isSelected: Boolean, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt index 58e337a805..c0c3764c5e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt @@ -1,19 +1,12 @@ package com.tangem.core.ui.components.rows.model -import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.currency.icon.CurrencyIconState @Immutable data class ChainRowUM( - val name: TextReference, - val type: TextReference, - val icon: Icon, -) { - - @Immutable - data class Icon( - @DrawableRes val resId: Int, - val isColored: Boolean, - ) -} \ No newline at end of file + val name: String, + val type: String, + val icon: CurrencyIconState, + val showCustom: Boolean, +) \ No newline at end of file From 32e3adabfadb7be245b47a91865f56c3af1a6821 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 13:35:15 +0400 Subject: [PATCH 111/223] Updated on 2026-08-14 --- .../core/ui/components/fields/SearchBar.kt | 162 ++++++++++++++++++ .../components/fields/entity/SearchBarUM.kt | 11 ++ .../com/tangem/core/ui/res/TangemShapes.kt | 2 + 3 files changed, 175 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt new file mode 100644 index 0000000000..0c669a23ad --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -0,0 +1,162 @@ +package com.tangem.core.ui.components.fields + +import android.content.res.Configuration +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarColors) { + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + TextField( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size48) + .onFocusChanged { + if (it.isFocused) { + state.onActiveChange(true) + } else { + state.onActiveChange(false) + } + }, + value = state.query, + onValueChange = state.onQueryChange, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Search, + ), + keyboardActions = KeyboardActions( + onSearch = { + keyboardController?.hide() + focusManager.clearFocus() + }, + ), + singleLine = true, + maxLines = 1, + textStyle = TangemTheme.typography.body2, + leadingIcon = { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_search_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + }, + trailingIcon = { + ClearButton( + state = state, + focusManager = focusManager, + keyboardController = keyboardController, + ) + }, + placeholder = { + Text( + text = state.placeholderText.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + shape = TangemTheme.shapes.roundedCornersXLarge, + colors = colors, + ) +} + +@Composable +private fun ClearButton( + state: SearchBarUM, + focusManager: FocusManager, + keyboardController: SoftwareKeyboardController?, + modifier: Modifier = Modifier, +) { + if (state.query.isNotEmpty() || state.isActive) { + IconButton( + modifier = modifier, + onClick = { + if (state.query.isNotEmpty()) { + state.onQueryChange("") + } + focusManager.clearFocus() + keyboardController?.hide() + state.onActiveChange(false) + }, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = painterResource(id = R.drawable.ic_close_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } +} + +private val TangemSearchBarColors: TextFieldColors + @Composable + get() = TextFieldDefaults.colors().copy( + focusedContainerColor = TangemTheme.colors.field.primary, + unfocusedContainerColor = TangemTheme.colors.field.primary, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) + +// region Previews +@Preview(widthDp = 328) +@Preview(widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TokensSearchBar(@PreviewParameter(SearchBarkConfigProvider::class) state: SearchBarUM) { + TangemThemePreview { + SearchBar(state) + } +} + +private class SearchBarkConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "BTC", + onQueryChange = {}, + isActive = true, + onActiveChange = {}, + ), + SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + ), +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt new file mode 100644 index 0000000000..d2ac8fcc8b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.components.fields.entity + +import com.tangem.core.ui.extensions.TextReference + +data class SearchBarUM( + val placeholderText: TextReference, + val query: String, + val onQueryChange: (String) -> Unit, + val isActive: Boolean, + val onActiveChange: (Boolean) -> Unit, +) \ No newline at end of file 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 8bf99452ed..5da8defa94 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 @@ -12,6 +12,7 @@ data class TangemShapes internal constructor( val roundedCornersMedium: Shape, val roundedCornersXMedium: Shape, val roundedCornersLarge: Shape, + val roundedCornersXLarge: Shape, val bottomSheet: Shape, val bottomSheetLarge: Shape, ) { @@ -22,6 +23,7 @@ data class TangemShapes internal constructor( roundedCornersMedium = RoundedCornerShape(size = dimens.radius12), roundedCornersXMedium = RoundedCornerShape(size = dimens.radius16), roundedCornersLarge = RoundedCornerShape(size = dimens.radius28), + roundedCornersXLarge = RoundedCornerShape(size = dimens.radius36), bottomSheet = RoundedCornerShape( topStart = dimens.radius16, topEnd = dimens.radius16, From 0233011b885f7e9fb373f8a30d728e423f91c0c1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 8 Jul 2024 10:35:55 +0100 Subject: [PATCH 112/223] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1afb6daf70..1a01fb3200 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -37,10 +37,10 @@ }, { "name": "DETAILS_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.13.0" }, { "name": "PUSH_NOTIFICATIONS_ENABLED", - "version": "undefined" + "version": "5.13.0" } ] From 46ff455a6f66f3c851687c22d02e86ab4ff98810 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jul 2024 12:44:23 +0300 Subject: [PATCH 113/223] Updated on 2026-08-14 --- .../main/java/com/tangem/datasource/config/ConfigManagerImpl.kt | 1 + .../main/java/com/tangem/datasource/config/models/JsonModels.kt | 1 + .../blockchainsdk/converters/BlockchainSDKConfigConverter.kt | 1 + .../main/java/com/tangem/blockchainsdk/utils/ProviderTypeExt.kt | 1 + 4 files changed, 4 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 6bda8dd093..36bba25d83 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -103,6 +103,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { polygonScanApiKey = configValues.polygonScanApiKey, bittensorDwellirApiKey = configValues.bittensorDwellirApiKey, bittensorOnfinalityApiKey = configValues.bittensorOnfinalityKey, + koinosProApiKey = configValues.koinosProApiKey, ), amplitudeApiKey = configValues.amplitudeApiKey, sprinklr = configValues.sprinklr, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 685b4e3ffe..a6929291e1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -46,6 +46,7 @@ class ConfigValueModel( val stakeKitApiKey: String?, @Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?, @Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?, + @Json(name = "koinosProApiKey") val koinosProApiKey: String?, ) @JsonClass(generateAdapter = true) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt index 5d713313ea..f8e769ebd1 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -41,6 +41,7 @@ internal object BlockchainSDKConfigConverter : Converter ProviderType.Tron.TronGrid "dwellirBittensor" -> ProviderType.Bittensor.Dwellir "onfinalityBittensor" -> ProviderType.Bittensor.Onfinality + "koinospro" -> ProviderType.Koinos.KoinosPro else -> { Timber.e("Private provider with name $name is not supported") null From e3eada68fd54aa589ea62e8787c4612417b81d83 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jul 2024 12:45:17 +0300 Subject: [PATCH 114/223] Updated on 2026-08-14 --- .../api/markets/TangemTechMarketsApi.kt | 41 ++++++ .../response/TokenMarketChartListResponse.kt | 8 ++ .../response/TokenMarketChartResponse.kt | 9 ++ .../response/TokenMarketDetailsResponse.kt | 132 ++++++++++++++++++ .../response/TokenMarketListResponse.kt | 43 ++++++ .../api/tangemTech/models/QuotesResponse.kt | 6 +- .../com/tangem/datasource/di/NetworkModule.kt | 18 +++ .../com/tangem/pagination/BatchFetchResult.kt | 2 +- .../com/tangem/pagination/BatchListSource.kt | 12 +- .../tangem/pagination/BatchListSourceFlow.kt | 17 +++ .../fetcher/LimitOffsetBatchFetcher.kt | 10 +- data/markets/.gitignore | 1 + data/markets/build.gradle.kts | 32 +++++ .../markets/DefaultMarketsTokenRepository.kt | 121 ++++++++++++++++ .../converters/TokenListChartConverter.kt | 16 +++ .../converters/TokenListConfigConverters.kt | 28 ++++ .../converters/TokenMarketChartsConverter.kt | 33 +++++ .../converters/TokenMarketListConverter.kt | 32 +++++ .../converters/TokenQuotesConverter.kt | 30 ++++ .../data/markets/di/MarketsDataModule.kt | 31 ++++ .../data/tokens/utils/QuotesConverter.kt | 2 +- domain/markets/.gitignore | 1 + domain/markets/build.gradle.kts | 20 +++ domain/markets/models/.gitignore | 1 + domain/markets/models/build.gradle.kts | 12 ++ .../domain/markets/PriceChangeInterval.kt | 5 + .../com/tangem/domain/markets/TokenChart.kt | 13 ++ .../com/tangem/domain/markets/TokenMarket.kt | 33 +++++ .../domain/markets/TokenMarketListConfig.kt | 18 +++ .../markets/TokenMarketUpdateRequest.kt | 13 ++ .../com/tangem/domain/markets/TokenQuotes.kt | 8 ++ .../repositories/MarketsTokenRepository.kt | 14 ++ .../com/tangem/domain/tokens/model/Quote.kt | 3 - gradle.properties | 2 +- settings.gradle.kts | 3 + 35 files changed, 757 insertions(+), 13 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt create mode 100644 data/markets/.gitignore create mode 100644 data/markets/build.gradle.kts create mode 100644 data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt create mode 100644 domain/markets/.gitignore create mode 100644 domain/markets/build.gradle.kts create mode 100644 domain/markets/models/.gitignore create mode 100644 domain/markets/models/build.gradle.kts create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt create mode 100644 domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt new file mode 100644 index 0000000000..0ded78dbab --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -0,0 +1,41 @@ +package com.tangem.datasource.api.markets + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.markets.models.response.* +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface TangemTechMarketsApi { + + @Suppress("LongParameterList") + @GET("coins/list") + suspend fun getCoinsList( + @Query("currency") currency: String, + @Query("interval") interval: String, + @Query("offset") offset: Int, + @Query("limit") limit: Int, + @Query("order") order: String, + @Query("general_coins") generalCoins: Boolean, + @Query("search") search: String?, + ): ApiResponse + + @GET("coins/{coin_id}") + suspend fun getCoinMarketData( + @Path("coin_id") coinId: String, + @Query("currency") currency: String, + ): ApiResponse + + @GET("coins/{coin_id}/history") + suspend fun getCoinChart( + @Query("currency") currency: String, + @Query("interval") interval: String, + ): ApiResponse + + @GET("coins/history_preview") + suspend fun getCoinsListCharts( + @Query("coin_ids") coinIds: List, + @Query("currency") currency: String, + @Query("interval") interval: String, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt new file mode 100644 index 0000000000..0d1667ee24 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json + +class TokenMarketChartListResponse( + @Json(name = "tokens") + val tokens: Map, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt new file mode 100644 index 0000000000..777bbc1228 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class TokenMarketChartResponse( + @Json(name = "prices") + val prices: Map, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt new file mode 100644 index 0000000000..227496a66e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt @@ -0,0 +1,132 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class TokenMarketDetailsResponse( + @Json(name = "id") + val id: String, + @Json(name = "name") + val name: String, + @Json(name = "symbol") + val symbol: String, + @Json(name = "active") + val active: Boolean, + @Json(name = "current_price") + val currentPrice: BigDecimal, + @Json(name = "price_change_percentage") + val priceChangePercentage: PriceChangePercentage, + @Json(name = "networks") + val networks: List, + @Json(name = "short_description") + val shortDescription: String?, + @Json(name = "full_description") + val fullDescription: String?, + @Json(name = "insights") + val insights: List?, + @Json(name = "metrics") + val metrics: Metrics, + @Json(name = "links") + val links: Links, + @Json(name = "price_performance") + val pricePerformance: PricePerformance, +) { + data class PriceChangePercentage( + @Json(name = "24h") + val h24: BigDecimal, + @Json(name = "1w") + val week1: BigDecimal, + @Json(name = "1m") + val month1: BigDecimal, + @Json(name = "3m") + val month3: BigDecimal, + @Json(name = "6m") + val month6: BigDecimal, + @Json(name = "1y") + val year1: BigDecimal, + @Json(name = "all_time") + val allTime: BigDecimal, + ) + + data class Network( + @Json(name = "network_id") + val networkId: String, + @Json(name = "exchangeable") + val exchangeable: Boolean, + @Json(name = "contract_address") + val contractAddress: String, + @Json(name = "decimalCount") + val decimalCount: Int, + ) + + data class Insight( + @Json(name = "holders_change") + val holdersChange: Change, + @Json(name = "liquidity_change") + val liquidityChange: Change, + @Json(name = "buy_pressure_change") + val buyPressureChange: Change, + @Json(name = "experienced_buyer_change") + val experiencedBuyerChange: Change, + ) { + data class Change( + @Json(name = "1d") + val day1: Int, + @Json(name = "1w") + val week1: Int, + @Json(name = "1m") + val month1: Int, + ) + } + + data class Metrics( + @Json(name = "market_rating") + val marketRating: Int, + @Json(name = "circulating_supply") + val circulatingSupply: BigDecimal, + @Json(name = "market_cap") + val marketCap: BigDecimal, + @Json(name = "volume_24h") + val volume24h: BigDecimal, + @Json(name = "total_supply") + val totalSupply: BigDecimal, + @Json(name = "fully_diluted_valuation") + val fullyDilutedValuation: BigDecimal, + ) + + data class Links( + @Json(name = "official_links") + val officialLinks: List = emptyList(), + @Json(name = "social") + val social: List = emptyList(), + @Json(name = "repository") + val repository: List = emptyList(), + @Json(name = "blockchain_site") + val blockchainSite: List = emptyList(), + ) + + data class Link( + @Json(name = "title") + val title: String?, + @Json(name = "id") + val id: String, + @Json(name = "link") + val url: String, + ) + + data class PricePerformance( + @Json(name = "high_price") + val highPrice: Price, + @Json(name = "low_price") + val lowPrice: Price, + ) { + data class Price( + @Json(name = "24h") + val h24: BigDecimal, + @Json(name = "1m") + val month1: BigDecimal, + @Json(name = "all_time") + val allTime: BigDecimal, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt new file mode 100644 index 0000000000..4157bd2bd6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -0,0 +1,43 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class TokenMarketListResponse( + @Json(name = "imageHost") + val imageHost: String, + @Json(name = "tokens") + val tokens: List, + @Json(name = "total") + val total: Int, + @Json(name = "limit") + val limit: Int, + @Json(name = "offset") + val offset: Int, +) { + data class Token( + @Json(name = "id") + val id: String, + @Json(name = "name") + val name: String, + @Json(name = "symbol") + val symbol: String, + @Json(name = "current_price") + val currentPrice: BigDecimal, + @Json(name = "price_change_percentage") + val priceChangePercentage: PriceChangePercentage, + @Json(name = "market_rating") + val marketRating: Int?, + @Json(name = "market_cap") + val marketCap: BigDecimal?, + ) { + data class PriceChangePercentage( + @Json(name = "24h") + val h24: BigDecimal, + @Json(name = "1w") + val week1: BigDecimal, + @Json(name = "30d") + val day30: BigDecimal, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt index 2155d0bf77..e1e9e07e40 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt @@ -12,6 +12,10 @@ data class QuotesResponse( @Json(name = "price") val price: BigDecimal?, @Json(name = "priceChange24h") - val priceChange: BigDecimal?, + val priceChange24h: BigDecimal?, + @Json(name = "priceChange1w") + val priceChange1w: BigDecimal?, + @Json(name = "priceChange30d") + val priceChange30d: BigDecimal?, ) } \ 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 ed7b6b5621..d2b395d5f9 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 @@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 @@ -128,6 +129,23 @@ class NetworkModule { ) } + @Provides + @DevTangemApi + @Singleton + fun provideCoinMarketsApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechMarketsApi { + return provideTangemTechApiInternal( + moshi = moshi, + context = context, + appVersionProvider = appVersionProvider, + baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, + requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)), + ) + } + private inline fun provideTangemTechApiInternal( moshi: Moshi, context: Context, diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index a247310b9c..a358df8bf2 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -16,7 +16,7 @@ sealed class BatchFetchResult { */ data class Success( val data: TData, - val last: Boolean = false, + val last: Boolean, ) : BatchFetchResult() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index fc1d0bd1c2..77e58eafcf 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -269,10 +269,14 @@ private class DefaultBatchListSource val batches = state.value.data val batchesToUpdate = batches.filter { action.keys.contains(it.key) } - val result = updateFetcher.fetchUpdate( - toUpdate = batchesToUpdate, - updateRequest = action.updateRequest, - ) + val result = try { + updateFetcher.fetchUpdate( + toUpdate = batchesToUpdate, + updateRequest = action.updateRequest, + ) + } catch (t: Throwable) { + BatchUpdateResult.Error(t) + } if (result is BatchUpdateResult.Success) { state.update { currentState -> diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt new file mode 100644 index 0000000000..258a20b8d0 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt @@ -0,0 +1,17 @@ +package com.tangem.pagination + +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +fun BatchListSource.toBatchFlow() = + object : BatchFlow { + override val state: StateFlow> + get() = this@toBatchFlow.state + override val updateResults: SharedFlow>> + get() = this@toBatchFlow.updateResults + } + +interface BatchFlow { + val state: StateFlow> + val updateResults: SharedFlow>> +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 01059c4676..3dd04c24c7 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -35,7 +35,10 @@ class LimitOffsetBatchFetcher( request = requestParams, ) - val res = fetch(req) + val res = runCatching { + fetch(req) + }.getOrElse { BatchFetchResult.Error(it) } + lastRequest.value = req return res } @@ -61,7 +64,10 @@ class LimitOffsetBatchFetcher( last } - val res = fetch(req) + val res = runCatching { + fetch(req) + }.getOrElse { BatchFetchResult.Error(it) } + lastRequest.value = req return res } diff --git a/data/markets/.gitignore b/data/markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts new file mode 100644 index 0000000000..8e9347ad54 --- /dev/null +++ b/data/markets/build.gradle.kts @@ -0,0 +1,32 @@ +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.markets" +} + +dependencies { + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.core.pagination) + implementation(projects.domain.tokens.models) + implementation(projects.domain.markets) + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Others dependencies + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + + implementation(projects.libs.blockchainSdk) + // endregion +} diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt new file mode 100644 index 0000000000..65ebc717bb --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -0,0 +1,121 @@ +package com.tangem.data.markets + +import com.tangem.data.markets.converters.* +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.markets.* +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.pagination.* +import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultMarketsTokenRepository( + private val marketsApi: TangemTechMarketsApi, + private val tangemTechApi: TangemTechApi, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : MarketsTokenRepository { + + private val tokenListConverter = TokenMarketListConverter() + private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) + private val tokenQuotesConverter = TokenQuotesConverter() + + private val tokenMarketsFetcher + get() = LimitOffsetBatchFetcher>( + prefetchDistance = 50, + batchSize = 30, + fetch = { params -> + withContext(dispatcherProvider.io) { + val res = marketsApi.getCoinsList( + currency = params.request.fiatPriceCurrency, + interval = params.request.priceChangeInterval.toRequestParam(), + order = params.request.priceChangeInterval.toRequestParam(), + search = params.request.searchText, + generalCoins = params.request.showUnder100kMarketCapTokens.not(), + offset = params.offset, + limit = params.limit, + ).getOrThrow() + + val last = res.tokens.size < params.limit + + BatchFetchResult.Success( + data = tokenListConverter.convert(res), + last = last, + ) + } + }, + ) + + private val tokenMarketsUpdateFetcher + get() = BatchUpdateFetcher, TokenMarketUpdateRequest> { toUpdate, updateRequest -> + withContext(dispatcherProvider.io) { + val idsToUpdate = toUpdate.map { batch -> + batch.data.map { it.id } + }.flatten() + + val updatedBatches = when (updateRequest) { + is TokenMarketUpdateRequest.UpdateChart -> { + val res = marketsApi.getCoinsListCharts( + coinIds = idsToUpdate, + interval = updateRequest.interval.toRequestParam(), + currency = updateRequest.currency, + ).getOrThrow() + + toUpdate.map { batch -> + batch.copy( + data = batch.data.map { + it.copy( + tokenCharts = tokenListChartsConverter.convert( + chartsToCopy = it.tokenCharts, + tokenId = it.id, + interval = updateRequest.interval, + value = res, + ), + ) + }, + ) + } + } + is TokenMarketUpdateRequest.UpdateQuotes -> { + val quotesRes = tangemTechApi.getQuotes( + currencyId = updateRequest.currencyId, + coinIds = idsToUpdate.joinToString(separator = ","), + fields = quoteFields.joinToString(separator = ","), + ).getOrThrow() + + toUpdate.map { batch -> + batch.copy( + data = batch.data.map { + it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) + }, + ) + } + } + } + + BatchUpdateResult.Success(updatedBatches) + } + } + + override suspend fun getTokenListFlow( + batchingContext: BatchingContext, + ): BatchFlow, TokenMarketUpdateRequest> { + return BatchListSource( + fetchDispatcher = dispatcherProvider.io, + context = batchingContext, + generateNewKey = { it.size }, + batchFetcher = tokenMarketsFetcher, + updateFetcher = tokenMarketsUpdateFetcher, + ).toBatchFlow() + } + + companion object { + private val quoteFields = listOf( + "price", + "priceChange24h", + "priceChange1w", + "priceChange30d", + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt new file mode 100644 index 0000000000..0b4cf678c6 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketChartResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenChart + +class TokenListChartConverter { + + fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart { + return TokenChart( + interval = interval, + priceY = value.prices.values.toList(), + timeStamp = value.prices.keys.toList(), + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt new file mode 100644 index 0000000000..eee63ef4e9 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -0,0 +1,28 @@ +package com.tangem.data.markets.converters + +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarketListConfig + +fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) { + TokenMarketListConfig.Interval.H24 -> "24h" + TokenMarketListConfig.Interval.WEEK -> "1w" + TokenMarketListConfig.Interval.MONTH -> "30d" +} + +fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { + TokenMarketListConfig.Order.ByRating -> "rating" + TokenMarketListConfig.Order.Trending -> "trending" + TokenMarketListConfig.Order.Buyers -> "buyers" + TokenMarketListConfig.Order.TopGainers -> "gainers" + TokenMarketListConfig.Order.TopLosers -> "losers" +} + +fun PriceChangeInterval.toRequestParam(): String = when (this) { + PriceChangeInterval.H24 -> "24h" + PriceChangeInterval.WEEK -> "1w" + PriceChangeInterval.MONTH -> "1m" + PriceChangeInterval.MONTH3 -> "3m" + PriceChangeInterval.MONTH6 -> "6m" + PriceChangeInterval.YEAR -> "1y" + PriceChangeInterval.ALL_TIME -> "all" +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt new file mode 100644 index 0000000000..aade2d2073 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarket + +class TokenMarketChartsConverter( + private val tokenListChartConverter: TokenListChartConverter, +) { + + fun convert( + chartsToCopy: TokenMarket.Charts, + tokenId: String, + interval: PriceChangeInterval, + value: TokenMarketChartListResponse, + ): TokenMarket.Charts { + val prices = requireNotNull(value.tokens[tokenId]) { + "$tokenId is not found in the response. This shouldn't have happened." + } + return when (interval) { + PriceChangeInterval.H24 -> chartsToCopy.copy( + h24 = tokenListChartConverter.convert(interval, prices), + ) + PriceChangeInterval.WEEK -> chartsToCopy.copy( + week = tokenListChartConverter.convert(interval, prices), + ) + PriceChangeInterval.MONTH -> chartsToCopy.copy( + month = tokenListChartConverter.convert(interval, prices), + ) + else -> error("unsupported interval=$interval. This shouldn't have happened.") + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt new file mode 100644 index 0000000000..ea8587e123 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenQuotes +import com.tangem.utils.converter.Converter + +class TokenMarketListConverter : Converter> { + + override fun convert(value: TokenMarketListResponse): List { + return value.tokens.map { token -> + TokenMarket( + id = token.id, + name = token.name, + symbol = token.symbol, + marketRating = token.marketRating, + marketCap = token.marketCap, + imageHost = value.imageHost, + tokenQuotes = TokenQuotes( + currentPrice = token.currentPrice, + priceChanges = mapOf( + PriceChangeInterval.H24 to token.priceChangePercentage.h24, + PriceChangeInterval.WEEK to token.priceChangePercentage.week1, + PriceChangeInterval.MONTH to token.priceChangePercentage.day30, + ), + ), + tokenCharts = TokenMarket.Charts(null, null, null), + ) + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt new file mode 100644 index 0000000000..a2b61460d0 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenQuotes + +class TokenQuotesConverter { + + fun convert(tokenId: String, value: QuotesResponse): TokenQuotes { + val quote = requireNotNull(value.quotes[tokenId]) { + "$tokenId is not found in the response. This shouldn't have happened." + } + return TokenQuotes( + currentPrice = requireNotNull(quote.price) { + "Price is not found in the QuotesResponse. This shouldn't have happened." + }, + priceChanges = mapOf( + PriceChangeInterval.H24 to requireNotNull(quote.priceChange1w) { + "priceChange1w is not found in the QuotesResponse. This shouldn't have happened." + }, + PriceChangeInterval.WEEK to requireNotNull(quote.priceChange1w) { + "priceChange1w is not found in the QuotesResponse. This shouldn't have happened." + }, + PriceChangeInterval.MONTH to requireNotNull(quote.priceChange30d) { + "priceChange30d is not found in the QuotesResponse. This shouldn't have happened." + }, + ), + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt new file mode 100644 index 0000000000..be8a925029 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -0,0 +1,31 @@ +package com.tangem.data.markets.di + +import com.tangem.data.markets.DefaultMarketsTokenRepository +import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.markets.repositories.MarketsTokenRepository +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 MarketsDataModule { + + @Provides + @Singleton + fun provideMarketsRepository( + marketsApi: TangemTechMarketsApi, + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): MarketsTokenRepository { + return DefaultMarketsTokenRepository( + marketsApi = marketsApi, + tangemTechApi = tangemTechApi, + dispatcherProvider = dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index d76681efa4..85c903ceec 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -13,7 +13,7 @@ internal class QuotesConverter : Converter { return Quote( rawCurrencyId = rawCurrencyId, fiatRate = responseQuote.price ?: BigDecimal.ZERO, - priceChange = (responseQuote.priceChange ?: BigDecimal.ZERO).movePointLeft(2), + priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), ) } } \ No newline at end of file diff --git a/domain/markets/.gitignore b/domain/markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts new file mode 100644 index 0000000000..5529922be2 --- /dev/null +++ b/domain/markets/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.markets" +} + + +dependencies { + api(projects.domain.markets.models) + api(projects.domain.core) + api(projects.core.pagination) + + implementation(deps.kotlin.serialization) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/markets/models/.gitignore b/domain/markets/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/markets/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/markets/models/build.gradle.kts b/domain/markets/models/build.gradle.kts new file mode 100644 index 0000000000..7558c5ca61 --- /dev/null +++ b/domain/markets/models/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + + implementation(deps.kotlin.serialization) + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt new file mode 100644 index 0000000000..ff9b0ba89a --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.markets + +enum class PriceChangeInterval { + H24, WEEK, MONTH, MONTH3, MONTH6, YEAR, ALL_TIME +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt new file mode 100644 index 0000000000..d239ae395a --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenChart( + val interval: PriceChangeInterval, + val priceY: List, + val timeStamp: List, +) { + init { + require(priceY.size == timeStamp.size) + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt new file mode 100644 index 0000000000..c9587db1f9 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarket( + val id: String, + val name: String, + val symbol: String, + val marketRating: Int?, + val marketCap: BigDecimal?, + val tokenQuotes: TokenQuotes, + val tokenCharts: Charts, + private val imageHost: String, +) { + + data class Charts( + val h24: TokenChart?, + val week: TokenChart?, + val month: TokenChart?, + ) + + // 25x25 + val imageUrlThumb = + "$imageHost/:thumb/:$id.png" + + // 50x50 + val imageUrlSmall = + "$imageHost/:small/:$id.png" + + // 250x250 + val imageUrlLarge = + "$imageHost/:large/:$id.png" +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt new file mode 100644 index 0000000000..ad952091b6 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.markets + +data class TokenMarketListConfig( + val fiatPriceCurrency: String, + val searchText: String?, + val showUnder100kMarketCapTokens: Boolean, + val priceChangeInterval: Interval, + val order: Order, +) { + + enum class Order { + ByRating, Trending, Buyers, TopGainers, TopLosers + } + + enum class Interval { + H24, WEEK, MONTH, + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt new file mode 100644 index 0000000000..bcac52fddb --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.markets + +sealed class TokenMarketUpdateRequest { + + data class UpdateQuotes( + val currencyId: String, + ) : TokenMarketUpdateRequest() + + data class UpdateChart( + val interval: PriceChangeInterval, + val currency: String, + ) : TokenMarketUpdateRequest() +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt new file mode 100644 index 0000000000..e26b6abb9f --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenQuotes( + val currentPrice: BigDecimal, + val priceChanges: Map, +) \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt new file mode 100644 index 0000000000..97aeb80c43 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.markets.repositories + +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.TokenMarketUpdateRequest +import com.tangem.pagination.BatchFlow +import com.tangem.pagination.BatchingContext + +interface MarketsTokenRepository { + + suspend fun getTokenListFlow( + batchingContext: BatchingContext, + ): BatchFlow, TokenMarketUpdateRequest> +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 9f5dfbe387..3db9280b76 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -8,12 +8,9 @@ import java.math.BigDecimal * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. - * @property values The values representing the cryptocurrency's price changes over a 24-hour period, - * suitable for chart plotting. */ data class Quote( val rawCurrencyId: String, val fiatRate: BigDecimal, val priceChange: BigDecimal, - val values: List? = null, ) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7218026460..b2ef2745d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs = -Xmx4096m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +org.gradle.jvmargs = -Xmx6144m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects diff --git a/settings.gradle.kts b/settings.gradle.kts index 6d1f411939..0d6be44673 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -224,6 +224,8 @@ include(":domain:qr-scanning:models") include(":domain:staking") include(":domain:staking:models") include(":domain:wallet-connect") +include(":domain:markets") +include(":domain:markets:models") // endregion Domain modules // region Data modules @@ -245,4 +247,5 @@ include(":data:feedback") include(":data:qr-scanning") include(":data:staking") include(":data:wallet-connect") +include(":data:markets") // endregion Data modules \ No newline at end of file From 71038ca2d866ea9c2ee13812b82e816a376c2932 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 14:49:08 +0300 Subject: [PATCH 115/223] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../GiveTxPermisssionBottomSheet.kt | 2 +- .../core/decompose/model/ModelEntryPoint.kt | 10 +- .../configs/feature_toggles_config.json | 4 + core/res/src/main/res/values-ru/strings.xml | 3 +- core/res/src/main/res/values/strings.xml | 53 ++- .../com/tangem/core/ui/components/Shimmers.kt | 11 +- .../segmentedbutton/SegmentedButton.kt | 11 +- .../core/ui/components/fields/SearchBar.kt | 4 +- .../decompose/ComposableContentComponent.kt | 1 - .../ui/decompose/ComposableDialogComponent.kt | 1 - .../com/tangem/core/ui/res/TangemTheme.kt | 18 +- .../preview/PreviewDetailsComponent.kt | 1 - .../preview/PreviewUserWalletListComponent.kt | 1 - .../features/markets/MarketsFeatureToggles.kt | 5 + .../features/markets/MarketsListComponent.kt | 12 - .../{ => component}/BottomSheetState.kt | 2 +- .../markets/component/MarketsListComponent.kt | 23 ++ features/markets/impl/build.gradle.kts | 18 + .../markets/DefaultMarketsFeatureToggles.kt | 11 + .../impl/DefaultMarketsListComponent.kt | 44 +++ .../features/markets/di/ComponentModule.kt | 18 + .../features/markets/di/FeatureModule.kt | 20 ++ .../tangem/features/markets/di/ModelModule.kt | 20 ++ .../markets/model/MarketsListModel.kt | 61 ++++ .../tangem/features/markets/ui/MarketsList.kt | 217 ++++++++++++ .../ui/{ => components}/MarketsListItem.kt | 19 +- .../components/MarketsListItemPlaceholder.kt | 110 ++++++ .../features/markets/ui/entity/ListItem.kt | 3 + .../MarketsListItemUM.kt} | 4 +- .../markets/ui/entity/MarketsListUM.kt | 42 +++ .../MarketChartListItemPreviewDataProvider.kt | 28 +- .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- features/wallet/impl/build.gradle.kts | 1 + .../wallet/presentation/WalletFragment.kt | 37 ++ .../router/DefaultWalletRouter.kt | 4 +- .../presentation/router/InnerWalletRouter.kt | 4 +- .../presentation/wallet/ui/WalletScreen.kt | 333 +++++++++++++++++- tangem-android-tools | 2 +- 39 files changed, 1073 insertions(+), 89 deletions(-) create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt rename features/markets/api/src/main/kotlin/com/tangem/features/markets/{ => component}/BottomSheetState.kt (57%) create mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/{ => components}/MarketsListItem.kt (96%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/{models/MarketsListItemModel.kt => entity/MarketsListItemUM.kt} (91%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6f66b3c04..c58fa8b592 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,6 +142,8 @@ dependencies { implementation(projects.features.pushNotifications.impl) implementation(projects.features.walletSettings.api) implementation(projects.features.walletSettings.impl) + implementation(projects.features.markets.api) + implementation(projects.features.markets.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt index e65b7b86f1..0922cb2cc6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt @@ -70,7 +70,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh ) Text( text = stringResource( - id = R.string.give_permission_subtitle, + id = R.string.give_permission_swap_subtitle, data.currency, ), color = TangemTheme.colors.text.secondary, diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt index 286678b4c3..56b718e970 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt @@ -40,15 +40,7 @@ inline fun AppComponentContext.getOrCreateM val hiltComponent = hiltComponentBuilder .router(router) .uiMessageSender(messageSender) - .let { builder -> - if (params != null) { - val container = MutableParamsContainer(params) - - builder.paramsContainer(container) - } else { - builder - } - } + .paramsContainer(MutableParamsContainer(params ?: Unit)) .build() EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java) 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 1a01fb3200..7170767402 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -42,5 +42,9 @@ { "name": "PUSH_NOTIFICATIONS_ENABLED", "version": "5.13.0" + }, + { + "name": "MARKETS_ENABLED", + "version": "undefined" } ] diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 015132e02d..6cb64bafa2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -268,7 +268,8 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Укажите лимит доступа к выбранному токену Количество %s - Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. + Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно Купить diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b38d48131f..4a8d97d929 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -271,7 +271,9 @@ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Specify the approve limit for the selected token Amount %s - To continue, grant 1inch smart contracts permission to use your %s + The Approve function is needed to grant permission to another address to use a specific amount of your tokens. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the StakeKit smart contract to use them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can stake your token after giving approval. + To continue you need to allow StakeKit smart contract to use your %s + To continue, grant %1s smart contracts permission to use your %2s Give Permission Unlimited Order card @@ -327,15 +329,62 @@ To start buying, exchanging or receiving this asset, add this token to at least 1 network This asset is not available Add to portfolio + Available networks My portfolio Market + Select network Select wallet + 1m + 1y + 24h + 3m + 6m + 7d + All + Experienced buyers + Rating Sort By + Top Gainers + Top losers + Trending + + Based on %d rating + Based on %d ratings + + Blockchain site + Buy pressure + The difference between buyers volume and sellers volume + Circulating supply + The total number of coins that are available for trading and are circulating in the market + Experienced buyers + Net buyers with the additional requirement of having at least 100 outgoing transactions + Fully diluted valuation + The total theoretical value of a cryptocurrency if all coins that could exist are in circulation, including those not currently circulating + Genesis date + High + Holders + The change in the number of token holders within a specific timeframe Insights Links + Liquidity + The change in how much liquidity is available for the token during the specified timeframe + Liquidity index + Low + Market capitalization + The total market value of a cryptocurrency, calculated by multiplying the current price of the coin by the total number of coins in circulation + Market rating + Position in crypto rating between all coins based on market capitalization + Max supply Metrics + Official links Price performance + Repository Security score + Social + Total supply + The maximum number of coins or tokens that can ever exist for a particular cryptocurrency + Trading volume (24h) + The total amount of a cryptocurrency that has been traded within the last 24 hours, indicating the level of activity and liquidity in the market You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -675,7 +724,7 @@ Tap the twin card with number %s and do not remove until the end of the operation Use %s or scan a card to have an access to your wallet Stay up to date with the latest features and news - Be able to get important market notifications + Be the first to know about new promotions Would you like to use\nPush-notifications? Add new wallet Are you sure you want to delete this wallet? diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index ddb5122ee1..2e5b4ee1c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -17,10 +17,7 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp -import com.tangem.core.ui.res.LocalIsInDarkTheme -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.* import com.valentinilk.shimmer.* /** @@ -31,7 +28,7 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dim Box( modifier = modifier .clip(RoundedCornerShape(size = radius)) - .shimmer(TangemShimmer), + .shimmer(LocalTangemShimmer.current), ) } @@ -44,11 +41,11 @@ fun CircleShimmer(modifier: Modifier = Modifier) { Box( modifier = modifier .clip(CircleShape) - .shimmer(TangemShimmer), + .shimmer(LocalTangemShimmer.current), ) } -private val TangemShimmer: Shimmer +internal val TangemShimmer: Shimmer @Composable get() = rememberShimmer( shimmerBounds = ShimmerBounds.View, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index 1101935641..c4be2dacd0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -4,9 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -18,6 +16,7 @@ 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 androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList @@ -57,12 +56,14 @@ inline fun SegmentedButtons( val index = if (initialSelectedItem == null) 0 else config.indexOf(initialSelectedItem) mutableIntStateOf(index) } + val shape = RoundedCornerShape(TangemTheme.dimens.radius26) Row( modifier = modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius26)) + .clip(shape) .background(dividerColor) - .padding(TangemTheme.dimens.spacing1), + .border(BorderStroke(1.dp, dividerColor), shape = shape) + .height(IntrinsicSize.Max), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing1), ) { repeat(config.size) { index -> diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 0c669a23ad..cf2abf3f5f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -2,7 +2,7 @@ package com.tangem.core.ui.components.fields import android.content.res.Configuration import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -37,7 +37,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie TextField( modifier = modifier .fillMaxWidth() - .height(TangemTheme.dimens.size48) + .heightIn(min = TangemTheme.dimens.size48) .onFocusChanged { if (it.isFocused) { state.onActiveChange(true) diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt index 4f8144398d..c44405f037 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt @@ -8,6 +8,5 @@ import androidx.compose.ui.Modifier fun interface ComposableContentComponent { @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check fun Content(modifier: Modifier) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt index a827872ee3..24eed9ce86 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt @@ -9,6 +9,5 @@ interface ComposableDialogComponent { val doOnDismiss: () -> Unit @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check fun Dialog() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index ea874c3bfb..1bd2251a68 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -7,9 +7,11 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.core.ui.components.TangemShimmer import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.MockHapticManager import com.tangem.core.ui.windowsize.WindowSize +import com.valentinilk.shimmer.Shimmer @Composable fun TangemTheme( @@ -49,10 +51,14 @@ fun TangemTheme( LocalSnackbarHostState provides snackbarHostState, LocalWindowSize provides windowSize, ) { - ProvideTextStyle( - value = TangemTheme.typography.body1, - content = content, - ) + CompositionLocalProvider( + LocalTangemShimmer provides TangemShimmer, + ) { + ProvideTextStyle( + value = TangemTheme.typography.body1, + content = content, + ) + } } } } @@ -227,4 +233,8 @@ val LocalSnackbarHostState = staticCompositionLocalOf { val LocalWindowSize = staticCompositionLocalOf { error("No WindowSize provided") +} + +val LocalTangemShimmer = staticCompositionLocalOf { + error("No TangemShimmer provided") } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 5d2d2b67d0..209cad1288 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -33,7 +33,6 @@ internal class PreviewDetailsComponent : DetailsComponent { ) @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check override fun Content(modifier: Modifier) { DetailsScreen( modifier = modifier, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 09950611e8..a8906b5061 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -45,7 +45,6 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { ) @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check override fun Content(modifier: Modifier) { UserWalletListBlock(state = previewState, modifier = modifier) } diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt new file mode 100644 index 0000000000..34758a7b41 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.markets + +interface MarketsFeatureToggles { + val isFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt deleted file mode 100644 index e987e0d66e..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets - -import androidx.compose.runtime.Stable - -@Stable -interface MarketsListComponent { - - // TODO - - // @Composable - // fun BottomSheetContent(state: State, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier) -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt similarity index 57% rename from features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt rename to features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt index 0084b73528..5c8cc47920 100644 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/BottomSheetState.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets +package com.tangem.features.markets.component enum class BottomSheetState { EXPANDED, diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt new file mode 100644 index 0000000000..c0d14dbea5 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/component/MarketsListComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.markets.component + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.tangem.core.decompose.context.AppComponentContext + +@Stable +interface MarketsListComponent { + + @Composable + fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) + + interface Factory { + fun create(context: AppComponentContext): MarketsListComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 80288a27be..86ae97c792 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -1,6 +1,9 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) id("configuration") } @@ -9,16 +12,31 @@ android { } dependencies { + /* Project - API */ + api(projects.features.markets.api) + + /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) + implementation(deps.lifecycle.compose) + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* Other */ implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + /* Core */ + implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + implementation(projects.common.ui) implementation(projects.common.uiCharts) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt new file mode 100644 index 0000000000..75062eddef --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.markets + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultMarketsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : MarketsFeatureToggles { + + override val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("MARKETS_ENABLED") +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt new file mode 100644 index 0000000000..316720a3b6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.markets.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.component.MarketsListComponent +import com.tangem.features.markets.model.MarketsListModel +import com.tangem.features.markets.ui.MarketsList +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultMarketsListComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, +) : MarketsListComponent, AppComponentContext by context { + + private val model: MarketsListModel = getOrCreateModel() + + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + val state by model.state.collectAsStateWithLifecycle() + + MarketsList( + onHeaderSizeChange = onHeaderSizeChange, + state = state, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : MarketsListComponent.Factory { + override fun create(context: AppComponentContext): DefaultMarketsListComponent + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt new file mode 100644 index 0000000000..f88b0b5ad0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.markets.di + +import com.tangem.features.markets.component.MarketsListComponent +import com.tangem.features.markets.component.impl.DefaultMarketsListComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindMarketsListComponent(factory: DefaultMarketsListComponent.Factory): MarketsListComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt new file mode 100644 index 0000000000..a2085dfe77 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.markets.DefaultMarketsFeatureToggles +import com.tangem.features.markets.MarketsFeatureToggles +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 FeatureModule { + + @Provides + @Singleton + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): MarketsFeatureToggles = + DefaultMarketsFeatureToggles(featureTogglesManager = featureTogglesManager) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt new file mode 100644 index 0000000000..9ee24735da --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.markets.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.markets.model.MarketsListModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(MarketsListModel::class) + fun provideMarketsListModel(model: MarketsListModel): Model +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt new file mode 100644 index 0000000000..6cbf590bc5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt @@ -0,0 +1,61 @@ +package com.tangem.features.markets.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.ui.entity.ListUM +import com.tangem.features.markets.ui.entity.MarketsListUM +import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class MarketsListModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val state = MutableStateFlow( + MarketsListUM( + list = ListUM.Loading, + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + selectedSortBy = SortByTypeUM.Rating, + selectedInterval = MarketsListUM.TrendInterval.H24, + onIntervalClick = {}, + onSortByButtonClick = {}, + ), + ) + + init { + modelScope.launch { + delay(timeMillis = 5000) + state.update { + it.copy( + list = ListUM.Content( + items = MarketChartListItemPreviewDataProvider().values + .flatMap { item -> List(size = 10) { item } } + .mapIndexed { index, item -> + item.copy(id = index.toString()) + } + .toImmutableList(), + ), + ) + } + } + } + + // TODO +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt new file mode 100644 index 0000000000..7929779ddb --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt @@ -0,0 +1,217 @@ +package com.tangem.features.markets.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.ui.components.MarketsListItem +import com.tangem.features.markets.ui.components.MarketsListItemPlaceholder +import com.tangem.features.markets.ui.entity.ListUM +import com.tangem.features.markets.ui.entity.MarketsListUM +import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun MarketsList(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { + Content( + modifier = modifier, + state = state, + onHeaderSizeChange = onHeaderSizeChange, + ) +} + +@Composable +private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { + val density = LocalDensity.current + + Column( + modifier = modifier + .fillMaxSize() + .imePadding() + .background(color = TangemTheme.colors.background.primary), + ) { + SearchBar( + state = state.searchBar, + modifier = Modifier + .background(color = TangemTheme.colors.background.primary) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing4, + ) + .onGloballyPositioned { + with(density) { onHeaderSizeChange(it.size.height.toDp()) } + }, + ) + Spacer(Modifier.height(TangemTheme.dimens.spacing20)) + Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) { + Title() + SpacerH12() + Options( + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } + SpacerH12() + Items(state = state.list) + } +} + +@Composable +private fun Title(modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = stringResource(id = R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) +} + +@Composable +private fun Options( + sortByTypeUM: SortByTypeUM, + trendInterval: MarketsListUM.TrendInterval, + onSortByClick: () -> Unit, + onIntervalClick: (MarketsListUM.TrendInterval) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .height(IntrinsicSize.Max) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + SecondarySmallButton( + config = SmallButtonConfig( + text = sortByTypeUM.text, + onClick = onSortByClick, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), + ), + ) + SegmentedButtons( + config = persistentListOf( + MarketsListUM.TrendInterval.H24, + MarketsListUM.TrendInterval.D7, + MarketsListUM.TrendInterval.M1, + ), + initialSelectedItem = trendInterval, + onClick = onIntervalClick, + modifier = Modifier + .width(160.dp) + .fillMaxHeight(), + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding( + vertical = TangemTheme.dimens.spacing4, + ), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.text.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +@Composable +private fun Items(state: ListUM, modifier: Modifier = Modifier) { + val lazyListState = rememberLazyListState() + val scrollEnabled = state !is ListUM.Loading + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + LazyColumn( + modifier = modifier, + state = lazyListState, + contentPadding = PaddingValues(bottom = bottomBarHeight), + userScrollEnabled = scrollEnabled, + ) { + when (state) { + ListUM.Loading -> { + items(count = 50, key = { it }) { + MarketsListItemPlaceholder() + } + } + ListUM.SearchNothingFound -> { + // TODO + } + is ListUM.Content -> { + items( + items = state.items, + key = { it.id }, + ) { item -> + MarketsListItem( + model = item, + ) + } + } + } + } +} + +//region: Preview + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + MarketsList( + state = MarketsListUM( + list = ListUM.Content( + items = MarketChartListItemPreviewDataProvider().values + .flatMap { item -> List(size = 10) { item } } + .mapIndexed { index, item -> + item.copy(id = index.toString()) + } + .toImmutableList(), + ), + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + selectedSortBy = SortByTypeUM.Rating, + selectedInterval = MarketsListUM.TrendInterval.H24, + onIntervalClick = {}, + onSortByButtonClick = {}, + ), + onHeaderSizeChange = {}, + ) + } +} + +//endregion: Preview \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt similarity index 96% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt index 7ceb15ca05..ff348ef270 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui +package com.tangem.features.markets.ui.components import android.content.res.Configuration import androidx.compose.animation.Animatable @@ -45,7 +45,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R -import com.tangem.features.markets.ui.models.MarketsListItemModel +import com.tangem.features.markets.ui.entity.MarketsListItemUM import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -63,7 +63,7 @@ const val SWIPE_VELOCITY_THRESHOLD = 20f @OptIn(ExperimentalFoundationApi::class) @Composable fun MarketsListItem( - model: MarketsListItemModel, + model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}, onSwipeToAction: () -> Unit = {}, @@ -184,7 +184,7 @@ fun MarketsListItem( } @Composable -private fun MarketsListItemContent(model: MarketsListItemModel, modifier: Modifier = Modifier) { +private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) { val windowSize = LocalWindowSize.current Row( @@ -303,7 +303,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) { color = TangemTheme.colors.field.primary, shape = TangemTheme.shapes.roundedCornersSmall2, ) - .padding(horizontal = TangemTheme.dimens.size5), // TODO check + .padding(horizontal = TangemTheme.dimens.spacing5), ) { Text( text = ratingPosition ?: "-", @@ -379,8 +379,9 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD RectangleShimmer( modifier = Modifier .fillMaxWidth() - .height(TangemTheme.dimens.size15) // TODO check + .height(TangemTheme.dimens.size12) .align(Alignment.Center), + radius = TangemTheme.dimens.radius3, ) } } @@ -391,7 +392,7 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD @Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, widthDp = 320, name = "small width") @Composable -private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemModel) { +private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) { TangemThemePreview { var state1 by remember { mutableStateOf(state) } var state2 by remember { mutableStateOf(state) } @@ -435,13 +436,13 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl } } state1 = state1.copy( - price = MarketsListItemModel.Price( + price = MarketsListItemUM.Price( text = "0.${prices[0].first}023 $", changeType = prices[0].second, ), ) state2 = state2.copy( - price = MarketsListItemModel.Price( + price = MarketsListItemUM.Price( text = "0.${prices[1].first}023 $", changeType = prices[1].second, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt new file mode 100644 index 0000000000..251844c2d6 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItemPlaceholder.kt @@ -0,0 +1,110 @@ +package com.tangem.features.markets.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.LocalWindowSize +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.windowsize.WindowSizeType + +@Suppress("LongMethod") +@Composable +fun MarketsListItemPlaceholder() { + val density = LocalDensity.current + val windowSize = LocalWindowSize.current + val sp12 = with(density) { 12.sp.toDp() } + + Row( + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing15, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer(Modifier.size(TangemTheme.dimens.size36)) + + SpacerW12() + + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing4), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + RectangleShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size70) + .height(sp12), + radius = TangemTheme.dimens.radius3, + ) + SpacerW8() + RectangleShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size70) + .height(sp12), + radius = TangemTheme.dimens.radius3, + ) + } + + SpacerH(height = TangemTheme.dimens.spacing2) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing2), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + RectangleShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size52) + .height(sp12), + radius = TangemTheme.dimens.radius3, + ) + RectangleShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size52) + .height(sp12), + radius = TangemTheme.dimens.radius3, + ) + } + } + + if (windowSize.widthAtLeast(WindowSizeType.Small)) { + Spacer(Modifier.width(TangemTheme.dimens.spacing10)) + + Box { + RectangleShimmer( + modifier = Modifier + .align(Alignment.Center) + .width(TangemTheme.dimens.size56) + .height(TangemTheme.dimens.size12), + radius = TangemTheme.dimens.radius3, + ) + } + } + } +} + +@Preview(showBackground = true, widthDp = 360, name = "normal") +@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 320, name = "small width") +@Composable +private fun Preview() { + TangemThemePreview { + Column(Modifier.background(TangemTheme.colors.background.tertiary)) { + repeat(20) { + MarketsListItemPlaceholder() + } + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt new file mode 100644 index 0000000000..70f3921dac --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt @@ -0,0 +1,3 @@ +package com.tangem.features.markets.ui.entity + +sealed class ListItem \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt similarity index 91% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt index 4c8d21f229..a9f12c9a19 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.ui.models +package com.tangem.features.markets.ui.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartLook @@ -6,7 +6,7 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType @Immutable -data class MarketsListItemModel( +data class MarketsListItemUM( val id: String, val name: String, val currencySymbol: String, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt new file mode 100644 index 0000000000..b431e9e54c --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt @@ -0,0 +1,42 @@ +package com.tangem.features.markets.ui.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R +import kotlinx.collections.immutable.ImmutableList + +internal data class MarketsListUM( + val list: ListUM, + val searchBar: SearchBarUM, + val selectedSortBy: SortByTypeUM, + val selectedInterval: TrendInterval, + val onIntervalClick: (TrendInterval) -> Unit, + val onSortByButtonClick: () -> Unit, +) { + enum class TrendInterval(val text: TextReference) { + H24(resourceReference(R.string.markets_selector_interval_24h_title)), + D7(resourceReference(R.string.markets_selector_interval_7d_title)), + M1(resourceReference(R.string.markets_selector_interval_1m_title)), + } +} + +enum class SortByTypeUM(val text: TextReference) { + Rating(resourceReference(R.string.markets_sort_by_rating_title)), + Trending(resourceReference(R.string.markets_sort_by_trending_title)), + ExperiencedBuyers(resourceReference(R.string.markets_sort_by_experienced_buyers_title)), + TopGainers(resourceReference(R.string.markets_sort_by_top_gainers_title)), + TopLosers(resourceReference(R.string.markets_sort_by_top_losers_title)), +} + +@Immutable +sealed class ListUM { + + data class Content( + val items: ImmutableList, + ) : ListUM() + + data object Loading : ListUM() + data object SearchNothingFound : ListUM() +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt index 6121a35222..782d4bfbc8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -4,86 +4,86 @@ package com.tangem.features.markets.ui.preview import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.features.markets.ui.models.MarketsListItemModel +import com.tangem.features.markets.ui.entity.MarketsListItemUM -internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( +internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( collection = listOf( - MarketsListItemModel( + MarketsListItemUM( id = "1", name = "Bitcoin", currencySymbol = "BTC", iconUrl = "", ratingPosition = "10", marketCap = "$6.233 B", - price = MarketsListItemModel.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), ), ), - MarketsListItemModel( + MarketsListItemUM( id = "1", name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, ratingPosition = "10", marketCap = "$6.233 B", - price = MarketsListItemModel.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.NEUTRAL, chardData = null, ), - MarketsListItemModel( + MarketsListItemUM( id = "1", name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", currencySymbol = "BTC", iconUrl = null, ratingPosition = "10", marketCap = "$6.23348172384781234 B", - price = MarketsListItemModel.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.DOWN, chardData = MarketChartRawData( y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), ), ), - MarketsListItemModel( + MarketsListItemUM( id = "1", name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, ratingPosition = "10", marketCap = null, - price = MarketsListItemModel.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), ), ), - MarketsListItemModel( + MarketsListItemUM( id = "1", name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, ratingPosition = null, marketCap = "$6.233 B", - price = MarketsListItemModel.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), ), ), - MarketsListItemModel( + MarketsListItemUM( id = "1", name = "Bitcoin", currencySymbol = "BTC", iconUrl = null, ratingPosition = null, marketCap = null, - price = MarketsListItemModel.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price(text = "31 285.72$"), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chardData = MarketChartRawData( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 2af78f3dfd..9dad38b55e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1407,7 +1407,7 @@ internal class StateBuilder( return NotificationConfig( title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( - id = R.string.give_permission_subtitle, + id = R.string.give_permission_swap_subtitle, formatArgs = wrappedList(fromTokenSymbol), ), iconResId = R.drawable.ic_locked_24, diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index bbfc624efd..c96d1c74b4 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { implementation(projects.common.routing) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) + implementation(projects.core.decompose) implementation(projects.libs.crypto) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 7c3629f145..fa0bdbd72d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -1,11 +1,18 @@ package com.tangem.feature.wallet.presentation +import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.arkivanov.decompose.defaultComponentContext +import com.tangem.core.decompose.context.DefaultAppComponentContext +import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.features.markets.MarketsFeatureToggles +import com.tangem.features.markets.component.MarketsListComponent import com.tangem.features.wallet.navigation.WalletRouter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -24,15 +31,45 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var walletRouter: WalletRouter + @Inject + internal lateinit var marketsListComponentFactory: MarketsListComponent.Factory + + @Inject + internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider + + @Inject + internal lateinit var componentBuilder: DecomposeComponent.Builder + + @Inject + internal lateinit var marketsFeatureToggles: MarketsFeatureToggles + + private var marketsListComponent: MarketsListComponent? = null + private val _walletRouter: InnerWalletRouter get() = requireNotNull(walletRouter as? InnerWalletRouter) { "_walletRouter should be instance of InnerWalletRouter" } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (marketsFeatureToggles.isFeatureEnabled) { + val appContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = uiDependencies.eventMessageHandler, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + ) + + marketsListComponent = marketsListComponentFactory.create(appContext) + } + } + @Composable override fun ScreenContent(modifier: Modifier) { _walletRouter.Initialize( onFinish = requireActivity()::finish, + marketsListComponent = marketsListComponent, ) } 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 815f0493b4..c90cea3084 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 @@ -25,6 +25,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel +import com.tangem.features.markets.component.MarketsListComponent import kotlin.properties.Delegates /** Default implementation of wallet feature router */ @@ -40,7 +41,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit) { + override fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) { this.onFinish = onFinish NavHost( @@ -55,6 +56,7 @@ internal class DefaultWalletRouter( WalletScreen( state = viewModel.uiState.collectAsStateWithLifecycle().value, + marketsListComponent = marketsListComponent, ) } 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 d213d227f8..0666e1191a 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.markets.component.MarketsListComponent import com.tangem.features.wallet.navigation.WalletRouter /** @@ -22,9 +23,8 @@ internal interface InnerWalletRouter : WalletRouter { * * @param onFinish finish activity callback */ - @Suppress("TopLevelComposableFunctions") @Composable - fun Initialize(onFinish: () -> Unit) + fun Initialize(onFinish: () -> Unit, marketsListComponent: MarketsListComponent?) /** Pop back stack */ fun popBackStack() 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 0de613caa1..41698402f7 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 @@ -2,6 +2,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -10,27 +15,36 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.FabPosition -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -55,10 +69,13 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.component.MarketsListComponent import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.launch @Composable -internal fun WalletScreen(state: WalletScreenState) { +internal fun WalletScreen(state: WalletScreenState, marketsListComponent: MarketsListComponent?) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -81,6 +98,8 @@ internal fun WalletScreen(state: WalletScreenState) { snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, + marketsListComponent = marketsListComponent, + alertConfig = alertConfig, ) WalletEventEffect( @@ -100,6 +119,8 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, + marketsListComponent: MarketsListComponent?, + alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, ) { var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } @@ -195,12 +216,39 @@ private fun WalletContent( ) } - BaseScaffold( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - ) { - scaffoldContent() + if (marketsListComponent != null) { + val bottomSheetState = remember { + mutableStateOf(BottomSheetState.COLLAPSED) + } + var headerSize by remember { + mutableStateOf(0.dp) + } + + BaseScaffoldWithMarkets( + state = state, + selectedWallet = selectedWallet, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = { headerSize }, + alertConfig = alertConfig, + onBottomSheetStateChange = { bottomSheetState.value = it }, + bottomSheetContent = { + marketsListComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = Modifier, + ) + }, + ) { + scaffoldContent() + } + } else { + BaseScaffold( + state = state, + selectedWallet = selectedWallet, + snackbarHostState = snackbarHostState, + ) { + scaffoldContent() + } } } @@ -265,6 +313,268 @@ private fun BaseScaffold( ) } +@Suppress("LongParameterList", "LongMethod") +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Composable +private fun BaseScaffoldWithMarkets( + state: WalletScreenState, + selectedWallet: WalletState, + snackbarHostState: SnackbarHostState, + bottomSheetHeaderHeightProvider: () -> Dp, + bottomSheetContent: @Composable () -> Unit, + alertConfig: WalletAlertState?, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + content: @Composable () -> Unit, +) { + // show the bottom sheet if there is at least one multicurrency wallet + val showManageTokensBottomSheet = remember(state.wallets) { + state.wallets.any { it is WalletState.MultiCurrency } + } + val bottomSheetState = rememberSheetStateEnhanced( + initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden, + confirmValueChange = { sheetValue -> + when { + sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false + sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false + else -> true + } + }, + skipHiddenState = showManageTokensBottomSheet, + ) + + val keyboardShown = keyboardAsState() + + BottomSheetStateEffects( + bottomSheetState = bottomSheetState, + showManageTokensBottomSheet = showManageTokensBottomSheet, + alertConfig = alertConfig, + keyboardShown = keyboardShown, + onBottomSheetStateChange = onBottomSheetStateChange, + ) + + val scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState, + ) + + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + + val coroutineScope = rememberCoroutineScope() + + BottomSheetScaffold( + snackbarHost = { + WalletSnackbarHost( + snackbarHostState = it, + event = state.event, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + ) + }, + containerColor = TangemTheme.colors.background.secondary, + sheetContainerColor = TangemTheme.colors.background.primary, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetDragHandle = { + Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary)) + }, + sheetTonalElevation = 8.dp, + sheetShadowElevation = 8.dp, + sheetContent = { + BoxWithConstraints { + Box( + modifier = Modifier + .sizeIn(maxHeight = maxHeight - statusBarHeight) + .align(Alignment.BottomCenter), + ) { + bottomSheetContent() + } + } + + // hide bottom sheet when back pressed + BackHandler( + keyboardShown.value is Keyboard.Closed && + bottomSheetState.currentValue == SheetValue.Expanded, + ) { + coroutineScope.launch { bottomSheetState.partialExpand() } + } + }, + content = { _ -> + val pullRefreshState = rememberPullRefreshState( + refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + onRefresh = { + selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) + }, + ) + + Column { + WalletTopBar(config = state.topBarConfig) + Box( + modifier = Modifier.pullRefresh(pullRefreshState), + ) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } + } + + BottomSheetScrim( + color = BottomSheetDefaults.ScrimColor, + visible = bottomSheetState.targetValue == SheetValue.Expanded, + onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, + ) + }, + ) +} + +@Composable +private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = TweenSpec(), + label = "scrim", + ) + val dismissSheet = if (visible) { + Modifier + .pointerInput(onDismissRequest) { + detectTapGestures { + onDismissRequest() + } + } + .clearAndSetSemantics {} + } else { + Modifier + } + Canvas( + Modifier + .fillMaxSize() + .then(dismissSheet), + ) { + drawRect(color = color, alpha = alpha) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod") +@Composable +private fun BottomSheetStateEffects( + bottomSheetState: SheetState, + showManageTokensBottomSheet: Boolean, + alertConfig: WalletAlertState?, + keyboardShown: State, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { + // Bottom sheet during initialization internally expand partially after its content was remeasured, + // therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected + // so we have to manually restrict expansion in this case + LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) { + if (!showManageTokensBottomSheet && + (bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden) + ) { + bottomSheetState.hide() + } + } + // react to changes in wallet list + LaunchedEffect(showManageTokensBottomSheet) { + when { + showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> { + bottomSheetState.partialExpand() + } + !showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> { + bottomSheetState.hide() + } + } + } + + val systemUiController = rememberSystemUiController() + val navigationBarColor = TangemTheme.colors.background.primary + + LaunchedEffect(key1 = bottomSheetState.targetValue, navigationBarColor) { + when (bottomSheetState.targetValue) { + SheetValue.Hidden, + SheetValue.Expanded, + -> systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = navigationBarColor.luminance() > 0.5f, + navigationBarContrastEnforced = true, + ) + SheetValue.PartiallyExpanded, + -> systemUiController.setNavigationBarColor(navigationBarColor) + } + } + + DisposableEffect(showManageTokensBottomSheet) { + onDispose { + if (showManageTokensBottomSheet) { + systemUiController.setNavigationBarColor( + color = Color.Transparent, + darkIcons = navigationBarColor.luminance() > 0.5f, + navigationBarContrastEnforced = false, + ) + } + } + } + + // expand bottom sheet when keyboard appears + LaunchedEffect(keyboardShown.value is Keyboard.Opened) { + if (keyboardShown.value is Keyboard.Opened && alertConfig == null) { + bottomSheetState.expand() + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + // hide keyboard when bottom sheet is about to be hidden + LaunchedEffect(Unit) { + snapshotFlow { + bottomSheetState.currentValue == SheetValue.Expanded && + bottomSheetState.targetValue == SheetValue.PartiallyExpanded + }.collect { sheetHasBeenHidden -> + if (sheetHasBeenHidden) { + keyboardController?.hide() + } + } + } + + val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded + LaunchedEffect(isSheetHidden) { + onBottomSheetStateChange( + if (isSheetHidden) { + BottomSheetState.COLLAPSED + } else { + BottomSheetState.EXPANDED + }, + ) + } +} + +/** + * Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718 + * Current material3 version: 1.2.0 + */ +@Composable +@ExperimentalMaterial3Api +private fun rememberSheetStateEnhanced( + skipPartiallyExpanded: Boolean = false, + confirmValueChange: (SheetValue) -> Boolean = { true }, + initialValue: SheetValue = SheetValue.Hidden, + skipHiddenState: Boolean = false, +): SheetState { + val density = LocalDensity.current + return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) { + SheetState( + skipPartiallyExpanded = skipPartiallyExpanded, + density = density, + initialValue = initialValue, + confirmValueChange = confirmValueChange, + skipHiddenState = skipHiddenState, + ) + } +} + @Composable private fun WalletSnackbarHost( snackbarHostState: SnackbarHostState, @@ -328,6 +638,7 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, + marketsListComponent = null, ) } } diff --git a/tangem-android-tools b/tangem-android-tools index d8e6da9ab3..0ed07b85e6 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit d8e6da9ab3a0442657822352cb6274237d88e77d +Subproject commit 0ed07b85e64805707b2ef6a92f42bc2c4e8b7753 From a41d65dd2c71b2d0b312d7d12ed8be506ad5873d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 15:31:44 +0200 Subject: [PATCH 116/223] Updated on 2026-08-14 --- .../com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt index ed6cab9758..7c9071598f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/ImageBackgroundContrastChecker.kt @@ -39,6 +39,6 @@ class ImageBackgroundContrastChecker( private companion object { // https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef - private const val LOW_CONTRAST_RATIO = 1.5f + private const val LOW_CONTRAST_RATIO = 1.1f } } \ No newline at end of file From 437f9a0f0de003241f8335849ce27f95ed8ab727 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 21:36:47 +0500 Subject: [PATCH 117/223] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../feature/swap/viewmodels/SwapViewModel.kt | 106 ++++++++++-------- 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4a8d97d929..952d0831fa 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -668,6 +668,7 @@ Fees All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve + Fee estimation error. Please send feedback to support. You swap Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 6237a44e56..9e9a4eec98 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -14,6 +14,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -35,7 +36,10 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.SwapStateHolder +import com.tangem.feature.swap.models.SwapWarning +import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -493,9 +497,9 @@ internal class SwapViewModel @Inject constructor( } val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) val fee = dataState.selectedFee - // TODO: unexpected crash for some users, this workaround to prevent app crash and follow to support + if (fee == null) { - makeDefaultAlert(TextReference.Str("Fee estimation error. Please send feedback to support.")) + makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return } viewModelScope.launch(dispatchers.main) { @@ -587,57 +591,63 @@ internal class SwapViewModel @Inject constructor( private fun givePermissionsToSwap() { viewModelScope.launch(dispatchers.main) { - val fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { - "dataState.fromCurrency might not be null" - } - val feeForPermission = when (val fee = dataState.approveDataModel?.fee) { - TxFeeState.Empty -> error("Fee should not be Empty") - is TxFeeState.MultipleFeeState -> fee.priorityFee - is TxFeeState.SingleFeeState -> fee.fee - null -> error("Fee should not be null") - } - val approveType = requireNotNull(dataState.approveType) { - "uiState.permissionState should not be null" - }.toDomainApproveType() - runCatching(dispatchers.io) { - swapInteractor.givePermissionToSwap( - networkId = fromToken.network.backendId, - permissionOptions = PermissionOptions( - approveData = requireNotNull(dataState.approveDataModel) { - "dataState.approveDataModel might not be null" - }, - forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) - ?.contractAddress ?: "", - fromToken = fromToken, - approveType = approveType, - txFee = feeForPermission, - spenderAddress = requireNotNull(dataState.approveDataModel?.spenderAddress) { - "dataState.approveDataModel.spenderAddress shouldn't be null" - }, - ), - ) - }.onSuccess { - when (it) { - is SwapTransactionState.TxSent -> { - sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) - updateWalletBalance() - uiState = stateBuilder.loadingPermissionState(uiState) - uiState = stateBuilder.dismissBottomSheet(uiState) - startLoadingQuotesFromLastState(isSilent = true) + runCatching { + val fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { + "dataState.fromCurrency might not be null" + } + val approveDataModel = requireNotNull(dataState.approveDataModel) { + "dataState.approveDataModel.spenderAddress shouldn't be null" + } + val approveType = requireNotNull(dataState.approveType?.toDomainApproveType()) { + "uiState.permissionState should not be null" + } + val feeForPermission = when (val fee = approveDataModel.fee) { + TxFeeState.Empty -> { + makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + Timber.e("Fee should not be Empty") + return@runCatching } - is SwapTransactionState.UserCancelled -> Unit - else -> { - uiState = stateBuilder.createErrorTransaction(uiState, it) { - uiState = stateBuilder.clearAlert(uiState) + is TxFeeState.MultipleFeeState -> fee.priorityFee + is TxFeeState.SingleFeeState -> fee.fee + } + runCatching(dispatchers.io) { + swapInteractor.givePermissionToSwap( + networkId = fromToken.network.backendId, + permissionOptions = PermissionOptions( + approveData = approveDataModel, + forTokenContractAddress = (fromToken as? CryptoCurrency.Token)?.contractAddress.orEmpty(), + fromToken = fromToken, + approveType = approveType, + txFee = feeForPermission, + spenderAddress = approveDataModel.spenderAddress, + ), + ) + }.onSuccess { + when (it) { + is SwapTransactionState.TxSent -> { + sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) + updateWalletBalance() + uiState = stateBuilder.loadingPermissionState(uiState) + uiState = stateBuilder.dismissBottomSheet(uiState) + startLoadingQuotesFromLastState(isSilent = true) + } + is SwapTransactionState.UserCancelled -> Unit + else -> { + uiState = stateBuilder.createErrorTransaction(uiState, it) { + uiState = stateBuilder.clearAlert(uiState) + } } } - } - }.onFailure { - makeDefaultAlert() - } + }.onFailure { makeDefaultAlert() } + }.onFailure { showGenericError(it.message.orEmpty()) } } } + private fun showGenericError(message: String) { + makeDefaultAlert(resourceReference(R.string.common_unknown_error)) + Timber.e(message) + } + private fun onSearchEntered(searchQuery: String) { viewModelScope.launch(dispatchers.io) { val tokenDataState = dataState.tokensDataState ?: return@launch From 76841e8f625c8f5b2a69710dc90cf0cb94a30f49 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 9 Jul 2024 15:05:17 +0100 Subject: [PATCH 118/223] Updated on 2026-08-14 --- .../data/DefaultLegacyWalletConnectRepository.kt | 2 +- .../feature/qrscanning/viewmodel/QrScanningClickIntents.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 922581137c..04a4940928 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -494,6 +494,6 @@ internal class DefaultLegacyWalletConnectRepository( private companion object { - val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro") + val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt index 075894e1c6..5c2ab95304 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt @@ -45,13 +45,13 @@ internal class QrScanningClickIntentsImplementor @Inject constructor( override fun onQrScanned(qrCode: String) { if (qrCode.isNotBlank()) { + viewModelScope.launch(dispatcher.mainImmediate) { + emitQrScannedEventUseCase.invoke(source, qrCode) + } if (!isScanned) { router.popBackStack() isScanned = true } - viewModelScope.launch(dispatcher.main) { - emitQrScannedEventUseCase.invoke(source, qrCode) - } } } From 84a82bdfb29245a62d5248acd9de39a4eca113a6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 17:51:58 +0200 Subject: [PATCH 119/223] Updated on 2026-08-14 --- fastlane/Fastfile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 2d59c37cd6..d1cfaf3f7e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -23,12 +23,8 @@ platform :android do desc "Runs all the tests" lane :test do - gradle( - task: "clean assemble", - build_type: "Debug" - ) gradle(task: "detekt") - gradle(task: "test") + gradle(task: "testDebugUnitTest") end desc "Build a signed release APK" From 9fee474c903e63cf853ba1603e6e40232d6ccc45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Jul 2024 16:19:45 +0500 Subject: [PATCH 120/223] Updated on 2026-08-14 --- .../GiveTxPermisssionBottomSheet.kt | 42 +++++++------------ .../state/GiveTxPermissionState.kt | 2 + core/res/src/main/res/values/strings.xml | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 18 ++++++-- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt index 0922cb2cc6..e0d8b57dd7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt @@ -69,10 +69,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh ), ) Text( - text = stringResource( - id = R.string.give_permission_swap_subtitle, - data.currency, - ), + text = content.data.subtitle.resolveReference(), color = TangemTheme.colors.text.secondary, style = TangemTheme.typography.body2, textAlign = TextAlign.Center, @@ -109,8 +106,8 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh // region dialog if (isPermissionAlertShow) { BasicDialog( - message = stringResource(id = R.string.swapping_approve_information_text), - title = stringResource(id = R.string.swapping_approve_information_title), + message = content.data.dialogText.resolveReference(), + title = stringResource(id = R.string.common_approve), confirmButton = DialogButton { isPermissionAlertShow = false }, onDismissDialog = {}, ) @@ -210,26 +207,17 @@ private fun AmountItem( style = TangemTheme.typography.subtitle1, maxLines = 1, ) - when (approveType) { - ApproveType.LIMITED -> { - Text( - text = stringResource(id = R.string.give_permission_current_transaction), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - } - ApproveType.UNLIMITED -> { - Icon( - painter = rememberVectorPainter( - image = ImageVector.vectorResource(id = R.drawable.ic_infinity_24), - ), - tint = TangemTheme.colors.text.tertiary, - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size20), - ) - } - } + Text( + text = stringResource( + when (approveType) { + ApproveType.LIMITED -> R.string.give_permission_current_transaction + ApproveType.UNLIMITED -> R.string.give_permission_unlimited + }, + ), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) } DropdownSelector( isExpanded = isExpandSelector, @@ -335,6 +323,8 @@ private val previewData = GiveTxPermissionBottomSheetConfig( approveButton = ApprovePermissionButton(true) {}, cancelButton = CancelPermissionButton(true), onChangeApproveType = { ApproveType.UNLIMITED }, + subtitle = resourceReference(R.string.give_permission_staking_subtitle), + dialogText = resourceReference(R.string.give_permission_staking_footer), ), onCancel = {}, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt index 9b07504849..5f8cc96667 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt @@ -11,6 +11,8 @@ sealed class GiveTxPermissionState { data object Empty : GiveTxPermissionState() data class ReadyForRequest( + val subtitle: TextReference, + val dialogText: TextReference, val currency: String, val amount: String, val walletAddress: String, diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 952d0831fa..6b23058989 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -332,6 +332,7 @@ Available networks My portfolio Market + To generate addresses for selected networks, you need to attach a Tangem card Select network Select wallet 1m diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 9dad38b55e..23a67bb6a5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -218,6 +218,7 @@ internal class StateBuilder( quoteModel = quoteModel, fromToken = fromToken, selectedFeeType = selectedFeeType, + providerName = swapProvider.name, ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus @@ -258,6 +259,7 @@ internal class StateBuilder( permissionState = convertPermissionState( lastPermissionState = uiStateHolder.permissionState, permissionDataState = quoteModel.permissionState, + providerName = swapProvider.name, onGivePermissionClick = actions.onGivePermissionClick, onChangeApproveType = actions.onChangeApproveType, ), @@ -317,11 +319,12 @@ internal class StateBuilder( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, selectedFeeType: FeeType, + providerName: String, ): List { val warnings = mutableListOf() maybeAddDomainWarnings(quoteModel, warnings) maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) - maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) + maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken, providerName) maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) maybeAddInsufficientFundsWarning(quoteModel, warnings) @@ -469,6 +472,7 @@ internal class StateBuilder( quoteModel: SwapState.QuotesLoadedState, warnings: MutableList, fromToken: CryptoCurrency, + providerName: String, ) { if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && @@ -476,7 +480,7 @@ internal class StateBuilder( ) { warnings.add( SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(fromToken.symbol), + createPermissionNotificationConfig(providerName, fromToken.symbol), ), ) } @@ -1147,6 +1151,7 @@ internal class StateBuilder( private fun convertPermissionState( lastPermissionState: GiveTxPermissionState, permissionDataState: PermissionDataState, + providerName: String, onGivePermissionClick: () -> Unit, onChangeApproveType: (ApproveType) -> Unit, ): GiveTxPermissionState { @@ -1180,6 +1185,11 @@ internal class StateBuilder( enabled = true, ), onChangeApproveType = onChangeApproveType, + subtitle = resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, permissionDataState.currency), + ), + dialogText = resourceReference(R.string.swapping_approve_information_text), ) } } @@ -1403,12 +1413,12 @@ internal class StateBuilder( } // region warnings - private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig { + private fun createPermissionNotificationConfig(providerName: String, fromTokenSymbol: String): NotificationConfig { return NotificationConfig( title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( id = R.string.give_permission_swap_subtitle, - formatArgs = wrappedList(fromTokenSymbol), + formatArgs = wrappedList(providerName, fromTokenSymbol), ), iconResId = R.drawable.ic_locked_24, ) From 59ad4c5c5cf586f2f7cad914ec856677214bfebc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Jul 2024 14:42:04 +0200 Subject: [PATCH 121/223] Updated on 2026-08-14 --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 7ef818eaa8..f8c3184558 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.13 \ No newline at end of file +versionName=5.13.0 \ No newline at end of file From 98ec93e259b9bc8d89b8f9c521ced90dd73891db Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Jul 2024 15:46:47 +0300 Subject: [PATCH 122/223] Updated on 2026-08-14 --- .../ui/amountScreen/ui/SendDoneButtons.kt | 73 +++++++++++++++++++ .../components/inputrow/InputRowImageBase.kt | 3 +- .../inputrow/InputRowImageChevron.kt | 13 ++-- .../inputrow/InputRowImageSelector.kt | 6 +- .../com/tangem/core/ui/res/TangemDimens.kt | 2 +- .../GetExplorerTransactionUrlUseCase.kt | 1 - .../presentation/ui/SendNavigationButtons.kt | 43 +---------- features/staking/impl/build.gradle.kts | 1 + .../staking/impl/di/StakingRouterModule.kt | 7 +- .../impl/navigation/DefaultStakingRouter.kt | 10 ++- .../impl/navigation/InnerStakingRouter.kt | 2 +- .../impl/presentation/StakingFragment.kt | 11 ++- .../state/InnerConfirmationStakingState.kt | 7 ++ .../state/StakingStateController.kt | 2 +- .../presentation/state/StakingStateRouter.kt | 45 ++++-------- .../impl/presentation/state/StakingUiState.kt | 27 +++---- .../state/TransactionDoneState.kt | 14 ++++ .../impl/presentation/state/ValidatorState.kt | 21 +++++- ...ata.kt => ConfirmationStatePreviewData.kt} | 8 +- .../state/stub/StakingClickIntentsStub.kt | 4 + .../SetConfirmStateInProgressTransformer.kt | 25 ------- ... SetConfirmationStateAssentTransformer.kt} | 14 ++-- ...etConfirmationStateCompletedTransformer.kt | 60 +++++++++++++++ ...tConfirmationStateInProgressTransformer.kt | 27 +++++++ ...SetConfirmationStateLoadingTransformer.kt} | 12 +-- .../SetInitialDataStateTransformer.kt | 12 +-- .../ValidatorSelectChangeTransformer.kt | 6 +- ...ntent.kt => StakingConfirmationContent.kt} | 28 +++++-- .../ui/StakingNavigationButtons.kt | 51 +++++++++---- .../impl/presentation/ui/StakingScreen.kt | 15 ++-- .../ui/StakingValidatorListContent.kt | 4 +- .../presentation/ui/block/ValidatorBlock.kt | 2 + .../viewmodel/StakingClickIntents.kt | 4 + .../viewmodel/StakingViewModel.kt | 57 ++++++++++----- 34 files changed, 407 insertions(+), 210 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/TransactionDoneState.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/{ConfirmStakingStatePreviewData.kt => ConfirmationStatePreviewData.kt} (92%) delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/{SetConfirmStateConfirmTransformer.kt => SetConfirmationStateAssentTransformer.kt} (77%) create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/{SetConfirmStateLoadingTransformer.kt => SetConfirmationStateLoadingTransformer.kt} (74%) rename features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/{StakingConfirmContent.kt => StakingConfirmationContent.kt} (72%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt new file mode 100644 index 0000000000..8689591d9b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt @@ -0,0 +1,73 @@ +package com.tangem.common.ui.amountScreen.ui + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.res.TangemTheme +import com.tangem.common.ui.R +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun SendDoneButtons( + txUrl: String, + onExploreClick: () -> Unit, + onShareClick: () -> Unit, + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + AnimatedVisibility( + visible = isVisible && txUrl.isNotBlank(), + modifier = modifier, + enter = slideInVertically().plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate show sent state buttons", + ) { + Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) { + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + onShareClick() + }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun SendDoneButtons_Preview() { + TangemThemePreview { + SendDoneButtons( + txUrl = "txUrl", + onShareClick = {}, + onExploreClick = {}, + isVisible = true, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 871dccfd94..00213b0077 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -30,8 +30,7 @@ internal fun InputRowImageBase( InputRowAsyncImage( imageUrl = imageUrl, modifier = Modifier - .size(TangemTheme.dimens.spacing36) - .padding(vertical = TangemTheme.dimens.size1), + .size(TangemTheme.dimens.spacing36), ) Column { Text( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt index 83fd0ccbf0..c341f3ceac 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt @@ -35,6 +35,7 @@ fun InputRowImageChevron( modifier: Modifier = Modifier, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, + showChevron: Boolean = true, ) { InputRowImageBase( subtitle = subtitle, @@ -45,11 +46,13 @@ fun InputRowImageChevron( captionColor = captionColor, ) { SpacerWMax() - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) + if (showChevron) { + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt index 9e672eb56b..5231d7f896 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.annotatedReference @@ -65,13 +66,14 @@ fun InputRowImageSelector( ) .padding(TangemTheme.dimens.spacing12), ) { + SpacerWMax() TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect) } } //region preview -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(widthDp = 328) +@Preview(widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun InputRowImageSelectorPreview( @PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData, 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 4d47c4bf0f..9b00a821e5 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 @@ -122,7 +122,7 @@ data class TangemDimens internal constructor( val spacing30: Dp = 30.dp, val spacing32: Dp = 32.dp, val spacing34: Dp = 34.dp, - val spacing36: Dp = 34.dp, + val spacing36: Dp = 36.dp, val spacing38: Dp = 38.dp, val spacing40: Dp = 40.dp, val spacing44: Dp = 44.dp, diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt index cedb309901..991475bef2 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetExplorerTransactionUrlUseCase.kt @@ -11,7 +11,6 @@ import com.tangem.domain.wallets.models.UserWalletId class GetExplorerTransactionUrlUseCase( private val repository: TxHistoryRepository, ) { - @Deprecated("Replace with invoke [UserWalletId, Network]") operator fun invoke(txHash: String, networkId: Network.ID): Either { return either { catch( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index e47d869fb6..3e676181c9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -16,17 +16,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.ui.SendDoneButtons import com.tangem.common.ui.amountScreen.utils.getCryptoReference import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.R import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.SecondaryButtonIconStart import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -220,46 +219,6 @@ private fun SendingText( } } -@Composable -private fun SendDoneButtons( - txUrl: String, - onExploreClick: () -> Unit, - onShareClick: () -> Unit, - isVisible: Boolean, - modifier: Modifier = Modifier, -) { - val hapticFeedback = LocalHapticFeedback.current - val context = LocalContext.current - - AnimatedVisibility( - visible = isVisible && txUrl.isNotBlank(), - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) { - SecondaryButtonIconStart( - text = stringResource(id = R.string.common_explore), - iconResId = R.drawable.ic_web_24, - onClick = onExploreClick, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = stringResource(id = R.string.common_share), - iconResId = R.drawable.ic_share_24, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(txUrl) - onShareClick() - }, - modifier = Modifier.weight(1f), - ) - } - } -} - private fun getButtonData( uiState: SendUiState, currentState: SendUiCurrentScreen, diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 8325def273..15213f79d6 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.models) implementation(projects.domain.transaction) + implementation(projects.domain.txhistory) /** Common */ implementation(projects.common.ui) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt index c351d87a6e..c64b0dd4f2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.di +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import com.tangem.features.staking.api.navigation.StakingRouter import dagger.Module @@ -17,7 +18,9 @@ internal object StakingRouterModule { @Provides @ActivityScoped - fun provideStakingRouter(): StakingRouter { - return DefaultStakingRouter() + fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter { + return DefaultStakingRouter( + urlOpener = urlOpener, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 351a4a7a28..464d28694e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,9 +1,15 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.staking.impl.presentation.StakingFragment -internal class DefaultStakingRouter : InnerStakingRouter { - +internal class DefaultStakingRouter( + private val urlOpener: UrlOpener, +) : InnerStakingRouter { override fun getEntryFragment(): Fragment = StakingFragment.create() + + override fun openUrl(url: String) { + urlOpener.openUrl(url) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index dd7187d902..d4197489fb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -4,5 +4,5 @@ import com.tangem.features.staking.api.navigation.StakingRouter interface InnerStakingRouter : StakingRouter { - // TODO staking + fun openUrl(url: String) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt index 64b167ead5..cb590bcb4c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment @@ -15,7 +16,6 @@ import com.tangem.features.staking.impl.presentation.state.StakingStateRouter import com.tangem.features.staking.impl.presentation.ui.StakingScreen import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel import dagger.hilt.android.AndroidEntryPoint -import java.lang.ref.WeakReference import javax.inject.Inject /** @@ -28,7 +28,10 @@ internal class StakingFragment : ComposeFragment() { override lateinit var uiDependencies: UiDependencies @Inject - lateinit var router: StakingRouter + lateinit var stakingRouter: StakingRouter + + @Inject + lateinit var appRouter: AppRouter @Inject lateinit var stateController: StakingStateController @@ -38,7 +41,7 @@ internal class StakingFragment : ComposeFragment() { private val viewModel by viewModels() private val innerStakingRouter: InnerStakingRouter - get() = requireNotNull(router as? InnerStakingRouter) { + get() = requireNotNull(stakingRouter as? InnerStakingRouter) { "innerStakingRouter should be instance of InnerStakingRouter" } @@ -49,7 +52,7 @@ internal class StakingFragment : ComposeFragment() { viewModel.setRouter( innerStakingRouter, StakingStateRouter( - fragmentManager = WeakReference(parentFragmentManager), + appRouter = appRouter, stateController = stateController, ), ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt new file mode 100644 index 0000000000..babc93a2dd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerConfirmationStakingState.kt @@ -0,0 +1,7 @@ +package com.tangem.features.staking.impl.presentation.state + +enum class InnerConfirmationStakingState { + ASSENT, + IN_PROGRESS, + COMPLETED, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index d62526b9a8..f263b18a14 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -40,7 +40,7 @@ internal class StakingStateController @Inject constructor() { initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), - confirmStakingState = StakingStates.ConfirmStakingState.Empty(), + confirmationState = StakingStates.ConfirmationState.Empty(), isBalanceHidden = false, event = consumedEvent(), bottomSheetConfig = null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 5f277ce4a4..d3edfd4322 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -1,49 +1,37 @@ package com.tangem.features.staking.impl.presentation.state -import androidx.fragment.app.FragmentManager -import java.lang.ref.WeakReference +import com.tangem.common.routing.AppRouter internal class StakingStateRouter( - private val fragmentManager: WeakReference, + private val appRouter: AppRouter, private val stateController: StakingStateController, ) { - private fun closeStaking() { - fragmentManager.get()?.popBackStack() + fun onBackClick() { + appRouter.pop() stateController.clear() } - fun onBackClick(isSuccess: Boolean = false) { - val type = stateController.uiState.value.currentStep - when { - isSuccess -> closeStaking() - else -> when (type) { - StakingStep.Amount -> showInitial() - StakingStep.Confirm -> showAmount() - else -> closeStaking() - } - } - } - fun onNextClick() { when (stateController.uiState.value.currentStep) { StakingStep.InitialInfo -> showAmount() StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Amount, - -> showConfirm() - StakingStep.Confirm -> showSuccess() - StakingStep.Success -> closeStaking() + -> showConfirmation() + StakingStep.Confirmation -> { + // TODO staking handle + } } } fun onPrevClick() { when (stateController.uiState.value.currentStep) { + StakingStep.InitialInfo -> onBackClick() StakingStep.Amount -> showInitial() - StakingStep.Confirm -> showAmount() - StakingStep.Success -> closeStaking() - StakingStep.Validators -> showConfirm() - else -> closeStaking() + StakingStep.Confirmation -> showAmount() + StakingStep.Validators -> showConfirmation() + StakingStep.RewardsValidators -> showInitial() } } @@ -63,12 +51,7 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Validators) } } - fun showConfirm() { - stateController.update { it.copy(currentStep = StakingStep.Confirm) } - } - - fun showSuccess() { - // stateController.update { it.copy(currentStep = StakingStep.Success) } - // TODO staking [REDACTED_TASK_KEY] + fun showConfirmation() { + stateController.update { it.copy(currentStep = StakingStep.Confirmation) } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 65fe669de8..cd1e65b946 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -20,7 +20,7 @@ internal data class StakingUiState( val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, val rewardsValidatorsState: StakingStates.RewardsValidatorsState, - val confirmStakingState: StakingStates.ConfirmStakingState, + val confirmationState: StakingStates.ConfirmationState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, val event: StateEvent, @@ -29,11 +29,11 @@ internal data class StakingUiState( fun copyWrapped( initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, amountState: AmountState = this.amountState, - confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState, + confirmationState: StakingStates.ConfirmationState = this.confirmationState, ): StakingUiState = copy( initialInfoState = initialInfoState, amountState = amountState, - confirmStakingState = confirmStakingState, + confirmationState = confirmationState, ) } @@ -74,27 +74,21 @@ internal sealed class StakingStates { ) : RewardsValidatorsState() } - /** Confirm state */ - sealed class ConfirmStakingState : StakingStates() { + /** Confirmation state */ + sealed class ConfirmationState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val innerState: InnerConfirmationStakingState, val feeState: FeeState, val validatorState: ValidatorState, val notifications: ImmutableList, val footerText: String, - val innerState: InnerConfirmStakingState, - ) : ConfirmStakingState() { - - enum class InnerConfirmStakingState { - CONFIRM, - IN_PROGRESS, - SUCCESS, - } - } + val transactionDoneState: TransactionDoneState, + ) : ConfirmationState() data class Empty( override val isPrimaryButtonEnabled: Boolean = false, - ) : ConfirmStakingState() + ) : ConfirmationState() } } @@ -102,7 +96,6 @@ enum class StakingStep { InitialInfo, RewardsValidators, Amount, - Confirm, + Confirmation, Validators, - Success, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/TransactionDoneState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/TransactionDoneState.kt new file mode 100644 index 0000000000..3af58c716f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/TransactionDoneState.kt @@ -0,0 +1,14 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class TransactionDoneState { + + data class Content( + val timestamp: Long, + val txUrl: String, + ) : TransactionDoneState() + + data object Empty : TransactionDoneState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt index d99ed81d8e..0510d12380 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt @@ -6,12 +6,29 @@ import com.tangem.domain.staking.model.Yield @Immutable internal sealed class ValidatorState { + abstract val isClickable: Boolean + data class Content( + override val isClickable: Boolean, val chosenValidator: Yield.Validator, val availableValidators: List, ) : ValidatorState() - data object Loading : ValidatorState() + data object Loading : ValidatorState() { + override val isClickable: Boolean + get() = false + } - data object Error : ValidatorState() + data object Error : ValidatorState() { + override val isClickable: Boolean + get() = false + } + + fun copySealed(isClickable: Boolean): ValidatorState { + return if (this is Content) { + copy(isClickable = isClickable) + } else { + this + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt similarity index 92% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 34916ee421..3d1e0fcebf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -12,7 +12,7 @@ import com.tangem.features.staking.impl.presentation.state.ValidatorState import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -internal object ConfirmStakingStatePreviewData { +internal object ConfirmationStatePreviewData { private val validatorList = listOf( Yield.Validator( @@ -62,8 +62,9 @@ internal object ConfirmStakingStatePreviewData { ), ) - val confirmStakingState = StakingStates.ConfirmStakingState.Data( + val assentStakingState = StakingStates.ConfirmationState.Data( isPrimaryButtonEnabled = true, + innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( fee = fee, rate = BigDecimal.ONE, @@ -72,6 +73,7 @@ internal object ConfirmStakingStatePreviewData { isFeeConvertibleToFiat = true, ), validatorState = ValidatorState.Content( + isClickable = true, chosenValidator = validatorList[0], availableValidators = validatorList, ), @@ -82,6 +84,6 @@ internal object ConfirmStakingStatePreviewData { days = 2, ), ), - innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, + transactionDoneState = TransactionDoneState.Empty, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 6320dce75c..64e66f66d8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -31,4 +31,8 @@ object StakingClickIntentsStub : StakingClickIntents { override fun openRewardsValidators() {} override fun selectRewardValidator(rewardValue: String) {} + + override fun onExploreClick() {} + + override fun onShareClick() {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt deleted file mode 100644 index edad01bfea..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.utils.transformer.Transformer - -internal class SetConfirmStateInProgressTransformer : Transformer { - - override fun transform(prevState: StakingUiState): StakingUiState { - return prevState.copy( - confirmStakingState = prevState.confirmStakingState.copyWrapped(), - ) - } - - private fun StakingStates.ConfirmStakingState.copyWrapped(): StakingStates.ConfirmStakingState { - return if (this is StakingStates.ConfirmStakingState.Data) { - copy( - isPrimaryButtonEnabled = false, - innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.IN_PROGRESS, - ) - } else { - this - } - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt similarity index 77% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index f058a772c3..df4fc57ae3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -10,9 +10,10 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer import com.tangem.blockchain.common.Amount +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState @Suppress("UnusedPrivateMember") -internal class SetConfirmStateConfirmTransformer( +internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val stakingGasEstimate: StakingGasEstimate, @@ -20,15 +21,16 @@ internal class SetConfirmStateConfirmTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - confirmStakingState = prevState.confirmStakingState.copyWrapped(stakingGasEstimate), + confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate), ) } - private fun StakingStates.ConfirmStakingState.copyWrapped( + private fun StakingStates.ConfirmationState.copyWrapped( gasEstimate: StakingGasEstimate, - ): StakingStates.ConfirmStakingState { - if (this is StakingStates.ConfirmStakingState.Data) { + ): StakingStates.ConfirmationState { + if (this is StakingStates.ConfirmationState.Data) { return copy( + innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( fee = Fee.Common( Amount( @@ -42,8 +44,8 @@ internal class SetConfirmStateConfirmTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), + validatorState = validatorState.copySealed(isClickable = true), isPrimaryButtonEnabled = true, - innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, ) } else { return this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt new file mode 100644 index 0000000000..f83c870fc6 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -0,0 +1,60 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.TransactionDoneState +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer + +internal class SetConfirmationStateCompletedTransformer( + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val stakingGasEstimate: StakingGasEstimate, + private val txUrl: String, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmationState = prevState.confirmationState.copyWrapped(stakingGasEstimate), + ) + } + + private fun StakingStates.ConfirmationState.copyWrapped( + gasEstimate: StakingGasEstimate, + ): StakingStates.ConfirmationState { + if (this is StakingStates.ConfirmationState.Data) { + return copy( + isPrimaryButtonEnabled = true, + innerState = InnerConfirmationStakingState.COMPLETED, + feeState = FeeState.Content( + fee = Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), + rate = cryptoCurrencyStatusProvider().value.fiatRate, + isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + appCurrency = appCurrencyProvider(), + isFeeApproximate = false, + ), + validatorState = validatorState.copySealed( + isClickable = false, + ), + transactionDoneState = TransactionDoneState.Content( + timestamp = System.currentTimeMillis(), + txUrl = txUrl, + ), + ) + } else { + return this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt new file mode 100644 index 0000000000..e17aad84b4 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -0,0 +1,27 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class SetConfirmationStateInProgressTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmationState = prevState.confirmationState.copyWrapped(), + ) + } + + private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { + return if (this is StakingStates.ConfirmationState.Data) { + copy( + isPrimaryButtonEnabled = false, + innerState = InnerConfirmationStakingState.IN_PROGRESS, + validatorState = validatorState.copySealed(isClickable = false), + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt similarity index 74% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 838a5db5cd..0b12d8b8f8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -10,20 +10,22 @@ import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf @Suppress("UnusedPrivateMember") -internal class SetConfirmStateLoadingTransformer( +internal class SetConfirmationStateLoadingTransformer( private val yield: Yield, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - val possibleConfirmStakingState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data - val possibleValidatorState = possibleConfirmStakingState?.validatorState as? ValidatorState.Content + val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val possibleValidatorState = possibleConfirmationState?.validatorState as? ValidatorState.Content val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0] return prevState.copy( - confirmStakingState = StakingStates.ConfirmStakingState.Data( + confirmationState = StakingStates.ConfirmationState.Data( isPrimaryButtonEnabled = false, + innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Loading, validatorState = ValidatorState.Content( + isClickable = false, chosenValidator = chosenValidator, availableValidators = yield.validators, ), @@ -34,7 +36,7 @@ internal class SetConfirmStateLoadingTransformer( ), ), footerText = "", - innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, + transactionDoneState = TransactionDoneState.Empty, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index e66d509f0d..3cc9baa334 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.extensions.remove import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -20,7 +21,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero @@ -61,7 +62,7 @@ internal class SetInitialDataStateTransformer( currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), amountState = createInitialAmountState(), - confirmStakingState = createInitialConfirmationState(), + confirmationState = createInitialConfirmationState(), rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit), bottomSheetConfig = null, ) @@ -98,9 +99,10 @@ internal class SetInitialDataStateTransformer( return amountStateConverter.convert("") } - private fun createInitialConfirmationState(): StakingStates.ConfirmStakingState { - return ConfirmStakingStatePreviewData.confirmStakingState.copy( + private fun createInitialConfirmationState(): StakingStates.ConfirmationState { + return ConfirmationStatePreviewData.assentStakingState.copy( validatorState = ValidatorState.Content( + isClickable = true, chosenValidator = yield.validators.first(), availableValidators = yield.validators, ), @@ -116,7 +118,7 @@ internal class SetInitialDataStateTransformer( val formattedMinApr = BigDecimalFormatter.formatPercent( percent = minApr, useAbsoluteValue = true, - ) + ).remove("%") val formattedMaxApr = BigDecimalFormatter.formatPercent( percent = maxApr, useAbsoluteValue = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index b72f227693..714a89de67 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -10,11 +10,11 @@ internal class ValidatorSelectChangeTransformer( private val selectedValidator: Yield.Validator, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - val confirmState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data ?: return prevState - val validatorState = confirmState.validatorState as? ValidatorState.Content ?: return prevState + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState return prevState.copy( - confirmStakingState = confirmState.copy( + confirmationState = confirmationState.copy( validatorState = validatorState.copy( chosenValidator = selectedValidator, ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt similarity index 72% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index be8589fa2f..98f42fccf2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -14,23 +15,26 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.TransactionDoneState @Composable -internal fun StakingConfirmContent( +internal fun StakingConfirmationContent( amountState: AmountState, - state: StakingStates.ConfirmStakingState, + state: StakingStates.ConfirmationState, clickIntents: StakingClickIntents, ) { - if (state !is StakingStates.ConfirmStakingState.Data) return + if (state !is StakingStates.ConfirmationState.Data) return Column( modifier = Modifier @@ -40,6 +44,16 @@ internal fun StakingConfirmContent( .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { + AnimatedVisibility( + visible = state.transactionDoneState is TransactionDoneState.Content, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) { + val transactionDoneStateContent = state.transactionDoneState as TransactionDoneState.Content + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = transactionDoneStateContent.timestamp, + ) + } AmountBlock( amountState = amountState, isClickDisabled = true, @@ -68,12 +82,12 @@ private fun FooterText(text: String) { @Preview(widthDp = 360, showBackground = true) @Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_StakingConfirmContent() { +private fun Preview_StakingConfirmationContent() { TangemThemePreview { Column(Modifier.background(TangemTheme.colors.background.primary)) { - StakingConfirmContent( + StakingConfirmationContent( amountState = AmountStatePreviewData.amountState, - state = ConfirmStakingStatePreviewData.confirmStakingState, + state = ConfirmationStatePreviewData.assentStakingState, clickIntents = StakingClickIntentsStub, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 1ee7aca2ac..695d1a3bb7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -17,19 +17,22 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.amountScreen.ui.SendDoneButtons import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState @Composable internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) { + val confirmInnerState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState + val isSuccessState = confirmInnerState == InnerConfirmationStakingState.COMPLETED + Column( modifier = modifier .padding( @@ -38,6 +41,15 @@ internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifie bottom = TangemTheme.dimens.spacing16, ), ) { + val confirmationDataState = uiState.confirmationState as? StakingStates.ConfirmationState.Data + val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content + + SendDoneButtons( + txUrl = transactionDoneState?.txUrl.orEmpty(), + onExploreClick = uiState.clickIntents::onExploreClick, + onShareClick = uiState.clickIntents::onShareClick, + isVisible = isSuccessState, + ) StakingNavigationButton( uiState = uiState, modifier = Modifier, @@ -50,13 +62,19 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier val hapticFeedback = LocalHapticFeedback.current val isButtonsVisible = isPrevButtonVisible(uiState.currentStep) - val isStakingState = uiState.currentStep == StakingStep.Confirm + + val innerConfirmState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState + val isInProgressInnerState = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS + val isInAssentInnerState = innerConfirmState == InnerConfirmationStakingState.ASSENT + + val showTangemIcon = uiState.currentStep == StakingStep.Confirmation && + (isInProgressInnerState || isInAssentInnerState) val (buttonTextId, buttonClick) = getButtonData( currentState = uiState, ) val isButtonEnabled = isButtonEnabled(uiState) - val buttonIcon = if (isStakingState) { + val buttonIcon = if (showTangemIcon) { TangemButtonIconPosition.End(R.drawable.ic_tangem_24) } else { TangemButtonIconPosition.None @@ -82,7 +100,6 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier SpacerW12() } } - AnimatedVisibility( visible = buttonClick != null, enter = fadeIn(), @@ -93,7 +110,7 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier icon = buttonIcon, enabled = isButtonEnabled && buttonClick != null, onClick = { - if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) if (buttonClick != null) buttonClick() }, showProgress = false, @@ -116,18 +133,27 @@ private fun getButtonData(currentState: StakingUiState): Pair Unit)? } StakingStep.Amount, -> R.string.common_next to currentState.clickIntents::onNextClick - StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick + StakingStep.Confirmation -> { + val confirmationState = currentState.confirmationState + if (confirmationState is StakingStates.ConfirmationState.Data) { + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { + R.string.common_close to currentState.clickIntents::onBackClick + } else { + R.string.common_stake to currentState.clickIntents::onNextClick + } + } else { + R.string.common_close to currentState.clickIntents::onBackClick + } + } StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick - StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick - else -> R.string.common_next to null + StakingStep.RewardsValidators -> R.string.common_next to null } } private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { StakingStep.InitialInfo, StakingStep.RewardsValidators, - StakingStep.Confirm, - StakingStep.Success, + StakingStep.Confirmation, -> false StakingStep.Amount, StakingStep.Validators, @@ -138,9 +164,8 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean { return when (uiState.currentStep) { StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled - StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled - StakingStep.Success -> true StakingStep.Validators -> true } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 1b92f7fcea..e1b08be7ac 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -67,15 +67,13 @@ private fun SendAppBar(uiState: StakingUiState) { StakingStep.InitialInfo, StakingStep.RewardsValidators, StakingStep.Validators, - StakingStep.Confirm, + StakingStep.Confirmation, -> stringResource(id = R.string.common_stake) - StakingStep.Success -> "" } val backIcon = when (uiState.currentStep) { StakingStep.Amount, StakingStep.Validators, - StakingStep.Confirm, - StakingStep.Success, + StakingStep.Confirmation, -> { R.drawable.ic_close_24 } @@ -153,20 +151,19 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) - StakingStep.Confirm -> StakingConfirmContent( + StakingStep.Confirmation -> StakingConfirmationContent( amountState = uiState.amountState, - state = uiState.confirmStakingState, + state = uiState.confirmationState, clickIntents = uiState.clickIntents, ) StakingStep.Validators -> { - val confirmState = uiState.confirmStakingState - if (confirmState !is StakingStates.ConfirmStakingState.Data) return@AnimatedContent + val confirmState = uiState.confirmationState + if (confirmState !is StakingStates.ConfirmationState.Data) return@AnimatedContent StakingValidatorListContent( state = confirmState.validatorState, clickIntents = uiState.clickIntents, ) } - else -> TODO() } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 1d6e05103d..743979fa67 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -27,7 +27,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.ValidatorState -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import java.math.BigDecimal @@ -124,6 +124,6 @@ private fun StakingValidatorListContent_Preview( private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { override val values: Sequence - get() = sequenceOf(ConfirmStakingStatePreviewData.confirmStakingState.validatorState) + get() = sequenceOf(ConfirmationStatePreviewData.assentStakingState.validatorState) } // endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 4cee3bcba5..ffd1d0c966 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -35,6 +35,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) .clickable( + enabled = validatorState.isClickable, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = onClick, @@ -63,6 +64,7 @@ internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) ), ), imageUrl = validatorState.chosenValidator.image.orEmpty(), + showChevron = validatorState.isClickable, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 62f5fc6c5c..0173545ee9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -23,4 +23,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun openRewardsValidators() fun selectRewardValidator(rewardValue: String) + + fun onExploreClick() + + fun onShareClick() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 7b76ca2694..61d9860dbb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -56,6 +57,7 @@ internal class StakingViewModel @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase, private val sendTransactionUseCase: SendTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -97,18 +99,15 @@ internal class StakingViewModel @Inject constructor( } override fun onNextClick() { - handleOnNextConfirmClick() + handleOnNextConfirmationClick() stakingStateRouter.onNextClick() - if (value.currentStep == StakingStep.Confirm) { + if (isAssentState()) { initStaking() } } - private fun handleOnNextConfirmClick() { - if (value.currentStep == StakingStep.Confirm && - (value.confirmStakingState as? StakingStates.ConfirmStakingState.Data)?.innerState == - StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM - ) { + private fun handleOnNextConfirmationClick() { + if (isAssentState()) { viewModelScope.launch { stakingTransaction?.unsignedTransaction?.let { sendStakingTransaction(TransactionData.Compiled(value = it.hexToBytes())) @@ -120,7 +119,7 @@ internal class StakingViewModel @Inject constructor( private fun initStaking() { viewModelScope.launch { stateController.update( - SetConfirmStateLoadingTransformer( + SetConfirmationStateLoadingTransformer( yield = yield, ), ) @@ -144,7 +143,7 @@ internal class StakingViewModel @Inject constructor( val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info") stateController.update( - SetConfirmStateConfirmTransformer( + SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = stakingGasEstimate, @@ -196,6 +195,20 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onNextClick() } + override fun onExploreClick() { + val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data + val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content + val txUrl = transactionDoneState?.txUrl + + if (txUrl != null) { + innerRouter.openUrl(txUrl) + } + } + + override fun onShareClick() { + // TODO staking analytics event + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -254,7 +267,7 @@ internal class StakingViewModel @Inject constructor( } private suspend fun sendStakingTransaction(txData: TransactionData) { - stateController.update(SetConfirmStateInProgressTransformer()) + stateController.update(SetConfirmationStateInProgressTransformer()) sendTransactionUseCase( txData = txData, @@ -265,29 +278,35 @@ internal class StakingViewModel @Inject constructor( Timber.e(error.toString()) val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold stateController.update( - SetConfirmStateConfirmTransformer( + SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = gasEstimate, ), ) - // uiState = eventStateFactory.getSendTransactionErrorState( - // error = error, - // onConsume = { uiState = eventStateFactory.onConsumeEventState() }, - // ) - // analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) }, - ifRight = { + ifRight = { txHash -> val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold + + val txUrl = getExplorerTransactionUrlUseCase( + txHash = txHash, + networkId = cryptoCurrencyStatus.currency.network.id, + ).getOrElse { "" } stateController.update( - SetConfirmStateConfirmTransformer( + SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = gasEstimate, + txUrl = txUrl, ), ) - // sendScreenAnalyticSender.sendTransaction() }, ) } + + private fun isAssentState(): Boolean { + return value.currentStep == StakingStep.Confirmation && + (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == + InnerConfirmationStakingState.ASSENT + } } \ No newline at end of file From 320b3deffbfc84955c39e61242ce5acd626f57f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 12:46:32 +0300 Subject: [PATCH 123/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 14 +++++ .../tap/di/domain/StakingDomainModule.kt | 18 ++++++ .../local/preferences/PreferencesKeys.kt | 2 + .../local/token/DefaultUserTokensStore.kt | 15 ----- data/staking/build.gradle.kts | 4 +- .../data/staking/DefaultStakingRepository.kt | 61 +++++++++++++++++-- .../data/staking/di/StakingDataModule.kt | 3 + .../staking/SaveUnsubmittedHashUseCase.kt | 22 +++++++ .../staking/SendUnsubmittedHashesUseCase.kt | 16 +++++ .../domain/staking/SubmitHashUseCase.kt | 19 ++++++ .../model/UnsubmittedTransactionMetadata.kt | 6 ++ .../staking/repositories/StakingRepository.kt | 6 ++ .../repository/MockStakingRepository.kt | 9 +++ .../viewmodel/StakingViewModel.kt | 26 +++++++- 14 files changed, 199 insertions(+), 22 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index edd4f38350..2708e92fb4 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -47,6 +47,7 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -88,6 +89,7 @@ import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -149,6 +151,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var settingsRepository: SettingsRepository + @Inject + lateinit var sendUnsubmittedHashesUseCase: SendUnsubmittedHashesUseCase + @Inject lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase @@ -225,6 +230,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac observeStateUpdates() observePolkadotAccountHealthCheck() + sendStakingUnsubmittedHashes() if (intent != null) { deepLinksRegistry.launch(intent) @@ -583,4 +589,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } } + + private fun sendStakingUnsubmittedHashes() { + lifecycleScope.launch { + sendUnsubmittedHashesUseCase.invoke() + .onRight { Timber.d("Submitting hashes succeeded") } + .onLeft { Timber.e(it) } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 12aad7794a..49ee51f726 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -61,4 +61,22 @@ internal object StakingDomainModule { fun provideCreateEnterActionUseCase(stakingRepository: StakingRepository): InitializeStakingProcessUseCase { return InitializeStakingProcessUseCase(stakingRepository) } + + @Provides + @Singleton + fun provideSubmitHashUseCase(stakingRepository: StakingRepository): SubmitHashUseCase { + return SubmitHashUseCase(stakingRepository) + } + + @Provides + @Singleton + fun provideSaveUnsubmittedHashUseCase(stakingRepository: StakingRepository): SaveUnsubmittedHashUseCase { + return SaveUnsubmittedHashUseCase(stakingRepository) + } + + @Provides + @Singleton + fun provideSendUnsubmittedHashesUseCase(stakingRepository: StakingRepository): SendUnsubmittedHashesUseCase { + return SendUnsubmittedHashesUseCase(stakingRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 722573fc37..c517d9fdec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -85,6 +85,8 @@ object PreferencesKeys { val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") } + val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") } + val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoOkxShown") } 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 deleted file mode 100644 index 23cea085d6..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt +++ /dev/null @@ -1,15 +0,0 @@ -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 "user_tokens_${key.stringValue}" - } -} \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 009048e07e..0a1688dee1 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -11,7 +11,6 @@ android { } dependencies { - /** Core modules */ implementation(projects.core.datasource) implementation(projects.core.utils) @@ -33,14 +32,17 @@ dependencies { // endregion // region Others dependencies + implementation(deps.androidx.datastore) implementation(deps.jodatime) implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(projects.libs.blockchainSdk) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } + // endregion } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index edb32a111b..c9c22d3596 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -14,11 +14,11 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionStatusCo import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.api.stakekit.models.request.Address -import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody -import com.tangem.datasource.api.stakekit.models.request.EnterActionRequestBody -import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow @@ -33,14 +33,16 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.toFormattedString +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.math.BigDecimal -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, + private val appPreferencesStore: AppPreferencesStore, private val stakingYieldsStore: StakingYieldsStore, private val stakingBalanceStore: StakingBalanceStore, private val cacheRegistry: CacheRegistry, @@ -372,6 +374,55 @@ internal class DefaultStakingRepository( } } + override suspend fun submitHash(transactionId: String, transactionHash: String) { + withContext(dispatchers.io) { + stakeKitApi.submitTransactionHash( + transactionId = transactionId, + body = SubmitTransactionHashRequestBody( + hash = transactionHash, + ), + ) + } + } + + override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) { + withContext(dispatchers.io) { + appPreferencesStore.editData { preferences -> + val savedTransactions = preferences.getObjectListOrDefault( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + default = emptyList(), + ) + + preferences.setObjectList( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + value = savedTransactions + unsubmittedTransactionMetadata, + ) + } + } + } + + override suspend fun sendUnsubmittedHashes() { + withContext(NonCancellable) { + val savedTransactions = appPreferencesStore.getObjectListSync( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + ) + + savedTransactions.forEach { + stakeKitApi.submitTransactionHash( + transactionId = it.transactionId, + body = SubmitTransactionHashRequestBody(hash = it.transactionHash), + ) + } + + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectList( + key = PreferencesKeys.UNSUBMITTED_TRANSACTIONS_KEY, + value = emptyList(), + ) + } + } + } + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index b855fe3386..87b0102d21 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -3,6 +3,7 @@ package com.tangem.data.staking.di import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository @@ -22,6 +23,7 @@ internal object StakingDataModule { @Singleton fun provideStakingRepository( stakeKitApi: StakeKitApi, + appPreferencesStore: AppPreferencesStore, stakingTokenStore: StakingYieldsStore, stakingBalanceStore: StakingBalanceStore, dispatchers: CoroutineDispatcherProvider, @@ -30,6 +32,7 @@ internal object StakingDataModule { ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, + appPreferencesStore = appPreferencesStore, stakingYieldsStore = stakingTokenStore, stakingBalanceStore = stakingBalanceStore, dispatchers = dispatchers, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt new file mode 100644 index 0000000000..009469c351 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for saving hash that failed to submit during staking confirmation + */ +class SaveUnsubmittedHashUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke(transactionId: String, transactionHash: String): Either { + return Either.catch { + stakingRepository.storeUnsubmittedHash( + unsubmittedTransactionMetadata = UnsubmittedTransactionMetadata( + transactionId = transactionId, + transactionHash = transactionHash, + ), + ) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt new file mode 100644 index 0000000000..5c2fbd531d --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for commiting hashes that failed to submit during staking confirmation + */ +class SendUnsubmittedHashesUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke(): Either { + return Either.catch { + stakingRepository.sendUnsubmittedHashes() + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt new file mode 100644 index 0000000000..091d9a2f6b --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for submitting transaction hash to stakekit + */ +class SubmitHashUseCase(private val stakingRepository: StakingRepository) { + + suspend fun submitHash(transactionId: String, transactionHash: String): Either { + return Either.catch { + stakingRepository.submitHash( + transactionId = transactionId, + transactionHash = transactionHash, + ) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt new file mode 100644 index 0000000000..e57067b382 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.model + +data class UnsubmittedTransactionMetadata( + val transactionHash: String, + val transactionId: String, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 854aa92a93..8b74784341 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -69,4 +69,10 @@ interface StakingRepository { ): EnterAction suspend fun constructTransaction(transactionId: String): StakingTransaction + + suspend fun submitHash(transactionId: String, transactionHash: String) + + suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) + + suspend fun sendUnsubmittedHashes() } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index e6436b5f62..074c07f027 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -200,4 +200,13 @@ class MockStakingRepository : StakingRepository { ledgerHwAppId = null, isMessage = false, ) + + override suspend fun submitHash(transactionId: String, transactionHash: String) { + } + + override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) { + } + + override suspend fun sendUnsubmittedHashes() { + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 61d9860dbb..77a9005b01 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -15,6 +15,8 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.InitializeStakingProcessUseCase +import com.tangem.domain.staking.SaveUnsubmittedHashUseCase +import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase @@ -58,6 +60,8 @@ internal class StakingViewModel @Inject constructor( private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, + private val submitHashUseCase: SubmitHashUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -286,12 +290,17 @@ internal class StakingViewModel @Inject constructor( ) }, ifRight = { txHash -> - val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold + val transaction = stakingTransaction ?: return@fold + + submitHash(transaction.id, txHash) val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrencyStatus.currency.network.id, ).getOrElse { "" } + + val gasEstimate = transaction.gasEstimate ?: error("No gas for transaction") + stateController.update( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -304,6 +313,21 @@ internal class StakingViewModel @Inject constructor( ) } + private suspend fun submitHash(transactionId: String, transactionHash: String) { + submitHashUseCase.submitHash( + transactionId = transactionId, + transactionHash = transactionHash, + ) + .onLeft { + saveUnsubmittedHashUseCase.invoke( + transactionId = transactionId, + transactionHash = transactionHash, + ) + }.onRight { + Timber.d("Successful hash submission") + } + } + private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == From 3ff16909dfbbbced14f2ee7f83bb44ad0a20d31b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 4 Jul 2024 13:25:24 +0400 Subject: [PATCH 124/223] Updated on 2026-08-14 --- .../permission}/GiveTxPermisssionBottomSheet.kt | 4 ++-- .../permission}/state/GiveTxPermissionBottomSheetConfig.kt | 2 +- .../permission}/state/GiveTxPermissionState.kt | 2 +- .../java/com/tangem/feature/swap/analytics/SwapEvents.kt | 2 +- .../java/com/tangem/feature/swap/models/SwapStateHolder.kt | 2 +- .../main/java/com/tangem/feature/swap/models/UiActions.kt | 2 +- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 2 +- .../src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt | 7 ++++--- .../java/com/tangem/feature/swap/ui/SwapScreenContent.kt | 2 +- .../tangem/feature/swap/viewmodels/SwapProcessDataState.kt | 2 +- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 2 +- 11 files changed, 15 insertions(+), 14 deletions(-) rename common/ui/src/main/java/com/tangem/common/ui/{bottomsheets => bottomsheet/permission}/GiveTxPermisssionBottomSheet.kt (99%) rename common/ui/src/main/java/com/tangem/common/ui/{bottomsheets => bottomsheet/permission}/state/GiveTxPermissionBottomSheetConfig.kt (80%) rename common/ui/src/main/java/com/tangem/common/ui/{bottomsheets => bottomsheet/permission}/state/GiveTxPermissionState.kt (95%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt similarity index 99% rename from common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index e0d8b57dd7..9ced912bdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.bottomsheets +package com.tangem.common.ui.bottomsheet.permission import android.content.res.Configuration import androidx.compose.foundation.background @@ -25,7 +25,7 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.window.PopupProperties import com.tangem.common.ui.R -import com.tangem.common.ui.bottomsheets.state.* +import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons import com.tangem.core.ui.components.appbar.models.AdditionalButton diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionBottomSheetConfig.kt similarity index 80% rename from common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionBottomSheetConfig.kt index ad5b970ea9..08ffdcc086 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionBottomSheetConfig.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.bottomsheets.state +package com.tangem.common.ui.bottomsheet.permission.state import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt similarity index 95% rename from common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt index 5f8cc96667..dbdce22ba9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.bottomsheets.state +package com.tangem.common.ui.bottomsheet.permission.state import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList 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 fd4f8c9775..39df6ae13c 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.common.ui.bottomsheets.state.ApproveType +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.FeeType diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index e3d7c3d5c8..f901691c85 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index c0c890750e..c25fc38225 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.models -import com.tangem.common.ui.bottomsheets.state.ApproveType +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 23a67bb6a5..427d112540 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -2,7 +2,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.common.ui.bottomsheets.state.* +import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 9ae4b97bca..7379f7ad2a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -1,13 +1,14 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.tangem.common.ui.bottomsheets.GiveTxPermissionBottomSheet -import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionBottomSheetConfig +import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 582ba1f779..d58600cab3 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout -import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 969fadc27a..8a505fb148 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.viewmodels -import com.tangem.common.ui.bottomsheets.state.ApproveType +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 9e9a4eec98..0c7b32d114 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -9,7 +9,7 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle -import com.tangem.common.ui.bottomsheets.state.ApproveType +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic From 97dc754f0f5f159468db92dcc5d66676d8d5e76c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 17:17:29 +0400 Subject: [PATCH 125/223] Updated on 2026-08-14 --- .../core/ui/components/appbar/AppBar.kt | 55 --- .../appbar/AppBarWithAdditionalButtons.kt | 179 ---------- .../components/appbar/AppBarWithBackButton.kt | 53 +-- .../appbar/AppBarWithBackButtonAndIcon.kt | 44 +-- .../AppBarWithBackButtonAndIconContent.kt | 127 ------- .../ui/components/appbar/TangemTopAppBar.kt | 336 ++++++++++++++++++ .../ui/components/appbar/TopAppBarButton.kt | 26 ++ .../appbar/models/AdditionalButton.kt | 8 - .../appbar/models/TopAppBarButtonUM.kt | 19 + .../appbar/models/TopAppBarMedium.kt | 75 ---- 10 files changed, 409 insertions(+), 513 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt deleted file mode 100644 index 80afe74fd7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.core.ui.components.appbar - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -/** - * App bar without buttons - * - * @param text title - * @param modifier modifier - * - * @see Figma component - */ -@Composable -fun AppBar(text: TextReference, modifier: Modifier = Modifier) { - Box(modifier = modifier.fillMaxWidth()) { - Text( - text = text.resolveReference(), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.subtitle1, - modifier = Modifier - .align(Alignment.Center) - .padding(vertical = TangemTheme.dimens.spacing10), - ) - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun AppBar_Preview() { - TangemThemePreview { - AppBar( - text = stringReference("Tangem"), - modifier = Modifier.background(TangemTheme.colors.background.primary), - ) - } -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt deleted file mode 100644 index 07f48d4787..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.tangem.core.ui.components.appbar - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -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.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.components.appbar.models.AdditionalButton -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -/** - * App bar with title and two additional buttons - * - * @param startButton button information attached to the left edge - * @param endButton button information attached to the right edge - * - * @see - * Figma Component - */ -@Composable -fun AppBarWithAdditionalButtons( - text: TextReference, - modifier: Modifier = Modifier, - startButton: AdditionalButton? = null, - endButton: AdditionalButton? = null, - textColor: Color = TangemTheme.colors.text.primary1, - iconColor: Color = TangemTheme.colors.icon.primary1, -) { - AppBarWithAdditionalButtons( - text = text.resolveReference(), - startButton = startButton, - endButton = endButton, - modifier = modifier, - textColor = textColor, - iconColor = iconColor, - ) -} - -/** - * App bar with title and two additional buttons - * - * @param startButton button information attached to the left edge - * @param endButton button information attached to the right edge - * - * @see - * Figma Component - */ -@Composable -fun AppBarWithAdditionalButtons( - text: String, - modifier: Modifier = Modifier, - startButton: AdditionalButton? = null, - endButton: AdditionalButton? = null, - textColor: Color = TangemTheme.colors.text.primary1, - iconColor: Color = TangemTheme.colors.icon.primary1, -) { - Box( - modifier = modifier - .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing16), - ) { - if (startButton != null) { - Icon( - painter = painterResource(id = startButton.iconRes), - contentDescription = null, - tint = iconColor, - modifier = Modifier - .size(TangemTheme.dimens.size24) - .align(Alignment.CenterStart) - .clickable( - onClick = startButton.onIconClicked, - role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = androidx.compose.material.ripple.rememberRipple( - bounded = false, - radius = TangemTheme.dimens.size24 / 2, - ), - ), - ) - } - - Text( - text = text, - modifier = Modifier.align(Alignment.Center), - color = textColor, - maxLines = 1, - style = TangemTheme.typography.subtitle1, - ) - - if (endButton != null) { - Icon( - painter = painterResource(id = endButton.iconRes), - contentDescription = null, - tint = iconColor, - modifier = Modifier - .size(TangemTheme.dimens.size24) - .align(Alignment.CenterEnd) - .clickable( - onClick = endButton.onIconClicked, - role = Role.Button, - interactionSource = remember { MutableInteractionSource() }, - indication = androidx.compose.material.ripple.rememberRipple( - bounded = false, - radius = TangemTheme.dimens.size24 / 2, - ), - ), - ) - } - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AppBarWithAdditionalButtons() { - TangemThemePreview { - AppBarWithAdditionalButtons( - text = "Tangem", - startButton = AdditionalButton( - iconRes = R.drawable.ic_scan_24, - onIconClicked = {}, - ), - endButton = AdditionalButton( - iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = {}, - ), - modifier = Modifier.background(TangemTheme.colors.background.secondary), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AppBarWithOnlyStartButtons() { - TangemThemePreview { - AppBarWithAdditionalButtons( - text = "Tangem", - startButton = AdditionalButton( - iconRes = R.drawable.ic_scan_24, - onIconClicked = {}, - ), - modifier = Modifier.background(TangemTheme.colors.background.secondary), - ) - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AppBarWithOnlyEndButtons() { - TangemThemePreview { - AppBarWithAdditionalButtons( - text = "Tangem", - endButton = AdditionalButton( - iconRes = R.drawable.ic_more_vertical_24, - onIconClicked = {}, - ), - modifier = Modifier.background(TangemTheme.colors.background.secondary), - ) - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt index 80d3014615..65304bcf57 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButton.kt @@ -2,22 +2,12 @@ package com.tangem.core.ui.components.appbar import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme /** * App bar with back button and optional title @@ -37,37 +27,17 @@ fun AppBarWithBackButton( text: String? = null, @DrawableRes iconRes: Int? = null, ) { - Row( - modifier = modifier - .background(color = TangemTheme.colors.background.secondary) - .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(iconRes ?: R.drawable.ic_back_24), - contentDescription = null, - modifier = Modifier - .size(size = TangemTheme.dimens.size24) - .clickable( - indication = rememberRipple(bounded = false), - interactionSource = remember { MutableInteractionSource() }, - onClick = onBackClick, - ), - tint = TangemTheme.colors.icon.primary1, - ) - if (!text.isNullOrBlank()) { - Text( - text = text, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.subtitle1, - ) - } - } + TangemTopAppBar( + modifier = modifier, + title = text, + startButton = TopAppBarButtonUM( + iconRes = iconRes ?: R.drawable.ic_back_24, + onIconClicked = onBackClick, + ), + ) } +// region Preview @Preview(widthDp = 360, heightDp = 56, showBackground = true) @Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -75,4 +45,5 @@ private fun PreviewAppBarWithBackButton() { TangemThemePreview { AppBarWithBackButton(text = "Title", onBackClick = {}) } -} \ No newline at end of file +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index 0006bdad9d..aaa6fcf804 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -2,18 +2,14 @@ package com.tangem.core.ui.components.appbar import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.compose.animation.* -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview @Composable fun AppBarWithBackButtonAndIcon( @@ -26,30 +22,22 @@ fun AppBarWithBackButtonAndIcon( onIconClick: (() -> Unit)? = null, backgroundColor: Color = TangemTheme.colors.background.secondary, ) { - AppBarWithBackButtonAndIconContent( - onBackClick = onBackClick, + TangemTopAppBar( modifier = modifier, - text = text, + title = text, subtitle = subtitle, - backIconRes = backIconRes, - backgroundColor = backgroundColor, - iconContent = { - AnimatedContent( - targetState = iconRes, - transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) }, - label = "Toolbar icon change", - ) { - if (onIconClick != null && it != null) { - Icon( - painter = painterResource(it), - contentDescription = null, - modifier = Modifier - .size(size = TangemTheme.dimens.size24) - .clickable { onIconClick() }, - tint = TangemTheme.colors.icon.primary1, - ) - } - } + containerColor = backgroundColor, + startButton = TopAppBarButtonUM( + iconRes = backIconRes ?: R.drawable.ic_back_24, + onIconClicked = onBackClick, + ), + endButton = if (iconRes != null && onIconClick != null) { + TopAppBarButtonUM( + iconRes = iconRes, + onIconClicked = onIconClick, + ) + } else { + null }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt deleted file mode 100644 index fae3598a38..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.tangem.core.ui.components.appbar - -import android.content.res.Configuration -import androidx.annotation.DrawableRes -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple -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.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme - -/** - * App bar with back button and icon content for any possible number/style of icons - * - * @param onBackClick callback for back button - * @param modifier modifier - * @param text appbar title - * @param backIconRes icon for back button - * @param backgroundColor background color - * @param iconContent icon content - */ -@Composable -fun AppBarWithBackButtonAndIconContent( - onBackClick: () -> Unit, - modifier: Modifier = Modifier, - text: String? = null, - subtitle: String? = null, - @DrawableRes backIconRes: Int? = null, - backIconTint: Color = TangemTheme.colors.icon.primary1, - backgroundColor: Color = TangemTheme.colors.background.secondary, - iconContent: @Composable () -> Unit, -) { - Row( - modifier = modifier - .height(TangemTheme.dimens.size56) - .background(color = backgroundColor) - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(backIconRes ?: R.drawable.ic_back_24), - contentDescription = null, - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing16) - .size(size = TangemTheme.dimens.size24) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - ) { onBackClick() }, - tint = backIconTint, - ) - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier.weight(1f) - .animateContentSize(), - ) { - AnimatedVisibility( - visible = !text.isNullOrBlank(), - enter = fadeIn(), - exit = fadeOut(), - label = "Toolbar title change", - ) { - Text( - text = text.orEmpty(), - color = TangemTheme.colors.text.primary1, - maxLines = 1, - style = TangemTheme.typography.subtitle1, - ) - } - AnimatedVisibility( - visible = !subtitle.isNullOrBlank(), - enter = fadeIn().plus(expandVertically()), - exit = fadeOut().plus(shrinkVertically()), - label = "Toolbar subtitle change", - ) { - Text( - text = subtitle.orEmpty(), - color = TangemTheme.colors.text.secondary, - maxLines = 1, - style = TangemTheme.typography.caption2, - ) - } - } - iconContent() - } -} - -@Preview(widthDp = 360, heightDp = 56, showBackground = true) -@Preview(widthDp = 360, heightDp = 56, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewAppBarWithBackButtonAndIcon() { - TangemThemePreview { - AppBarWithBackButtonAndIconContent( - text = "Title", - subtitle = "Subtitle", - onBackClick = {}, - iconContent = { - Row { - Icon( - painter = painterResource(id = R.drawable.ic_qrcode_scan_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Icon( - painter = painterResource(id = R.drawable.ic_flash_on_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } - }, - ) - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt new file mode 100644 index 0000000000..716d79978f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TangemTopAppBar.kt @@ -0,0 +1,336 @@ +package com.tangem.core.ui.components.appbar + +import android.content.res.Configuration +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [TangemTopAppBar] height options. + * */ +enum class TangemTopAppBarHeight { + BOTTOM_SHEET, DEFAULT, +} + +/** + * Top app bar with two optional buttons: [startButton] and [endButton]. + * + * @see + * Figma Component + */ +@Composable +fun TangemTopAppBar( + modifier: Modifier = Modifier, + startButton: TopAppBarButtonUM? = null, + endButton: TopAppBarButtonUM? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconTint: Color = TangemTheme.colors.icon.primary1, + containerColor: Color = Color.Transparent, + height: TangemTopAppBarHeight = TangemTopAppBarHeight.DEFAULT, +) { + TangemTopAppBar( + modifier = modifier, + title = null, + startButton = startButton, + endButton = endButton, + textColor = textColor, + iconTint = iconTint, + containerColor = containerColor, + height = height, + ) +} + +/** + * Top app bar with [title], optional [subtitle] and two optional buttons: [startButton] and [endButton]. + * + * Where [title] and [subtitle] are [TextReference]. + * + * @see + * Figma Component + */ +@Composable +fun TangemTopAppBar( + title: TextReference, + modifier: Modifier = Modifier, + subtitle: TextReference? = null, + startButton: TopAppBarButtonUM? = null, + endButton: TopAppBarButtonUM? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconTint: Color = TangemTheme.colors.icon.primary1, + titleAlignment: Alignment.Horizontal = Alignment.Start, + containerColor: Color = Color.Transparent, + height: TangemTopAppBarHeight = TangemTopAppBarHeight.DEFAULT, +) { + TangemTopAppBar( + modifier = modifier, + title = title.resolveReference(), + subtitle = subtitle?.resolveReference(), + startButton = startButton, + endButton = endButton, + textColor = textColor, + iconTint = iconTint, + titleAlignment = titleAlignment, + containerColor = containerColor, + height = height, + ) +} + +/** + * Top app bar with [title], optional [subtitle] and two optional buttons: [startButton] and [endButton]. + * + * Where [title] and [subtitle] are [String]. + * + * @see + * Figma Component + */ +@Composable +fun TangemTopAppBar( + title: String?, + modifier: Modifier = Modifier, + subtitle: String? = null, + startButton: TopAppBarButtonUM? = null, + endButton: TopAppBarButtonUM? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconTint: Color = TangemTheme.colors.icon.primary1, + titleAlignment: Alignment.Horizontal = Alignment.Start, + containerColor: Color = Color.Transparent, + height: TangemTopAppBarHeight = TangemTopAppBarHeight.DEFAULT, +) { + TangemTopAppBar( + title = title, + modifier = modifier, + subtitle = subtitle, + startButton = startButton, + textColor = textColor, + iconTint = iconTint, + titleAlignment = titleAlignment, + containerColor = containerColor, + height = height, + endContent = { + if (endButton != null) { + TopAppBarButton( + button = endButton, + tint = iconTint, + ) + } + }, + ) +} + +/** + * Top app bar with [title], optional [subtitle], optional [startButton] and [endContent]. + * + * Where [title] and [subtitle] are [String] and [endContent] is a lambda that provides a [RowScope] to build + * the end content. + * + * @see + * Figma Component + */ +@Composable +fun TangemTopAppBar( + title: String?, + modifier: Modifier = Modifier, + subtitle: String? = null, + startButton: TopAppBarButtonUM? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconTint: Color = TangemTheme.colors.icon.primary1, + titleAlignment: Alignment.Horizontal = Alignment.Start, + containerColor: Color = Color.Transparent, + height: TangemTopAppBarHeight = TangemTopAppBarHeight.DEFAULT, + endContent: @Composable RowScope.() -> Unit, +) { + Row( + modifier = modifier + .background(color = containerColor) + .fillMaxWidth() + .heightIn(min = height.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing12) + .size(TangemTheme.dimens.size32), + contentAlignment = Alignment.Center, + ) { + if (startButton != null) { + TopAppBarButton( + button = startButton, + tint = iconTint, + ) + } + } + + TopAppBarTitle( + modifier = Modifier.weight(1f), + title = title, + subtitle = subtitle, + textColor = textColor, + titleAlignment = titleAlignment, + ) + + Row( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing12) + .widthIn(min = TangemTheme.dimens.size32) + .height(TangemTheme.dimens.size32), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.End), + content = endContent, + ) + } +} + +@Composable +private fun TopAppBarTitle( + title: String?, + subtitle: String?, + textColor: Color, + titleAlignment: Alignment.Horizontal, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + AnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + visible = !title.isNullOrBlank(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = titleAlignment, + ) { + Text( + text = title.orEmpty(), + style = TangemTheme.typography.subtitle1, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + AnimatedVisibility( + visible = !subtitle.isNullOrBlank(), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + label = "Toolbar subtitle visibility", + ) { + Text( + text = subtitle.orEmpty(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +private val TangemTopAppBarHeight.dp + @Composable + @ReadOnlyComposable + get() = when (this) { + TangemTopAppBarHeight.BOTTOM_SHEET -> TangemTheme.dimens.size44 + TangemTopAppBarHeight.DEFAULT -> TangemTheme.dimens.size56 + } + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_BasicTopAppBar( + @PreviewParameter(BasicTopAppBarPMPreviewProvider::class) params: BasicTopAppBarPM, +) { + TangemThemePreview { + TangemTopAppBar( + title = params.title, + subtitle = params.subtitle, + startButton = params.startButton, + endButton = params.endButton, + titleAlignment = params.titleAlignment, + containerColor = TangemTheme.colors.background.secondary, + height = params.height, + ) + } +} + +private data class BasicTopAppBarPM( + val title: String? = "Tangem", + val subtitle: String? = null, + val startButton: TopAppBarButtonUM? = null, + val endButton: TopAppBarButtonUM? = null, + val titleAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + val height: TangemTopAppBarHeight = TangemTopAppBarHeight.DEFAULT, +) + +private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + BasicTopAppBarPM( + subtitle = "Subtitle", + titleAlignment = Alignment.Start, + startButton = TopAppBarButtonUM.Back { }, + ), + BasicTopAppBarPM( + title = null, + startButton = TopAppBarButtonUM.Back { }, + ), + BasicTopAppBarPM( + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ), + BasicTopAppBarPM( + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_scan_24, + onIconClicked = {}, + ), + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_more_vertical_24, + onIconClicked = {}, + ), + ), + BasicTopAppBarPM( + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_scan_24, + onIconClicked = {}, + ), + ), + BasicTopAppBarPM( + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_more_vertical_24, + onIconClicked = {}, + ), + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ), + BasicTopAppBarPM( + title = "1234567891011121314151617181920", + subtitle = "12345678910111213141516171819202122232425", + titleAlignment = Alignment.Start, + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_scan_24, + onIconClicked = {}, + ), + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_more_vertical_24, + onIconClicked = {}, + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt new file mode 100644 index 0000000000..160e0148da --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/TopAppBarButton.kt @@ -0,0 +1,26 @@ +package com.tangem.core.ui.components.appbar + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun TopAppBarButton(button: TopAppBarButtonUM, tint: Color, modifier: Modifier = Modifier) { + IconButton( + modifier = modifier.size(TangemTheme.dimens.size32), + onClick = button.onIconClicked, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = button.iconRes), + tint = tint, + contentDescription = null, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt deleted file mode 100644 index 2517f5ef41..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/AdditionalButton.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.core.ui.components.appbar.models - -import androidx.annotation.DrawableRes - -data class AdditionalButton( - @DrawableRes val iconRes: Int, - val onIconClicked: () -> Unit, -) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt new file mode 100644 index 0000000000..61e3f25d39 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarButtonUM.kt @@ -0,0 +1,19 @@ +package com.tangem.core.ui.components.appbar.models + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.R + +data class TopAppBarButtonUM( + @DrawableRes val iconRes: Int, + val onIconClicked: () -> Unit, +) { + + @Suppress("FunctionName") + companion object { + + fun Back(onBackClicked: () -> Unit) = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = onBackClicked, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt deleted file mode 100644 index d50a8f5c75..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.core.ui.components.appbar.models - -import androidx.compose.foundation.layout.size -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme - -private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TopAppBarMedium( - title: TextReference, - scrollBehavior: TopAppBarScrollBehavior, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, - navigationIconResId: Int = R.drawable.ic_back_24, - colors: TopAppBarColors = TangemTopAppBarColors, -) { - MediumTopAppBar( - modifier = modifier, - scrollBehavior = scrollBehavior, - colors = colors, - title = { - val collapsedStyle = TangemTheme.typography.subtitle1 - val expandedStyle = TangemTheme.typography.h1 - val style by remember(scrollBehavior.state.collapsedFraction) { - derivedStateOf { - if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) { - collapsedStyle - } else { - expandedStyle - } - } - } - - Text( - text = title.resolveReference(), - style = style, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - navigationIcon = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onBackClick, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = navigationIconResId), - contentDescription = null, - ) - } - }, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -internal val TangemTopAppBarColors: TopAppBarColors - @Composable - @ReadOnlyComposable - get() = TopAppBarColors( - containerColor = TangemTheme.colors.background.secondary, - scrolledContainerColor = TangemTheme.colors.background.secondary, - navigationIconContentColor = TangemTheme.colors.icon.primary1, - titleContentColor = TangemTheme.colors.text.primary1, - actionIconContentColor = TangemTheme.colors.icon.primary1, - ) \ No newline at end of file From b3fe4cbe05b008a4b509df9ec3bb1439a56ba221 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 17:19:17 +0400 Subject: [PATCH 126/223] Updated on 2026-08-14 --- .../features/details/ui/DetailsScreen.kt | 32 +++++--- .../disclaimer/impl/ui/DisclaimerScreen.kt | 12 +-- .../presentation/QrScanningContent.kt | 76 ++++++++----------- .../walletsettings/ui/WalletSettingsScreen.kt | 36 +++++---- 4 files changed, 81 insertions(+), 75 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index c1db3e35bd..9132fff08b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -9,18 +9,21 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.appbar.models.TopAppBarMedium +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockItem import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -30,7 +33,6 @@ import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.impl.R -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun DetailsScreen( state: DetailsUM, @@ -38,12 +40,11 @@ internal fun DetailsScreen( modifier: Modifier = Modifier, ) { val backgroundColor = TangemTheme.colors.background.secondary - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() BackHandler(onBack = state.popBack) Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = modifier, containerColor = backgroundColor, snackbarHost = { TangemSnackbarHost( @@ -52,10 +53,9 @@ internal fun DetailsScreen( ) }, topBar = { - TopAppBarMedium( - title = resourceReference(R.string.details_title), - scrollBehavior = scrollBehavior, - onBackClick = state.popBack, + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.popBack), ) }, ) { paddingValues -> @@ -77,10 +77,18 @@ private fun Content( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), contentPadding = PaddingValues( - top = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16, ), ) { + item { + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + text = stringResource(R.string.details_title), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + } items( items = state.items, key = DetailsItemUM::id, diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 7567197f2d..fddb1a255b 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -25,8 +25,8 @@ import com.google.accompanist.permissions.rememberPermissionState import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons -import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.common.TangemButtonColors import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemColorPalette @@ -57,14 +57,14 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { .statusBarsPadding(), ) { Column(modifier = Modifier.padding(bottom = bottomPadding)) { - AppBarWithAdditionalButtons( - text = resourceReference(R.string.disclaimer_title), - startButton = AdditionalButton( + TangemTopAppBar( + title = resourceReference(R.string.disclaimer_title), + startButton = TopAppBarButtonUM( iconRes = R.drawable.ic_back_24, onIconClicked = state.popBack, ).takeIf { state.isTosAccepted }, textColor = textColor, - iconColor = iconColor, + iconTint = iconColor, ) DisclaimerContent(state.url, state.isTosAccepted) } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt index 6dc9d3592b..019b7b6134 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningContent.kt @@ -6,11 +6,9 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.Canvas -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.Icon +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -20,12 +18,13 @@ import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Constraints -import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIconContent +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TopAppBarButton +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette @@ -59,46 +58,37 @@ internal fun QrScanningContent( Overlay( message = uiState.message, ) - AppBarWithBackButtonAndIconContent( - onBackClick = uiState.onBackClick, + + TangemTopAppBar( modifier = Modifier.statusBarsPadding(), - backgroundColor = Color.Transparent, - backIconTint = TangemColorPalette.White, - iconContent = { - Row { - AnimatedContent( - targetState = isFlash, - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Flash Change", - ) { - val flashIconRes = if (it) R.drawable.ic_flash_on_24 else R.drawable.ic_flash_off_24 - Icon( - painter = painterResource(flashIconRes), - contentDescription = null, - modifier = Modifier - .padding(end = TangemTheme.dimens.spacing20) - .size(size = TangemTheme.dimens.size24) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onClick = { isFlash = !isFlash }, - ), - tint = TangemColorPalette.White, - ) - } - Icon( - painter = painterResource(R.drawable.ic_gallery_24), - contentDescription = null, - modifier = Modifier - .size(size = TangemTheme.dimens.size24) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - onClick = uiState.onGalleryClick, - ), + title = null, + startButton = TopAppBarButtonUM.Back(uiState.onBackClick), + iconTint = TangemColorPalette.White, + containerColor = Color.Transparent, + endContent = { + AnimatedContent( + targetState = isFlash, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + label = "Flash Change", + ) { + TopAppBarButton( + button = TopAppBarButtonUM( + iconRes = if (it) R.drawable.ic_flash_on_24 else R.drawable.ic_flash_off_24, + onIconClicked = { + isFlash = !isFlash + }, + ), tint = TangemColorPalette.White, ) } + + TopAppBarButton( + button = TopAppBarButtonUM( + iconRes = R.drawable.ic_gallery_24, + onIconClicked = uiState.onGalleryClick, + ), + tint = TangemColorPalette.White, + ) }, ) if (uiState.bottomSheetConfig != null) { diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index 9dcfd53ddb..29223948a2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -6,22 +6,20 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.appbar.models.TopAppBarMedium +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.BlockItem import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -30,7 +28,6 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WalletSettingsScreen( state: WalletSettingsUM, @@ -38,12 +35,11 @@ internal fun WalletSettingsScreen( modifier: Modifier = Modifier, ) { val backgroundColor = TangemTheme.colors.background.secondary - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() BackHandler(onBack = state.popBack) Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = modifier, containerColor = backgroundColor, snackbarHost = { TangemSnackbarHost( @@ -52,10 +48,9 @@ internal fun WalletSettingsScreen( ) }, topBar = { - TopAppBarMedium( - title = resourceReference(R.string.wallet_settings_title), - scrollBehavior = scrollBehavior, - onBackClick = state.popBack, + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = TopAppBarButtonUM.Back(state.popBack), ) }, content = { paddingValues -> @@ -79,11 +74,21 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { bottom = TangemTheme.dimens.spacing16, ), ) { + item { + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + text = stringResource(id = R.string.wallet_settings_title), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + } items( items = state.items, key = WalletSettingsItemUM::id, ) { item -> - val itemModifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16) + val itemModifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth() when (item) { is WalletSettingsItemUM.WithItems -> ItemsBlock( @@ -117,7 +122,10 @@ private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier verticalArrangement = Arrangement.Top, ) { model.blocks.forEach { block -> - BlockItem(model = block) + BlockItem( + modifier = Modifier.fillMaxWidth(), + model = block, + ) } } From e1ee680173041c3217c77e360e16c6a086284916 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 17:19:43 +0400 Subject: [PATCH 127/223] Updated on 2026-08-14 --- .../bottomsheets/TangemBottomSheet.kt | 163 +++++++++++++++--- .../TangemBottomSheetDraggableHeader.kt | 5 +- .../bottomsheets/TangemBottomSheetTitle.kt | 68 ++++++++ .../tangem/core/ui/res/TangemThemePreview.kt | 33 +++- 4 files changed, 230 insertions(+), 39 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index ec10ddfa0a..8b48fdcf60 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -1,11 +1,18 @@ package com.tangem.core.ui.components.bottomsheets import androidx.compose.foundation.layout.* -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue.Expanded +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero @@ -13,45 +20,79 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch /** - * Tangem bottom sheet with custom draggable header and config - * - * @param config data model containing logic and ui models - * @param - * @param content custom bottom sheet content + * Bottom sheet with [content], [titleText] and optional [titleAction]. + * */ +@Composable +inline fun TangemBottomSheet( + config: TangemBottomSheetConfig, + titleText: TextReference, + titleAction: TopAppBarButtonUM? = null, + containerColor: Color = TangemTheme.colors.background.primary, + addBottomInsets: Boolean = true, + crossinline content: @Composable ColumnScope.(T) -> Unit, +) { + TangemBottomSheet( + config = config, + containerColor = containerColor, + addBottomInsets = addBottomInsets, + title = { TangemBottomSheetTitle(title = titleText, endButton = titleAction) }, + content = content, + ) +} + +/** + * Bottom sheet with [content] and optional [title]. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable inline fun TangemBottomSheet( config: TangemBottomSheetConfig, containerColor: Color = TangemTheme.colors.background.primary, addBottomInsets: Boolean = true, + crossinline title: @Composable BoxScope.(T) -> Unit = {}, crossinline content: @Composable ColumnScope.(T) -> Unit, +) { + val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current + + if (isAlwaysVisible) { + PreviewBottomSheet( + config = config, + containerColor = containerColor, + addBottomInsets = addBottomInsets, + title = title, + content = content, + ) + } else { + DefaultBottomSheet( + config = config, + containerColor = containerColor, + addBottomInsets = addBottomInsets, + title = title, + content = content, + ) + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun DefaultBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color, + addBottomInsets: Boolean, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), ) { var isVisible by remember { mutableStateOf(value = config.isShow) } - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + if (isVisible && config.content is T) { - ModalBottomSheet( - // FIXME temporary solution to fix height of the bottom sheet - modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), - onDismissRequest = config.onDismissRequest, + BasicBottomSheet( + config = config, sheetState = sheetState, containerColor = containerColor, - shape = TangemTheme.shapes.bottomSheetLarge, - windowInsets = WindowInsetsZero, - dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, - ) { - if (addBottomInsets) { - Column( - // FIXME temporary solution to fix height of the bottom sheet - modifier = Modifier.navigationBarsPadding(), - ) { - content(config.content) - } - } else { - content(config.content) - } - } + addBottomInsets = addBottomInsets, + title = title, + content = content, + ) } LaunchedEffect(key1 = config.isShow) { @@ -63,6 +104,74 @@ inline fun TangemBottomSheet( } } +@Composable +@OptIn(ExperimentalMaterial3Api::class) +inline fun PreviewBottomSheet( + config: TangemBottomSheetConfig, + containerColor: Color, + addBottomInsets: Boolean, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), +) { + BasicBottomSheet( + config = config, + sheetState = SheetState( + skipPartiallyExpanded = true, + initialValue = Expanded, + density = LocalDensity.current, + ), + containerColor = containerColor, + addBottomInsets = addBottomInsets, + title = title, + content = content, + ) +} + +@Suppress("LongParameterList") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +inline fun BasicBottomSheet( + config: TangemBottomSheetConfig, + sheetState: SheetState, + containerColor: Color, + addBottomInsets: Boolean, + crossinline title: @Composable (BoxScope.(T) -> Unit), + crossinline content: @Composable (ColumnScope.(T) -> Unit), +) { + val model = config.content as? T ?: return + + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + + ModalBottomSheet( + // FIXME temporary solution to fix height of the bottom sheet + modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), + onDismissRequest = config.onDismissRequest, + sheetState = sheetState, + containerColor = containerColor, + shape = TangemTheme.shapes.bottomSheetLarge, + windowInsets = WindowInsetsZero, + dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, + ) { + Column( + modifier = Modifier.let { + if (addBottomInsets) { + it.navigationBarsPadding() + } else { + it + } + }, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + ) { + title(model) + } + + content(model) + } + } +} + @OptIn(ExperimentalMaterial3Api::class) suspend fun SheetState.collapse(onCollapsed: () -> Unit) { coroutineScope { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt index 0d2cbe9d4e..37bff1013a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.Surface +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -14,8 +14,7 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TangemBottomSheetDraggableHeader(color: Color = TangemTheme.colors.background.primary) { Surface( - modifier = Modifier - .height(TangemTheme.dimens.size20), + modifier = Modifier.height(TangemTheme.dimens.size20), color = color, ) { Box( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt new file mode 100644 index 0000000000..65e7c87924 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetTitle.kt @@ -0,0 +1,68 @@ +package com.tangem.core.ui.components.bottomsheets + +import android.content.res.Configuration +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 com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun TangemBottomSheetTitle( + title: TextReference?, + modifier: Modifier = Modifier, + endButton: TopAppBarButtonUM? = null, + containerColor: Color = Color.Transparent, +) { + TangemBottomSheetTitle( + modifier = modifier, + title = title?.resolveReference(), + endButton = endButton, + containerColor = containerColor, + ) +} + +@Composable +fun TangemBottomSheetTitle( + title: String?, + modifier: Modifier = Modifier, + endButton: TopAppBarButtonUM? = null, + containerColor: Color = Color.Transparent, +) { + TangemTopAppBar( + modifier = modifier, + title = title, + endButton = endButton, + textColor = TangemTheme.colors.text.primary1, + iconTint = TangemTheme.colors.icon.informative, + titleAlignment = Alignment.CenterHorizontally, + containerColor = containerColor, + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_TangemBottomSheetTitle() { + TangemThemePreview { + TangemBottomSheetTitle( + title = "Title", + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_information_24, + onIconClicked = {}, + ), + containerColor = TangemTheme.colors.background.secondary, + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 2135bbdff8..c3ed096dce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -3,6 +3,9 @@ package com.tangem.core.ui.res import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.platform.LocalHapticFeedback import com.tangem.core.ui.haptic.MockHapticManager import com.tangem.core.ui.windowsize.rememberWindowSizePreview @@ -12,18 +15,30 @@ fun TangemThemePreview( isDark: Boolean? = null, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, + alwaysShowBottomSheets: Boolean = true, content: @Composable () -> Unit, ) { val isDarkTheme = isDark ?: isSystemInDarkTheme() - BoxWithConstraints { - TangemTheme( - isDark = isDarkTheme, - typography = typography, - dimens = dimens, - windowSize = rememberWindowSizePreview(maxWidth, maxHeight), - hapticManager = MockHapticManager(LocalHapticFeedback.current), - content = content, - ) + CompositionLocalProvider( + LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets, + ) { + BoxWithConstraints { + TangemTheme( + isDark = isDarkTheme, + typography = typography, + dimens = dimens, + windowSize = rememberWindowSizePreview(maxWidth, maxHeight), + hapticManager = MockHapticManager(LocalHapticFeedback.current), + content = content, + ) + } } +} + +/** + * This is used to make the bottom sheet always visible in the Preview and should be `true` only in the Preview. + * */ +val LocalBottomSheetAlwaysVisible: ProvidableCompositionLocal = compositionLocalOf { + false } \ No newline at end of file From 53a56d94cb7a165df81702298a47088b6a14de45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 17:20:18 +0400 Subject: [PATCH 128/223] Updated on 2026-08-14 --- .../GiveTxPermisssionBottomSheet.kt | 59 ++++++++------- .../QrScanningCameraDeniedBottomSheet.kt | 74 +++++++++++++------ .../ui/AgreementBottomSheetContent.kt | 4 +- .../ui/bottomsheet/StakingInfoBottomSheet.kt | 46 ++++++++++-- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 66 +++++++---------- .../swap/ui/ChooseProviderBottomSheet.kt | 38 +++++----- 6 files changed, 170 insertions(+), 117 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 9ced912bdf..f59c494895 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -27,8 +27,7 @@ import androidx.compose.ui.window.PopupProperties import com.tangem.common.ui.R import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons -import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -42,17 +41,33 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { + var isPermissionAlertShow by remember { mutableStateOf(false) } + TangemBottomSheet( config = config, containerColor = TangemTheme.colors.background.secondary, - ) { content: GiveTxPermissionBottomSheetConfig -> - GiveTxPermissionBottomSheetContent(content = content) - } + titleText = resourceReference(R.string.give_permission_title), + titleAction = TopAppBarButtonUM( + iconRes = R.drawable.ic_information_24, + onIconClicked = { isPermissionAlertShow = true }, + ), + content = { content: GiveTxPermissionBottomSheetConfig -> + GiveTxPermissionBottomSheetContent(content = content) + + if (isPermissionAlertShow) { + BasicDialog( + message = content.data.dialogText.resolveReference(), + title = stringResource(id = R.string.common_approve), + confirmButton = DialogButton { isPermissionAlertShow = false }, + onDismissDialog = {}, + ) + } + }, + ) } @Composable private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSheetConfig) { - var isPermissionAlertShow by remember { mutableStateOf(false) } val data = content.data Column( modifier = Modifier @@ -60,14 +75,6 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { - AppBarWithAdditionalButtons( - text = resourceReference(id = R.string.give_permission_title), - iconColor = TangemTheme.colors.text.tertiary, - endButton = AdditionalButton( - iconRes = R.drawable.ic_information_24, - onIconClicked = { isPermissionAlertShow = true }, - ), - ) Text( text = content.data.subtitle.resolveReference(), color = TangemTheme.colors.text.secondary, @@ -102,16 +109,6 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh ) SpacerH16() - - // region dialog - if (isPermissionAlertShow) { - BasicDialog( - message = content.data.dialogText.resolveReference(), - title = stringResource(id = R.string.common_approve), - confirmButton = DialogButton { isPermissionAlertShow = false }, - onDismissDialog = {}, - ) - } } } @@ -303,12 +300,18 @@ private fun DropdownSelector( } // region preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_AgreementBottomSheet() { +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_GiveTxPermissionBottomSheet() { TangemThemePreview { - GiveTxPermissionBottomSheetContent(content = previewData) + GiveTxPermissionBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = previewData, + ), + ) } } diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt index 31f79da70e..cb06aa540c 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/QrScanningCameraDeniedBottomSheet.kt @@ -1,25 +1,35 @@ package com.tangem.feature.qrscanning.presentation import android.content.Intent +import android.content.res.Configuration import android.net.Uri import android.provider.Settings +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.core.content.ContextCompat import com.tangem.core.ui.components.SimpleSettingsRow import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.qrscanning.impl.R @Composable fun CameraDeniedBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: CameraDeniedBottomSheetConfig -> + TangemBottomSheet( + config = config, + title = { + CameraDeniedBottomSheetHeader() + }, + ) { content: CameraDeniedBottomSheetConfig -> CameraDeniedBottomSheet(content = content) } } @@ -28,7 +38,6 @@ fun CameraDeniedBottomSheet(config: TangemBottomSheetConfig) { private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) { val context = LocalContext.current Column { - CameraDeniedBottomSheetHeader() SimpleSettingsRow( title = stringResource(id = R.string.qr_scanner_camera_denied_settings_button), icon = R.drawable.ic_settings_24, @@ -55,28 +64,45 @@ private fun CameraDeniedBottomSheet(content: CameraDeniedBottomSheetConfig) { } @Composable -private fun CameraDeniedBottomSheetHeader() { - Text( - text = stringResource(id = R.string.qr_scanner_camera_denied_title), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier +private fun CameraDeniedBottomSheetHeader(modifier: Modifier = Modifier) { + Column( + modifier = modifier .padding( - start = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing20, - top = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, ), - ) - Text( - text = stringResource(id = R.string.qr_scanner_camera_denied_text), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing20, - top = TangemTheme.dimens.spacing3, - bottom = TangemTheme.dimens.spacing16, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + text = stringResource(id = R.string.qr_scanner_camera_denied_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResource(id = R.string.qr_scanner_camera_denied_text), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_CameraDeniedBottomSheet() { + TangemThemePreview { + CameraDeniedBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = CameraDeniedBottomSheetConfig( + onCancelClick = {}, + onGalleryClick = {}, + ), ), - ) -} \ No newline at end of file + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index 8d8b5aadfa..204393dc6a 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewState -import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R @@ -32,7 +32,7 @@ internal fun AgreementBottomSheetContent(url: String) { .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) + TangemTopAppBar(title = stringResource(id = R.string.details_referral_title)) AgreementHtmlView(url = url) Spacer(modifier = Modifier.height(bottomBarHeight)) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt index 0b1104c35b..6e0b80bd18 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.ui.bottomsheet +import android.content.res.Configuration import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -7,23 +8,29 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.appbar.AppBar +import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetTitle import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig @Composable fun StakingInfoBottomSheet(config: TangemBottomSheetConfig) { val scrollState = rememberScrollState() - TangemBottomSheet( + + TangemBottomSheet( config = config, - ) { content: StakingInfoBottomSheetConfig -> + title = { content -> + TangemBottomSheetTitle(title = content.title) + }, + ) { content -> Column( modifier = Modifier.verticalScroll(scrollState), ) { - AppBar(text = content.title) Text( text = content.text.resolveReference(), color = TangemTheme.colors.text.secondary, @@ -35,4 +42,33 @@ fun StakingInfoBottomSheet(config: TangemBottomSheetConfig) { ) } } -} \ No newline at end of file +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_StakingInfoBottomSheet() { + TangemThemePreview { + StakingInfoBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = StakingInfoBottomSheetConfig( + title = stringReference("Title"), + text = stringReference( + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce varius neque vel ligula + tincidunt, nec faucibus nulla ultricies. Maecenas euismod arcu in nunc volutpat, + at bibendum eros lacinia. Proin hendrerit massa non velit congue, + in volutpat nisi consequat. Sed vitae justo nec orci tincidunt malesuada. + Nullam feugiat purus vel lectus efficitur, vel fringilla urna volutpat. + Donec sagittis enim in metus lacinia, vel tempor nunc bibendum. + """.trimIndent(), + ), + ), + ), + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 8b1ac6f243..f183c74cbd 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -1,12 +1,11 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.ClickableText -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.stringResource @@ -15,12 +14,12 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -35,6 +34,7 @@ fun ChooseFeeBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, containerColor = TangemTheme.colors.background.tertiary, + titleText = resourceReference(R.string.common_fee_selector_title), ) { content: ChooseFeeBottomSheetConfig -> ChooseFeeBottomSheetContent(content = content) } @@ -47,14 +47,6 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) { .background(TangemTheme.colors.background.tertiary) .padding(bottom = TangemTheme.dimens.spacing8), ) { - Text( - text = stringResource(R.string.common_fee_selector_title), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing10) - .align(Alignment.CenterHorizontally), - ) Column( modifier = Modifier .padding(TangemTheme.dimens.spacing16) @@ -145,9 +137,11 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { } } -@Preview +// region Preview @Composable -private fun ChooseFeeBottomSheetContent_Preview() { +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_ChooseFeeBottomSheet() { val feeItems = listOf( FeeItemState.Content( feeType = FeeType.NORMAL, @@ -168,33 +162,23 @@ private fun ChooseFeeBottomSheetContent_Preview() { onClick = {}, ), ).toImmutableList() - Column { - TangemThemePreview(isDark = true) { - ChooseFeeBottomSheetContent( - ChooseFeeBottomSheetConfig( - selectedFee = FeeType.NORMAL, - onSelectFeeType = {}, - feeItems = feeItems, - readMore = stringReference("Read more"), - readMoreUrl = "", - onReadMoreClick = {}, - ), - ) - } + val content = ChooseFeeBottomSheetConfig( + selectedFee = FeeType.NORMAL, + onSelectFeeType = {}, + feeItems = feeItems, + readMore = stringReference("Read more"), + readMoreUrl = "", + onReadMoreClick = {}, + ) - SpacerH24() - - TangemThemePreview(isDark = false) { - ChooseFeeBottomSheetContent( - ChooseFeeBottomSheetConfig( - selectedFee = FeeType.NORMAL, - onSelectFeeType = {}, - feeItems = feeItems, - readMore = stringReference("Read more"), - readMoreUrl = "", - onReadMoreClick = {}, - ), - ) - } + TangemThemePreview { + ChooseFeeBottomSheet( + config = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = content, + ), + ) } -} \ No newline at end of file +} +// endregion Preview \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 960b9d3373..3663417055 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -16,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -23,13 +25,14 @@ import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.PercentDifference import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.persistentListOf @Composable fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, containerColor = TangemTheme.colors.background.tertiary, + titleText = resourceReference(R.string.express_choose_providers_title), ) { content: ChooseProviderBottomSheetConfig -> ChooseProviderBottomSheetContent(content = content) } @@ -39,13 +42,6 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResource(R.string.express_choose_providers_title), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing10), - ) Text( text = stringResource(R.string.express_choose_providers_subtitle), style = TangemTheme.typography.caption2, @@ -104,10 +100,12 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC } } -@Preview +// region Preview @Composable -private fun ChooseProviderBottomSheet_Preview() { - val providers = listOf( +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_ChooseProviderBottomSheet() { + val providers = persistentListOf( ProviderState.Content( id = "1", name = "1inch", @@ -129,12 +127,18 @@ private fun ChooseProviderBottomSheet_Preview() { alertText = stringReference("Unavailable"), ), ) - TangemThemePreview(isDark = false) { - ChooseProviderBottomSheetContent( - ChooseProviderBottomSheetConfig( - selectedProviderId = "1", - providers = providers.toImmutableList(), + val content = ChooseProviderBottomSheetConfig( + selectedProviderId = "1", + providers = providers, + ) + TangemThemePreview { + ChooseProviderBottomSheet( + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = content, ), ) } -} \ No newline at end of file +} +// endregion Preview \ No newline at end of file From 6b8cb5604ea9873ddd70db5c655ae4cf31754c2d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 16:14:21 +0500 Subject: [PATCH 129/223] Updated on 2026-08-14 --- .../tokendetails/TokenDetailsPreviewData.kt | 49 ++++---- .../tokendetails/state/StakingBlockState.kt | 41 ------ .../tokendetails/state/StakingBlockUM.kt | 29 +++++ .../tokendetails/state/TokenDetailsState.kt | 2 +- ...TokenDetailsBalanceSelectStateConverter.kt | 6 +- .../TokenDetailsLoadedBalanceConverter.kt | 17 ++- .../TokenDetailsSkeletonStateConverter.kt | 5 +- .../factory/TokenStakingStateConverter.kt | 40 +++--- .../tokendetails/ui/TokenDetailsScreen.kt | 22 +--- .../components/staking/StakingBalanceBlock.kt | 19 ++- .../components/staking/TokenStakingBlock.kt | 119 ++++++++---------- 11 files changed, 146 insertions(+), 203 deletions(-) delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt 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 d685def542..182ed49b47 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 @@ -139,16 +139,24 @@ internal object TokenDetailsPreviewData { private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT") - private val stakingLoading = StakingBlocksState( - stakingAvailable = StakingAvailable.Loading(iconState), - stakingBalance = StakingBalance.Content( - cryptoValue = stringReference("5 SOL"), - fiatValue = stringReference("456.34 $"), - rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), - cryptoAmount = BigDecimal.ZERO, - fiatAmount = BigDecimal.ZERO, - onStakeClicked = {}, - ), + val stakingLoadingBlock = StakingBlockUM.Loading(iconState) + val stakingErrorBlock = StakingBlockUM.Error(iconState) + + val stakingAvailableBlock = StakingBlockUM.StakeAvailable( + interestRate = "7.38", + periodInDays = 4, + tokenSymbol = "XLM", + iconState = iconState, + onStakeClicked = {}, + ) + + val stakedBlock = StakingBlockUM.Staked( + cryptoValue = stringReference("5 SOL"), + fiatValue = stringReference("456.34 $"), + rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + cryptoAmount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + onStakeClicked = {}, ) private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( @@ -285,7 +293,7 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, - stakingBlocksState = stakingLoading, + stakingBlocksState = stakingLoadingBlock, notifications = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -317,23 +325,7 @@ internal object TokenDetailsPreviewData { type = PriceChangeType.UP, ), ), - stakingBlocksState = StakingBlocksState( - stakingAvailable = StakingAvailable.Content( - interestRate = "7.38", - periodInDays = 4, - tokenSymbol = "XLM", - iconState = iconState, - onStakeClicked = {}, - ), - stakingBalance = StakingBalance.Content( - cryptoValue = stringReference("5 SOL"), - fiatValue = stringReference("456.34 $"), - rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), - cryptoAmount = BigDecimal.ZERO, - fiatAmount = BigDecimal.ZERO, - onStakeClicked = {}, - ), - ), + stakingBlocksState = stakingAvailableBlock, notifications = persistentListOf(), txHistoryState = TxHistoryState.NotSupported( onExploreClick = {}, @@ -356,5 +348,6 @@ internal object TokenDetailsPreviewData { value = PagingData.from(txHistoryItems), ), ), + stakingBlocksState = stakedBlock, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt deleted file mode 100644 index d1a635dd87..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import java.math.BigDecimal - -internal data class StakingBlocksState( - val stakingAvailable: StakingAvailable, - val stakingBalance: StakingBalance, -) - -@Immutable -internal sealed interface StakingAvailable { - val iconState: IconState - - data class Error(override val iconState: IconState) : StakingAvailable - - data class Loading(override val iconState: IconState) : StakingAvailable - - data class Content( - override val iconState: IconState, - val interestRate: String, - val periodInDays: Int, - val tokenSymbol: String, - val onStakeClicked: () -> Unit, - ) : StakingAvailable -} - -@Immutable -sealed class StakingBalance { - data object Empty : StakingBalance() - - data class Content( - val cryptoValue: TextReference, - val fiatValue: TextReference, - val rewardValue: TextReference, - val cryptoAmount: BigDecimal?, - val fiatAmount: BigDecimal?, - val onStakeClicked: () -> Unit, - ) : StakingBalance() -} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt new file mode 100644 index 0000000000..91d76db70d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.TextReference +import java.math.BigDecimal + +@Stable +internal sealed interface StakingBlockUM { + data class Error(val iconState: IconState) : StakingBlockUM + + data class Loading(val iconState: IconState) : StakingBlockUM + + data class Staked( + val cryptoValue: TextReference, + val fiatValue: TextReference, + val rewardValue: TextReference, + val cryptoAmount: BigDecimal?, + val fiatAmount: BigDecimal?, + val onStakeClicked: () -> Unit, + ) : StakingBlockUM + + data class StakeAvailable( + val iconState: IconState, + val interestRate: String, + val periodInDays: Int, + val tokenSymbol: String, + val onStakeClicked: () -> Unit, + ) : StakingBlockUM +} \ 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 b9594669da..f6b3d59719 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 @@ -17,7 +17,7 @@ internal data class TokenDetailsState( val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, - val stakingBlocksState: StakingBlocksState, + val stakingBlocksState: StakingBlockUM, val notifications: ImmutableList, val pendingTxs: PersistentList, val swapTxs: PersistentList, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index a42e0f24d6..7695aa744c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -17,11 +17,11 @@ internal class TokenDetailsBalanceSelectStateConverter( override fun convert(value: TokenBalanceSegmentedButtonConfig): TokenDetailsState { return with(currentStateProvider()) { - if (stakingBlocksState.stakingBalance !is StakingBalance.Content) return this + if (stakingBlocksState !is StakingBlockUM.Staked) return this val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this - val stakingCryptoAmount = stakingBlocksState.stakingBalance.cryptoAmount - val stakingFiatAmount = stakingBlocksState.stakingBalance.fiatAmount + val stakingCryptoAmount = stakingBlocksState.cryptoAmount + val stakingFiatAmount = stakingBlocksState.fiatAmount copy( tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 5a1524feaa..5f47f33ef5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -14,7 +14,10 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.tokendetails.presentation.tokendetails.state.* +import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM +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.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance @@ -131,17 +134,17 @@ internal class TokenDetailsLoadedBalanceConverter( } } - private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlocksState { + private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM { val yieldBalance = status.value.yieldBalance as? YieldBalance.Data val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() val stakingRewardAmount = yieldBalance?.getRewardStakingBalance() val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } - val stakingBalance = if (stakingCryptoAmount.isNullOrZero()) { - StakingBalance.Empty + return if (stakingCryptoAmount.isNullOrZero()) { + StakingBlockUM.Loading(state.tokenInfoBlockState.iconState) } else { - StakingBalance.Content( + StakingBlockUM.Staked( cryptoAmount = stakingCryptoAmount, fiatAmount = stakingFiatAmount, cryptoValue = stringReference( @@ -167,10 +170,6 @@ internal class TokenDetailsLoadedBalanceConverter( onStakeClicked = clickIntents::onStakeBannerClick, ) } - return state.stakingBlocksState.copy( - stakingAvailable = state.stakingBlocksState.stakingAvailable, - stakingBalance = stakingBalance, - ) } private fun getMarketPriceState( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 3268e37904..a96bde767a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -54,10 +54,7 @@ internal class TokenDetailsSkeletonStateConverter( selectedBalanceType = BalanceType.ALL, ), marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), - stakingBlocksState = StakingBlocksState( - stakingAvailable = StakingAvailable.Loading(iconState), - stakingBalance = StakingBalance.Empty, - ), + stakingBlocksState = StakingBlockUM.Loading(iconState), notifications = persistentListOf(), pendingTxs = persistentListOf(), swapTxs = persistentListOf(), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 29fe830039..d7f0e655f9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -3,9 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlocksState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider @@ -14,31 +12,27 @@ import com.tangem.utils.converter.Converter internal class TokenStakingStateConverter( private val clickIntents: TokenDetailsClickIntents, private val currentStateProvider: Provider, -) : Converter, StakingBlocksState> { +) : Converter, StakingBlockUM> { - override fun convert(value: Either): StakingBlocksState { - value.fold( + override fun convert(value: Either): StakingBlockUM { + val state = currentStateProvider() + if (state.stakingBlocksState is StakingBlockUM.Staked) return state.stakingBlocksState + + val iconState = state.tokenInfoBlockState.iconState + return value.fold( ifLeft = { - return StakingBlocksState( - stakingAvailable = StakingAvailable.Error( - iconState = currentStateProvider().tokenInfoBlockState.iconState, - ), - stakingBalance = StakingBalance.Empty, - ) + StakingBlockUM.Error(iconState = iconState) }, ifRight = { - return StakingBlocksState( - stakingAvailable = StakingAvailable.Content( - interestRate = BigDecimalFormatter.formatPercent( - percent = it.interestRate, - useAbsoluteValue = true, - ), - periodInDays = it.periodInDays, - tokenSymbol = it.tokenSymbol, - iconState = currentStateProvider().tokenInfoBlockState.iconState, - onStakeClicked = clickIntents::onStakeBannerClick, + StakingBlockUM.StakeAvailable( + interestRate = BigDecimalFormatter.formatPercent( + percent = it.interestRate, + useAbsoluteValue = true, ), - stakingBalance = StakingBalance.Empty, + periodInDays = it.periodInDays, + tokenSymbol = it.tokenSymbol, + iconState = iconState, + onStakeClicked = clickIntents::onStakeBannerClick, ) }, ) 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 95bd0ed963..9ea4aa95ee 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 @@ -41,8 +41,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock @@ -52,7 +51,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.StakingBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock // TODO: Split to blocks [REDACTED_JIRA] @@ -141,25 +139,13 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { } if (state.isStakingBlockShown) { - if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) { - item( - key = StakingBalance::class.java, - contentType = StakingBalance::class.java, - content = { - StakingBalanceBlock( - state = state.stakingBlocksState.stakingBalance, - modifier = itemModifier, - ) - }, - ) - } item( - key = StakingAvailable::class.java, - contentType = StakingAvailable::class.java, + key = StakingBlockUM::class.java, + contentType = StakingBlockUM::class.java, content = { TokenStakingBlock( modifier = itemModifier, - state = state.stakingBlocksState.stakingAvailable, + state = state.stakingBlocksState, ) }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 34986f925b..3cbd9419a3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -21,13 +21,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakedBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Strings @Composable -fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modifier) { +internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifier = Modifier) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -87,21 +87,18 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun StakingBalanceBlock_Preview( - @PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBalance, + @PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBlockUM.Staked, ) { TangemThemePreview { StakingBalanceBlock( - state = data as StakingBalance.Content, + state = data, modifier = Modifier.padding(TangemTheme.dimens.spacing16), ) } } -private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - TokenDetailsPreviewData.tokenDetailsState_1.stakingBlocksState.stakingBalance, - TokenDetailsPreviewData.tokenDetailsState_2.stakingBlocksState.stakingBalance, - ) +private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(stakedBlock) } // endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index e9f66e62ab..afa3fc743d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -23,8 +22,11 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA import com.tangem.core.ui.utils.GrayscaleColorFilter import com.tangem.core.ui.utils.NORMAL_ALPHA +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingErrorBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingLoadingBlock import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon import com.tangem.features.tokendetails.impl.R @@ -35,7 +37,32 @@ import com.tangem.features.tokendetails.impl.R * @param modifier modifier */ @Composable -internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Modifier) { +internal fun TokenStakingBlock(state: StakingBlockUM, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = state, + contentAlignment = Alignment.CenterStart, + label = "Staking block animation", + ) { + when (it) { + is StakingBlockUM.Error -> Row {} // TODO staking error + is StakingBlockUM.Loading -> StakingLoading( + iconState = it.iconState, + modifier = modifier, + ) + is StakingBlockUM.Staked -> StakingBalanceBlock( + state = it, + modifier = modifier, + ) + is StakingBlockUM.StakeAvailable -> StakingAvailableContent( + state = it, + modifier = modifier, + ) + } + } +} + +@Composable +private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifier: Modifier = Modifier) { Column( modifier = modifier .background( @@ -43,46 +70,11 @@ internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Mod shape = TangemTheme.shapes.roundedCornersXMedium, ) .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size72) .padding(all = TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - horizontalAlignment = Alignment.Start, ) { - Content(state = state) - } -} - -@Composable -private fun Content(state: StakingAvailable, modifier: Modifier = Modifier) { - AnimatedContent( - modifier = modifier.heightIn(min = TangemTheme.dimens.size60), - targetState = state, - contentAlignment = Alignment.CenterStart, - label = "Update the content", - ) { stakingBlockState -> - when (stakingBlockState) { - is StakingAvailable.Content -> { - StakingContent( - stakingBlockState = stakingBlockState, - iconState = stakingBlockState.iconState, - ) - } - is StakingAvailable.Loading -> { - StakingLoading( - iconState = stakingBlockState.iconState, - ) - } - is StakingAvailable.Error -> Row {} // TODO staking - } - } -} - -@Composable -private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconState: IconState) { - Column { Row { - val (alpha, colorFilter) = remember(iconState.isGrayscale) { - if (iconState.isGrayscale) { + val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { + if (state.iconState.isGrayscale) { GRAY_SCALE_ALPHA to GrayscaleColorFilter } else { NORMAL_ALPHA to null @@ -93,7 +85,7 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat .size(TangemTheme.dimens.size20) .clip(TangemTheme.shapes.roundedCorners8) .align(Alignment.CenterVertically), - icon = iconState, + icon = state.iconState, alpha = alpha, colorFilter = colorFilter, ) @@ -102,7 +94,7 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat Text( text = stringResource( R.string.token_details_staking_block_title, - stakingBlockState.interestRate, + state.interestRate, ), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, @@ -113,8 +105,8 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat Text( text = stringResource( R.string.token_details_staking_block_subtitle, - stakingBlockState.tokenSymbol, - stakingBlockState.periodInDays, + state.tokenSymbol, + state.periodInDays, ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, @@ -126,14 +118,24 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat SecondaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.common_stake), - onClick = stakingBlockState.onStakeClicked, + onClick = state.onStakeClicked, ) } } @Composable -private fun StakingLoading(iconState: IconState) { - Column { +private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size72) + .padding(all = TangemTheme.dimens.spacing12), + + ) { Row { val (alpha, colorFilter) = remember(iconState.isGrayscale) { if (iconState.isGrayscale) { @@ -181,31 +183,18 @@ private fun StakingLoading(iconState: IconState) { @Composable private fun Preview_TokenStakingBlock( @PreviewParameter(StakingBlockStateProvider::class) - state: StakingAvailable, + state: StakingBlockUM, ) { TangemThemePreview { TokenStakingBlock(state = state) } } -private class StakingBlockStateProvider : CollectionPreviewParameterProvider( +private class StakingBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - StakingAvailable.Content( - iconState = iconState, - interestRate = "10", - periodInDays = 4, - tokenSymbol = "SOL", - onStakeClicked = {}, - ), - StakingAvailable.Loading(iconState = iconState), - StakingAvailable.Error(iconState = iconState), + stakingLoadingBlock, + stakingAvailableBlock, + stakingErrorBlock, ), ) - -private val iconState = IconState.TokenIcon( - url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", - fallbackTint = Color.Cyan, - fallbackBackground = Color.Blue, - isGrayscale = false, -) // endregion Preview \ No newline at end of file From 1c3b25169d8bb84fe42674238b9681b09fd9ffd7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 12:01:27 +0200 Subject: [PATCH 130/223] Updated on 2026-08-14 --- .../com/tangem/feature/swap/analytics/SwapEvents.kt | 4 ++++ .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) 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 39df6ae13c..55450cbdec 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 @@ -60,6 +60,8 @@ sealed class SwapEvents( data class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast + val sendBlockchain: String, + val receiveBlockchain: String, val sendToken: String, val receiveToken: String, ) : SwapEvents( @@ -69,6 +71,8 @@ sealed class SwapEvents( "Commission" to if (commission == FeeType.NORMAL) "Market" else "Fast", "Send Token" to sendToken, "Receive Token" to receiveToken, + "Send Blockchain" to sendBlockchain, + "Receive Blockchain" to receiveBlockchain, ), ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 0c7b32d114..51497e7f98 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -576,15 +576,17 @@ internal class SwapViewModel @Inject constructor( private fun sendSuccessEvent() { val provider = dataState.selectedProvider ?: return val fee = dataState.selectedFee?.feeType ?: return - val sendToken = dataState.fromCryptoCurrency?.currency?.symbol ?: return - val toToken = dataState.toCryptoCurrency?.currency?.symbol ?: return + val fromCurrency = dataState.fromCryptoCurrency?.currency ?: return + val toCurrency = dataState.toCryptoCurrency?.currency ?: return analyticsEventHandler.send( SwapEvents.SwapInProgressScreen( provider = provider, commission = fee, - sendToken = sendToken, - receiveToken = toToken, + sendBlockchain = fromCurrency.network.name, + receiveBlockchain = toCurrency.network.name, + sendToken = fromCurrency.symbol, + receiveToken = toCurrency.symbol, ), ) } From 0d255aa2cf431cfcc0d4c8f7bfb15efc6f9a91ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 18:09:00 +0400 Subject: [PATCH 131/223] Updated on 2026-08-14 --- .../details/model/UserWalletListModel.kt | 23 ++++----- .../details/utils/UserWalletsFetcher.kt | 49 +++++++++---------- .../model/WalletSettingsModel.kt | 4 +- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 202758486c..12f18cc1f4 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -13,27 +13,22 @@ import com.tangem.features.details.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.update import javax.inject.Inject @ComponentScoped internal class UserWalletListModel @Inject constructor( - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + userWalletsFetcher: UserWalletsFetcher, + shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val userWalletSaver: UserWalletSaver, - private val userWalletsFetcher: UserWalletsFetcher, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) - private val userWallets: SharedFlow> = userWalletsFetcher - .userWallets - .share() - - private val shouldSaveUserWallets: SharedFlow = shouldSaveUserWalletsUseCase() - .distinctUntilChanged() - .share() - val state: MutableStateFlow = MutableStateFlow( value = UserWalletListUM( userWallets = persistentListOf(), @@ -45,14 +40,14 @@ internal class UserWalletListModel @Inject constructor( init { combine( - userWallets, - shouldSaveUserWallets, + userWalletsFetcher.userWallets, + shouldSaveUserWalletsUseCase(), isWalletSavingInProgress, transform = ::updateState, ).launchIn(modelScope) } - private suspend fun updateState( + private fun updateState( userWallets: ImmutableList, shouldSaveUserWallets: Boolean, isWalletSavingInProgress: Boolean, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index e606030df5..5377be02ea 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -23,44 +23,43 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.transformLatest import javax.inject.Inject @ComponentScoped internal class UserWalletsFetcher @Inject constructor( - private val getWalletsUseCase: GetWalletsUseCase, + getWalletsUseCase: GetWalletsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val router: Router, private val messageSender: UiMessageSender, ) { - val userWallets: Flow> = getWalletsUseCase() - .distinctUntilChanged() - .transform { wallets -> - if (wallets.isEmpty()) { - error("Wallets must not be empty") - } else { - emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) - } + @OptIn(ExperimentalCoroutinesApi::class) + val userWallets: Flow> = getWalletsUseCase().transformLatest { wallets -> + emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) - combine( - getSelectedAppCurrencyUseCase(), - getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)), - ) { maybeAppCurrency, maybeBalances -> - val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse( - ifLoading = { return@combine }, - ifError = { - val message = resourceReference(R.string.common_unknown_error) - messageSender.send(SnackbarMessage(message)) + combine( + getSelectedAppCurrencyUseCase(), + getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)), + ) { maybeAppCurrency, maybeBalances -> + val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse( + ifLoading = { return@combine }, + ifError = { + val message = resourceReference(R.string.common_unknown_error) + messageSender.send(SnackbarMessage(message)) - return@combine - }, - ) + return@combine + }, + ) - emit(models) - }.collect() - } + emit(models) + }.collect() + } private fun createUiModels( wallets: List, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 7926ce7f87..188a3ba043 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -31,12 +31,12 @@ import javax.inject.Inject @Suppress("LongParameterList") @ComponentScoped internal class WalletSettingsModel @Inject constructor( + getWalletUseCase: GetUserWalletUseCase, + paramsContainer: ParamsContainer, private val router: Router, private val messageSender: UiMessageSender, - private val getWalletUseCase: GetUserWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val itemsBuilder: ItemsBuilder, - private val paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { From fa3f781881884a50a4a3395d17f95510715a64cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Jul 2024 13:58:53 +0500 Subject: [PATCH 132/223] Updated on 2026-08-14 --- .../tangem/tap/di/domain/CardDomainModule.kt | 6 ++++ .../card/NetworkHasDerivationUseCase.kt | 16 +++++++++ .../TokenDetailsSkeletonStateConverter.kt | 35 +++++++++++++++---- .../state/factory/TokenDetailsStateFactory.kt | 23 ++++++++++-- .../viewmodels/TokenDetailsViewModel.kt | 10 +++++- 5 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt 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 3d1b711ccf..0f3fab437c 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 @@ -84,4 +84,10 @@ internal object CardDomainModule { fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase { return DefaultResetCardUseCase(tangemSdkManager) } + + @Provides + @Singleton + fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase { + return NetworkHasDerivationUseCase() + } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt new file mode 100644 index 0000000000..7bc68e0d66 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.card + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.util.hasDerivation +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.Network + +class NetworkHasDerivationUseCase { + + operator fun invoke(scanResponse: ScanResponse, network: Network): Either { + val blockchain = Blockchain.fromId(network.id.value) + val derivationPath = network.derivationPath.value + return Either.catch { derivationPath != null && scanResponse.hasDerivation(blockchain, derivationPath) } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index a96bde767a..8339f1f69b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import arrow.core.getOrElse import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent @@ -7,8 +8,11 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig @@ -25,6 +29,9 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, + private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val userWalletId: UserWalletId, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -78,13 +85,7 @@ internal class TokenDetailsSkeletonStateConverter( private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( items = buildList { - if (featureToggles.isGenerateXPubEnabled() && isBitcoin(cryptoCurrency.network.id.value)) { - TokenDetailsAppBarMenuConfig.MenuItem( - title = resourceReference(R.string.token_details_generate_xpub), - textColorProvider = { TangemTheme.colors.text.primary1 }, - onClick = clickIntents::onGenerateExtendedKey, - ).let(::add) - } + addGenerateXPubMenuItem(cryptoCurrency) TokenDetailsAppBarMenuConfig.MenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, @@ -93,6 +94,26 @@ internal class TokenDetailsSkeletonStateConverter( }.toImmutableList(), ) + private fun MutableList.addGenerateXPubMenuItem( + cryptoCurrency: CryptoCurrency, + ) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return + val isGenerateXPubEnabled = featureToggles.isGenerateXPubEnabled() + val isBitcoin = isBitcoin(cryptoCurrency.network.id.value) + val hasDerivations = + networkHasDerivationUseCase(userWallet.scanResponse, cryptoCurrency.network).getOrElse { false } + + if (isGenerateXPubEnabled && isBitcoin && hasDerivations) { + add( + TokenDetailsAppBarMenuConfig.MenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColorProvider = { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onGenerateExtendedKey, + ), + ) + } + } + private fun createButtons(): ImmutableList { return persistentListOf( TokenDetailsActionButton.Buy(dimContent = false, onClick = {}), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 952b0d2dc6..789a0a6f82 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -15,6 +15,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo @@ -24,6 +25,11 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter @@ -46,6 +52,9 @@ internal class TokenDetailsStateFactory( private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, + private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val userWalletId: UserWalletId, stakingFeatureToggles: StakingFeatureToggles, symbol: String, decimals: Int, @@ -55,6 +64,9 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, featureToggles = featureToggles, + networkHasDerivationUseCase = networkHasDerivationUseCase, + getUserWalletUseCase = getUserWalletUseCase, + userWalletId = userWalletId, ) } @@ -348,12 +360,16 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithUpdatedMenu(cardTypesResolver: CardTypesResolver, isBitcoin: Boolean): TokenDetailsState { + fun getStateWithUpdatedMenu( + cardTypesResolver: CardTypesResolver, + hasDerivations: Boolean, + isBitcoin: Boolean, + ): TokenDetailsState { return with(currentStateProvider()) { copy( topAppBarConfig = topAppBarConfig.copy( tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig - ?.updateMenu(cardTypesResolver, isBitcoin), + ?.updateMenu(cardTypesResolver, hasDerivations, isBitcoin), ), ) } @@ -367,13 +383,14 @@ internal class TokenDetailsStateFactory( private fun TokenDetailsAppBarMenuConfig.updateMenu( cardTypesResolver: CardTypesResolver, + hasDerivations: Boolean, isBitcoin: Boolean, ): TokenDetailsAppBarMenuConfig? { if (cardTypesResolver.isSingleWalletWithToken()) return null val showGenerateExtendedKey = featureToggles.isGenerateXPubEnabled() && isBitcoin return copy( items = buildList { - if (showGenerateExtendedKey) { + if (showGenerateExtendedKey && hasDerivations) { TokenDetailsAppBarMenuConfig.MenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColorProvider = { TangemTheme.colors.text.primary1 }, 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 bccaa00b68..245dd160ad 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 @@ -25,6 +25,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase +import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder @@ -108,6 +109,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val stakingFeatureToggles: StakingFeatureToggles, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, private val getYieldUseCase: GetYieldUseCase, + private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -118,8 +120,8 @@ internal class TokenDetailsViewModel @Inject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, private val hapticManager: HapticManager, private val clipboardManager: ClipboardManager, + private val getUserWalletUseCase: GetUserWalletUseCase, tokenDetailsFeatureToggles: TokenDetailsFeatureToggles, - getUserWalletUseCase: GetUserWalletUseCase, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -156,6 +158,9 @@ internal class TokenDetailsViewModel @Inject constructor( decimals = cryptoCurrency.decimals, featureToggles = tokenDetailsFeatureToggles, stakingFeatureToggles = stakingFeatureToggles, + userWalletId = userWalletId, + networkHasDerivationUseCase = networkHasDerivationUseCase, + getUserWalletUseCase = getUserWalletUseCase, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -395,8 +400,11 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateTopBarMenu() { viewModelScope.launch(dispatchers.main) { + val hasDerivations = + networkHasDerivationUseCase(userWallet.scanResponse, cryptoCurrency.network).getOrElse { false } internalUiState.value = stateFactory.getStateWithUpdatedMenu( cardTypesResolver = userWallet.scanResponse.cardTypesResolver, + hasDerivations = hasDerivations, isBitcoin = isBitcoin(cryptoCurrency.network.id.value), ) } From 75f8a45cd40e1ce7839f393bbf681123686365a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 17:27:27 +0300 Subject: [PATCH 133/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 2 +- .../tap/di/domain/StakingDomainModule.kt | 85 +++++++++-- .../tangem/tap/features/main/MainViewModel.kt | 2 +- .../model/error/StakeKitErrorResponse.kt | 143 ++++++++++++++++++ .../staking/DefaultStakingErrorResolver.kt | 19 +++ .../error/StakeKitErrorConverter.kt | 108 +++++++++++++ .../data/staking/di/StakingDataModule.kt | 15 ++ .../domain/staking/model/StakingError.kt | 92 +++++++++++ .../staking/FetchStakingTokensUseCase.kt | 8 +- .../FetchStakingYieldBalanceUseCase.kt | 24 ++- .../staking/GetStakingAvailabilityUseCase.kt | 14 +- .../staking/GetStakingEntryInfoUseCase.kt | 13 +- .../staking/GetStakingYieldBalanceUseCase.kt | 9 +- .../tangem/domain/staking/GetYieldUseCase.kt | 14 +- .../InitializeStakingProcessUseCase.kt | 11 +- .../staking/SaveUnsubmittedHashUseCase.kt | 11 +- .../staking/SendUnsubmittedHashesUseCase.kt | 15 +- .../domain/staking/SubmitHashUseCase.kt | 24 ++- .../staking/error/StakingTokensError.kt | 6 - .../repositories/StakingErrorResolver.kt | 8 + .../state/factory/TokenDetailsStateFactory.kt | 3 +- .../factory/TokenStakingStateConverter.kt | 5 +- .../viewmodels/TokenDetailsViewModel.kt | 3 +- 23 files changed, 569 insertions(+), 65 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt delete mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2708e92fb4..2c3936c358 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -594,7 +594,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycleScope.launch { sendUnsubmittedHashesUseCase.invoke() .onRight { Timber.d("Submitting hashes succeeded") } - .onLeft { Timber.e(it) } + .onLeft { Timber.e(it.toString()) } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 49ee51f726..f080165fe4 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -14,69 +15,121 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { + fun provideGetStakingAvailabilityUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetStakingAvailabilityUseCase { return GetStakingAvailabilityUseCase( stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, ) } @Provides @Singleton - fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase { + fun provideGetStakingEntryInfoUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetStakingEntryInfoUseCase { return GetStakingEntryInfoUseCase( stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, ) } @Provides @Singleton - fun provideGetYieldUseCase(stakingRepository: StakingRepository): GetYieldUseCase { + fun provideGetYieldUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetYieldUseCase { return GetYieldUseCase( stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, ) } @Provides @Singleton - fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase { + fun provideGetStakingTokensUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): FetchStakingTokensUseCase { return FetchStakingTokensUseCase( stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, ) } @Provides @Singleton - fun provideFetchStakingYieldBalanceUseCase(stakingRepository: StakingRepository): FetchStakingYieldBalanceUseCase { - return FetchStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + fun provideFetchStakingYieldBalanceUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): FetchStakingYieldBalanceUseCase { + return FetchStakingYieldBalanceUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) } @Provides @Singleton - fun provideGetStakingYieldBalanceUseCase(stakingRepository: StakingRepository): GetStakingYieldBalanceUseCase { - return GetStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + fun provideGetStakingYieldBalanceUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetStakingYieldBalanceUseCase { + return GetStakingYieldBalanceUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) } @Provides @Singleton - fun provideCreateEnterActionUseCase(stakingRepository: StakingRepository): InitializeStakingProcessUseCase { - return InitializeStakingProcessUseCase(stakingRepository) + fun provideCreateEnterActionUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): InitializeStakingProcessUseCase { + return InitializeStakingProcessUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) } @Provides @Singleton - fun provideSubmitHashUseCase(stakingRepository: StakingRepository): SubmitHashUseCase { - return SubmitHashUseCase(stakingRepository) + fun provideSubmitHashUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): SubmitHashUseCase { + return SubmitHashUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) } @Provides @Singleton - fun provideSaveUnsubmittedHashUseCase(stakingRepository: StakingRepository): SaveUnsubmittedHashUseCase { - return SaveUnsubmittedHashUseCase(stakingRepository) + fun provideSaveUnsubmittedHashUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): SaveUnsubmittedHashUseCase { + return SaveUnsubmittedHashUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) } @Provides @Singleton - fun provideSendUnsubmittedHashesUseCase(stakingRepository: StakingRepository): SendUnsubmittedHashesUseCase { - return SendUnsubmittedHashesUseCase(stakingRepository) + fun provideSendUnsubmittedHashesUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): SendUnsubmittedHashesUseCase { + return SendUnsubmittedHashesUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index b127b60b18..fd2b740b37 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -125,7 +125,7 @@ internal class MainViewModel @Inject constructor( private fun fetchStakingTokens() { viewModelScope.launch(dispatchers.main) { fetchStakingTokensUseCase() - .onLeft { Timber.e(it, "Unable to fetch the staking tokens list") } + .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } .onRight { Timber.d("Staking token list was fetched successfully") } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt new file mode 100644 index 0000000000..cf62a7d43b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt @@ -0,0 +1,143 @@ +package com.tangem.datasource.api.stakekit.models.response.model.error + +import com.squareup.moshi.Json + +class StakeKitErrorResponse( + @Json(name = "details") + val details: StakeKitErrorDetailsDTO? = null, + @Json(name = "message") + val message: StakeKitErrorMessageDTO? = null, + @Json(name = "level") + val level: String? = null, // unused +) + +data class StakeKitErrorDetailsDTO( + @Json(name = "arguments") + val arguments: String? = null, + @Json(name = "amount") + val amount: String? = null, + @Json(name = "yieldId") + val yieldId: String? = null, +) + +enum class StakeKitErrorMessageDTO { + @Json(name = "MissingArgumentsError") + MISSING_ARGUMENTS_ERROR, + + @Json(name = "MinimumAmountNotReached") + MINIMUM_AMOUNT_NOT_REACHED, + + @Json(name = "YieldUnderMaintenanceError") + YIELD_UNDER_MAINTENANCE_ERROR, + + @Json(name = "InsufficientFundsError") + INSUFFICIENT_FUNDS_ERROR, + + @Json(name = "StakedPositionNotFoundError") + STAKED_POSITION_NOT_FOUND_ERROR, + + @Json(name = "InvalidAmountSubmittedError") + INVALID_AMOUNT_SUBMITTED_ERROR, + + @Json(name = "BalanceUnavailableError") + BALANCE_UNAVAILABLE_ERROR, + + @Json(name = "GasPriceUnavailableError") + GAS_PRICE_UNAVAILABLE_ERROR, + + @Json(name = "NotImplementedError") + NOT_IMPLEMENTED_ERROR, + + @Json(name = "TokenNotFoundError") + TOKEN_NOT_FOUND_ERROR, + + @Json(name = "BroadcastTransactionError") + BROADCAST_TRANSACTION_ERROR, + + @Json(name = "MissingGasPriceStrategyError") + MISSING_GAS_PRICE_STRATEGY_ERROR, + + @Json(name = "SubstrateMalformedTransactionHashError") + SUBSTRATE_MALFORMED_TRANSACTION_HASH_ERROR, + + @Json(name = "TronMaximumAmountOfValidatorsExceededError") + TRON_MAXIMUM_AMOUNT_OF_VALIDATORS_EXCEEDED_ERROR, + + @Json(name = "SubstratePoolNotFoundError") + SUBSTRATE_POOL_NOT_FOUND_ERROR, + + @Json(name = "SubstrateBondedAmountTooLowError") + SUBSTRATE_BONDED_AMOUNT_TOO_LOW_ERROR, + + @Json(name = "TronMissingResourceTypeArgumentError") + TRON_MISSING_RESOURCE_TYPE_ARGUMENT_ERROR, + + @Json(name = "AaveV3PoolFrozenError") + AAVE_V3_POOL_FROZEN_ERROR, + + @Json(name = "AaveV3TokenPairNotFoundError") + AAVE_V3_TOKEN_PAIR_NOT_FOUND_ERROR, + + @Json(name = "YearnVaultAtMaxCapacityError") + YEARN_VAULT_AT_MAX_CAPACITY_ERROR, + + @Json(name = "StETHNoWithdrawalRequestsFoundError") + STETH_NO_WITHDRAWAL_REQUESTS_FOUND_ERROR, + + @Json(name = "MorphoLendingPoolPausedError") + MORPHO_LENDING_POOL_PAUSED_ERROR, + + @Json(name = "NonceUnavailableError") + NONCE_UNAVAILABLE_ERROR, + + @Json(name = "CosmosAcccountNotFoundError") + COSMOS_ACCOUNT_NOT_FOUND_ERROR, + + @Json(name = "AvalancheMissingAdditionalAddressesArgumentError") + AVALANCHE_MISSING_ADDITIONAL_ADDRESSES_ARGUMENT_ERROR, + + @Json(name = "AvalancheValidatorInfoNotFoundError") + AVALANCHE_VALIDATOR_INFO_NOT_FOUND_ERROR, + + @Json(name = "SolanaTransactionSignatureVerificationFailureError") + SOLANA_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE_ERROR, + + @Json(name = "SolanaUnableTocreateStakeAccountError") + SOLANA_UNABLE_TO_CREATE_STAKE_ACCOUNT_ERROR, + + @Json(name = "SolanaStakeAmountTooLowError") + SOLANA_STAKE_AMOUNT_TOO_LOW_ERROR, + + @Json(name = "SolanaUnstakeAmountTooLowError") + SOLANA_UNSTAKE_AMOUNT_TOO_LOW_ERROR, + + @Json(name = "SolanaStakeAccountsNotFoundError") + SOLANA_STAKE_ACCOUNTS_NOT_FOUND_ERROR, + + @Json(name = "SolanaEligibleStakeAccountsNotFoundError") + SOLANA_ELIGIBLE_STAKE_ACCOUNTS_NOT_FOUND_ERROR, + + @Json(name = "TezosNoBalanceDelegatedError") + TEZOS_NO_BALANCE_DELEGATED_ERROR, + + @Json(name = "TezosMissingPubkeyArgumentError") + TEZOS_MISSING_PUBKEY_ARGUMENT_ERROR, + + @Json(name = "TezosEstimateRevealGasLimitError") + TEZOS_ESTIMATE_REVEAL_GAS_LIMIT_ERROR, + + @Json(name = "TezosBalanceAlreadyDelegatedError") + TEZOS_BALANCE_ALREADY_DELEGATED_ERROR, + + @Json(name = "BinanceAccountNotFoundError") + BINANCE_ACCOUNT_NOT_FOUND_ERROR, + + @Json(name = "BinanceMissingAccountNumberOrSequenceError") + BINANCE_MISSING_ACCOUNT_NUMBER_OR_SEQUENCE_ERROR, + + @Json(name = "GRTStakingDisabledError") + GRT_STAKING_DISABLED_ERROR, + + @Json(name = "GRTStakingDisabledLedgerLiveError") + GRT_STAKING_DISABLED_LEDGER_LIVE_ERROR, +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt new file mode 100644 index 0000000000..8fed7f5841 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt @@ -0,0 +1,19 @@ +package com.tangem.data.staking + +import com.tangem.data.staking.converters.error.StakeKitErrorConverter +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver + +internal class DefaultStakingErrorResolver( + private val stakeKitErrorConverter: StakeKitErrorConverter, +) : StakingErrorResolver { + + override fun resolve(throwable: Throwable): StakingError { + return if (throwable is ApiResponseError.HttpException) { + stakeKitErrorConverter.convert(throwable.errorBody.orEmpty()) + } else { + StakingError.UnknownError + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt new file mode 100644 index 0000000000..30020c0708 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt @@ -0,0 +1,108 @@ +package com.tangem.data.staking.converters.error + +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO +import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse +import com.tangem.domain.staking.model.StakingError +import com.tangem.utils.converter.Converter + +internal class StakeKitErrorConverter( + private val jsonAdapter: JsonAdapter, +) : Converter { + + @Suppress("CyclomaticComplexMethod", "LongMethod") + override fun convert(value: String): StakingError { + return try { + val stakeKitErrorResponse = jsonAdapter.fromJson(value) ?: return StakingError.UnknownError + + when (stakeKitErrorResponse.message) { + StakeKitErrorMessageDTO.MINIMUM_AMOUNT_NOT_REACHED -> StakingError.MinimumAmountNotReachedError( + amount = stakeKitErrorResponse.details?.amount ?: "", + ) + StakeKitErrorMessageDTO.MISSING_ARGUMENTS_ERROR -> StakingError.MissingArgumentsError( + arguments = stakeKitErrorResponse.details?.arguments ?: "", + ) + StakeKitErrorMessageDTO.YIELD_UNDER_MAINTENANCE_ERROR -> StakingError.YieldUnderMaintenanceError( + yieldId = stakeKitErrorResponse.details?.yieldId ?: "", + ) + StakeKitErrorMessageDTO.INSUFFICIENT_FUNDS_ERROR -> + StakingError.InsufficientFundsError + StakeKitErrorMessageDTO.STAKED_POSITION_NOT_FOUND_ERROR -> + StakingError.StakedPositionNotFoundError + StakeKitErrorMessageDTO.INVALID_AMOUNT_SUBMITTED_ERROR -> + StakingError.InvalidAmountSubmittedError + StakeKitErrorMessageDTO.BALANCE_UNAVAILABLE_ERROR -> + StakingError.BalanceUnavailableError + StakeKitErrorMessageDTO.GAS_PRICE_UNAVAILABLE_ERROR -> + StakingError.GasPriceUnavailableError + StakeKitErrorMessageDTO.NOT_IMPLEMENTED_ERROR -> + StakingError.NotImplementedError + StakeKitErrorMessageDTO.TOKEN_NOT_FOUND_ERROR -> + StakingError.TokenNotFoundError + StakeKitErrorMessageDTO.BROADCAST_TRANSACTION_ERROR -> + StakingError.BroadcastTransactionError + StakeKitErrorMessageDTO.MISSING_GAS_PRICE_STRATEGY_ERROR -> + StakingError.MissingGasPriceStrategyError + StakeKitErrorMessageDTO.SUBSTRATE_MALFORMED_TRANSACTION_HASH_ERROR -> + StakingError.SubstrateMalformedTransactionHashError + StakeKitErrorMessageDTO.TRON_MAXIMUM_AMOUNT_OF_VALIDATORS_EXCEEDED_ERROR -> + StakingError.TronMaximumAmountOfValidatorsExceededError + StakeKitErrorMessageDTO.SUBSTRATE_POOL_NOT_FOUND_ERROR -> + StakingError.SubstratePoolNotFoundError + StakeKitErrorMessageDTO.SUBSTRATE_BONDED_AMOUNT_TOO_LOW_ERROR -> + StakingError.SubstrateBondedAmountTooLowError + StakeKitErrorMessageDTO.TRON_MISSING_RESOURCE_TYPE_ARGUMENT_ERROR -> + StakingError.TronMissingResourceTypeArgumentError + StakeKitErrorMessageDTO.AAVE_V3_POOL_FROZEN_ERROR -> + StakingError.AaveV3PoolFrozenError + StakeKitErrorMessageDTO.AAVE_V3_TOKEN_PAIR_NOT_FOUND_ERROR -> + StakingError.AaveV3TokenPairNotFoundError + StakeKitErrorMessageDTO.YEARN_VAULT_AT_MAX_CAPACITY_ERROR -> + StakingError.YearnVaultAtMaxCapacityError + StakeKitErrorMessageDTO.STETH_NO_WITHDRAWAL_REQUESTS_FOUND_ERROR -> + StakingError.StETHNoWithdrawalRequestsFoundError + StakeKitErrorMessageDTO.MORPHO_LENDING_POOL_PAUSED_ERROR -> + StakingError.MorphoLendingPoolPausedError + StakeKitErrorMessageDTO.NONCE_UNAVAILABLE_ERROR -> + StakingError.NonceUnavailableError + StakeKitErrorMessageDTO.COSMOS_ACCOUNT_NOT_FOUND_ERROR -> + StakingError.CosmosAcccountNotFoundError + StakeKitErrorMessageDTO.AVALANCHE_MISSING_ADDITIONAL_ADDRESSES_ARGUMENT_ERROR -> + StakingError.AvalancheMissingAdditionalAddressesArgumentError + StakeKitErrorMessageDTO.AVALANCHE_VALIDATOR_INFO_NOT_FOUND_ERROR -> + StakingError.AvalancheValidatorInfoNotFoundError + StakeKitErrorMessageDTO.SOLANA_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE_ERROR -> + StakingError.SolanaTransactionSignatureVerificationFailureError + StakeKitErrorMessageDTO.SOLANA_UNABLE_TO_CREATE_STAKE_ACCOUNT_ERROR -> + StakingError.SolanaUnableTocreateStakeAccountError + StakeKitErrorMessageDTO.SOLANA_STAKE_AMOUNT_TOO_LOW_ERROR -> + StakingError.SolanaStakeAmountTooLowError + StakeKitErrorMessageDTO.SOLANA_UNSTAKE_AMOUNT_TOO_LOW_ERROR -> + StakingError.SolanaUnstakeAmountTooLowError + StakeKitErrorMessageDTO.SOLANA_STAKE_ACCOUNTS_NOT_FOUND_ERROR -> + StakingError.SolanaStakeAccountsNotFoundError + StakeKitErrorMessageDTO.SOLANA_ELIGIBLE_STAKE_ACCOUNTS_NOT_FOUND_ERROR -> + StakingError.SolanaEligibleStakeAccountsNotFoundError + StakeKitErrorMessageDTO.TEZOS_NO_BALANCE_DELEGATED_ERROR -> + StakingError.TezosNoBalanceDelegatedError + StakeKitErrorMessageDTO.TEZOS_MISSING_PUBKEY_ARGUMENT_ERROR -> + StakingError.TezosMissingPubkeyArgumentError + StakeKitErrorMessageDTO.TEZOS_ESTIMATE_REVEAL_GAS_LIMIT_ERROR -> + StakingError.TezosEstimateRevealGasLimitError + StakeKitErrorMessageDTO.TEZOS_BALANCE_ALREADY_DELEGATED_ERROR -> + StakingError.TezosBalanceAlreadyDelegatedError + StakeKitErrorMessageDTO.BINANCE_ACCOUNT_NOT_FOUND_ERROR -> + StakingError.BinanceAccountNotFoundError + StakeKitErrorMessageDTO.BINANCE_MISSING_ACCOUNT_NUMBER_OR_SEQUENCE_ERROR -> + StakingError.BinanceMissingAccountNumberOrSequenceError + StakeKitErrorMessageDTO.GRT_STAKING_DISABLED_ERROR -> + StakingError.GRTStakingDisabledError + StakeKitErrorMessageDTO.GRT_STAKING_DISABLED_LEDGER_LIVE_ERROR -> + StakingError.GRTStakingDisabledLedgerLiveError + else -> StakingError.UnknownError + } + } catch (e: Exception) { + StakingError.UnknownError + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 87b0102d21..f0562851c1 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -1,11 +1,17 @@ package com.tangem.data.staking.di +import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.DefaultStakingErrorResolver import com.tangem.data.staking.DefaultStakingRepository +import com.tangem.data.staking.converters.error.StakeKitErrorConverter import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse +import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -40,4 +46,13 @@ internal object StakingDataModule { stakingFeatureToggle = stakingFeatureToggle, ) } + + @Provides + @Singleton + internal fun provideStakingErrorResolver(@NetworkMoshi moshi: Moshi): StakingErrorResolver { + val jsonAdapter = moshi.adapter(StakeKitErrorResponse::class.java) + return DefaultStakingErrorResolver( + stakeKitErrorConverter = StakeKitErrorConverter(jsonAdapter), + ) + } } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt new file mode 100644 index 0000000000..0f1ff03f2b --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt @@ -0,0 +1,92 @@ +package com.tangem.domain.staking.model + +sealed class StakingError { + + // region stakekit errors + + data class MinimumAmountNotReachedError(val amount: String) : StakingError() + + data class MissingArgumentsError(val arguments: String) : StakingError() + + data class YieldUnderMaintenanceError(val yieldId: String) : StakingError() + + data object InsufficientFundsError : StakingError() + + data object StakedPositionNotFoundError : StakingError() + + data object InvalidAmountSubmittedError : StakingError() + + data object BalanceUnavailableError : StakingError() + + data object GasPriceUnavailableError : StakingError() + + data object NotImplementedError : StakingError() + + data object TokenNotFoundError : StakingError() + + data object BroadcastTransactionError : StakingError() + + data object MissingGasPriceStrategyError : StakingError() + + data object SubstrateMalformedTransactionHashError : StakingError() + + data object TronMaximumAmountOfValidatorsExceededError : StakingError() + + data object SubstratePoolNotFoundError : StakingError() + + data object SubstrateBondedAmountTooLowError : StakingError() + + data object TronMissingResourceTypeArgumentError : StakingError() + + data object AaveV3PoolFrozenError : StakingError() + + data object AaveV3TokenPairNotFoundError : StakingError() + + data object YearnVaultAtMaxCapacityError : StakingError() + + data object StETHNoWithdrawalRequestsFoundError : StakingError() + + data object MorphoLendingPoolPausedError : StakingError() + + data object NonceUnavailableError : StakingError() + + data object CosmosAcccountNotFoundError : StakingError() + + data object AvalancheMissingAdditionalAddressesArgumentError : StakingError() + + data object AvalancheValidatorInfoNotFoundError : StakingError() + + data object SolanaTransactionSignatureVerificationFailureError : StakingError() + + data object SolanaUnableTocreateStakeAccountError : StakingError() + + data object SolanaStakeAmountTooLowError : StakingError() + + data object SolanaUnstakeAmountTooLowError : StakingError() + + data object SolanaStakeAccountsNotFoundError : StakingError() + + data object SolanaEligibleStakeAccountsNotFoundError : StakingError() + + data object TezosNoBalanceDelegatedError : StakingError() + + data object TezosMissingPubkeyArgumentError : StakingError() + + data object TezosEstimateRevealGasLimitError : StakingError() + + data object TezosBalanceAlreadyDelegatedError : StakingError() + + data object BinanceAccountNotFoundError : StakingError() + + data object BinanceMissingAccountNumberOrSequenceError : StakingError() + + data object GRTStakingDisabledError : StakingError() + + data object GRTStakingDisabledLedgerLiveError : StakingError() + + // endregion + + data class DataError(val cause: Throwable) : StakingError() + + data object UnknownError : StakingError() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index 47e9960974..24f9b37b0e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -3,7 +3,8 @@ package com.tangem.domain.staking import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.domain.staking.error.StakingTokensError +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository /** @@ -11,12 +12,13 @@ import com.tangem.domain.staking.repositories.StakingRepository */ class FetchStakingTokensUseCase( private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke(isRefresh: Boolean = false): Either { + suspend operator fun invoke(isRefresh: Boolean = false): Either { return either { catch( block = { stakingRepository.fetchEnabledYields(isRefresh) }, - catch = { StakingTokensError.DataError(it) }, + catch = { stakingErrorResolver.resolve(it) }, ) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 525fd569cb..a2ff4120c1 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -1,23 +1,35 @@ package com.tangem.domain.staking import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, ) { suspend operator fun invoke( userWalletId: UserWalletId, address: CryptoCurrencyAddress, refresh: Boolean = false, - ): Either = Either.catch { - stakingRepository.fetchSingleYieldBalance( - userWalletId = userWalletId, - address = address, - refresh = refresh, - ) + ): Either { + return either { + catch( + block = { + stakingRepository.fetchSingleYieldBalance( + userWalletId = userWalletId, + address = address, + refresh = refresh, + ) + }, + catch = { stakingErrorResolver.resolve(it) }, + ) + } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 2cc658f615..9000f0af29 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -1,6 +1,10 @@ package com.tangem.domain.staking +import arrow.core.Either +import arrow.core.raise.catch import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency @@ -9,9 +13,15 @@ import com.tangem.domain.tokens.model.CryptoCurrency */ class GetStakingAvailabilityUseCase( private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingAvailability { - return stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol) + suspend operator fun invoke( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): Either { + return Either + .catch { stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol) } + .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt index 02a505aa03..451284a714 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt @@ -2,14 +2,21 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository /** * Use case for getting entry info about staking on token screen. */ -class GetStakingEntryInfoUseCase(private val stakingRepository: StakingRepository) { +class GetStakingEntryInfoUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { - suspend operator fun invoke(integrationId: String): Either { - return Either.catch { stakingRepository.getEntryInfo(integrationId) } + suspend operator fun invoke(integrationId: String): Either { + return Either + .catch { stakingRepository.getEntryInfo(integrationId) } + .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index f2e7c1440a..77ac00f093 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -4,7 +4,9 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.StakingError import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId @@ -13,16 +15,17 @@ import kotlinx.coroutines.flow.map class GetStakingYieldBalanceUseCase( private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, ) { operator fun invoke( userWalletId: UserWalletId, address: CryptoCurrencyAddress, - ): EitherFlow { + ): EitherFlow { return stakingRepository.getSingleYieldBalanceFlow( userWalletId = userWalletId, address = address, - ).map> { it.right() } - .catch { emit(it.left()) } + ).map> { it.right() } + .catch { emit(stakingErrorResolver.resolve(it).left()) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt index a63dc81cdc..92d0d6e3c4 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt @@ -1,16 +1,24 @@ package com.tangem.domain.staking import arrow.core.Either +import arrow.core.raise.catch +import com.tangem.domain.staking.model.StakingError import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency /** * Use case for getting staking yield for staking scenario start. */ -class GetYieldUseCase(private val stakingRepository: StakingRepository) { +class GetYieldUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { - suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either { - return Either.catch { stakingRepository.getYield(cryptoCurrencyId, symbol) } + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either { + return Either + .catch { stakingRepository.getYield(cryptoCurrencyId, symbol) } + .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt index a12b75f7ae..4a9fc87c47 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt @@ -1,9 +1,11 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.StakingError import com.tangem.domain.staking.model.Token import com.tangem.domain.staking.model.action.EnterAction import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import kotlinx.coroutines.delay import java.math.BigDecimal @@ -11,7 +13,10 @@ import java.math.BigDecimal /** * Use case for creating enter action */ -class InitializeStakingProcessUseCase(private val stakingRepository: StakingRepository) { +class InitializeStakingProcessUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { suspend operator fun invoke( integrationId: String, @@ -19,7 +24,7 @@ class InitializeStakingProcessUseCase(private val stakingRepository: StakingRepo address: String, validatorAddress: String, token: Token, - ): Either> { + ): Either> { return Either.catch { val createAction = stakingRepository.createEnterAction( integrationId = integrationId, @@ -36,6 +41,8 @@ class InitializeStakingProcessUseCase(private val stakingRepository: StakingRepo val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) createAction to patchedTransaction + }.mapLeft { + stakingErrorResolver.resolve(it) } } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt index 009469c351..4e7e5efedc 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt @@ -1,15 +1,20 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.StakingError import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository /** * Use case for saving hash that failed to submit during staking confirmation */ -class SaveUnsubmittedHashUseCase(private val stakingRepository: StakingRepository) { +class SaveUnsubmittedHashUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { - suspend operator fun invoke(transactionId: String, transactionHash: String): Either { + suspend operator fun invoke(transactionId: String, transactionHash: String): Either { return Either.catch { stakingRepository.storeUnsubmittedHash( unsubmittedTransactionMetadata = UnsubmittedTransactionMetadata( @@ -17,6 +22,8 @@ class SaveUnsubmittedHashUseCase(private val stakingRepository: StakingRepositor transactionHash = transactionHash, ), ) + }.mapLeft { + stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt index 5c2fbd531d..6a83ee4624 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt @@ -1,16 +1,21 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository /** * Use case for commiting hashes that failed to submit during staking confirmation */ -class SendUnsubmittedHashesUseCase(private val stakingRepository: StakingRepository) { +class SendUnsubmittedHashesUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { - suspend operator fun invoke(): Either { - return Either.catch { - stakingRepository.sendUnsubmittedHashes() - } + suspend operator fun invoke(): Either { + return Either + .catch { stakingRepository.sendUnsubmittedHashes() } + .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt index 091d9a2f6b..8fd4ce8d18 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt @@ -1,19 +1,27 @@ package com.tangem.domain.staking import arrow.core.Either +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository /** * Use case for submitting transaction hash to stakekit */ -class SubmitHashUseCase(private val stakingRepository: StakingRepository) { +class SubmitHashUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { - suspend fun submitHash(transactionId: String, transactionHash: String): Either { - return Either.catch { - stakingRepository.submitHash( - transactionId = transactionId, - transactionHash = transactionHash, - ) - } + suspend fun submitHash(transactionId: String, transactionHash: String): Either { + return Either + .catch { + stakingRepository.submitHash( + transactionId = transactionId, + transactionHash = transactionHash, + ) + }.mapLeft { + stakingErrorResolver.resolve(it) + } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt deleted file mode 100644 index d97a592cbd..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.staking.error - -sealed class StakingTokensError { - - data class DataError(val cause: Throwable) : StakingTokensError() -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt new file mode 100644 index 0000000000..c08fdb199d --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.staking.repositories + +import com.tangem.domain.staking.model.* + +interface StakingErrorResolver { + + fun resolve(throwable: Throwable): StakingError +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 789a0a6f82..512875fb4b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -19,6 +19,7 @@ import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.StakingError import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -239,7 +240,7 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithStaking(stakingEither: Either): TokenDetailsState { + fun getStateWithStaking(stakingEither: Either): TokenDetailsState { return currentStateProvider().copy( stakingBlocksState = stakingStateConverter.convert(stakingEither), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index d7f0e655f9..6f3e41632f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.StakingError import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents @@ -12,9 +13,9 @@ import com.tangem.utils.converter.Converter internal class TokenStakingStateConverter( private val clickIntents: TokenDetailsClickIntents, private val currentStateProvider: Provider, -) : Converter, StakingBlockUM> { +) : Converter, StakingBlockUM> { - override fun convert(value: Either): StakingBlockUM { + override fun convert(value: Either): StakingBlockUM { val state = currentStateProvider() if (state.stakingBlocksState is StakingBlockUM.Staked) return state.stakingBlocksState 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 245dd160ad..4c32037a24 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 @@ -389,7 +389,8 @@ internal class TokenDetailsViewModel @Inject constructor( val stakingAvailability = getStakingAvailabilityUseCase( cryptoCurrencyId = cryptoCurrency.id, symbol = cryptoCurrency.symbol, - ) + ).getOrElse { StakingAvailability.Unavailable } + internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) From a64efffd08e65cba3ed134a857733b967b264cbf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 20:08:03 +0500 Subject: [PATCH 134/223] Updated on 2026-08-14 --- .../com/tangem/tap/common/settings/IntentSettingsManager.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt index 09305fd59e..145073e75c 100644 --- a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.settings import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.net.Uri import android.provider.Settings import com.tangem.core.navigation.settings.SettingsManager @@ -11,7 +12,9 @@ internal class IntentSettingsManager(val context: Context) : SettingsManager { val openSettingsIntent = Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", context.packageName, null), - ) + ).apply { + flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } context.startActivity(openSettingsIntent) } } \ No newline at end of file From c84f5538bd21628708918de88ac5ceb0a70540a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 20:13:14 +0500 Subject: [PATCH 135/223] Updated on 2026-08-14 --- .../java/com/tangem/data/settings/di/SettingsDataModule.kt | 4 ++-- .../presentation/wallet/viewmodels/WalletViewModel.kt | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) 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 index da47f1f604..d1daa5b8a0 100644 --- 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 @@ -1,13 +1,13 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository -import com.tangem.data.settings.DefaultPromoSettingsRepository import com.tangem.data.settings.DefaultPermissionRepository +import com.tangem.data.settings.DefaultPromoSettingsRepository import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository -import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.PermissionRepository +import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides 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 b88fbcf1f9..e13c2f56af 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 @@ -7,8 +7,8 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.settings.* +import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -29,6 +29,7 @@ import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvid import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -151,7 +152,8 @@ internal class WalletViewModel @Inject constructor( viewModelScope.launch { val isPushToggled = pushNotificationsFeatureToggles.isPushNotificationsEnabled val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) - if (!isPushToggled || !shouldRequestPush) return@launch + val isPushPermissionAvailable = getPushPermissionOrNull() != null + if (!isPushToggled || !shouldRequestPush || !isPushPermissionAvailable) return@launch delay(timeMillis = 1_800) From f7412dfd86d6b2373bf3df484208b31101c572a9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 20:25:38 +0300 Subject: [PATCH 136/223] Updated on 2026-08-14 --- .../components/bottomsheets/TangemBottomSheet.kt | 3 ++- .../referral/ui/AgreementBottomSheetContent.kt | 15 +++++++++------ .../feature/referral/ui/ReferralBottomSheet.kt | 3 +++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 8b48fdcf60..67466771ba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -141,6 +141,7 @@ inline fun BasicBottomSheet( val model = config.content as? T ?: return val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } ModalBottomSheet( // FIXME temporary solution to fix height of the bottom sheet @@ -155,7 +156,7 @@ inline fun BasicBottomSheet( Column( modifier = Modifier.let { if (addBottomInsets) { - it.navigationBarsPadding() + it.padding(bottom = bottomBarHeight) } else { it } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index 204393dc6a..66be1782f1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -2,15 +2,19 @@ package com.tangem.feature.referral.ui import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.appbar.TangemTopAppBar @@ -22,11 +26,10 @@ import com.tangem.feature.referral.presentation.R * Bottom sheet content with referral program terms of use * * @param url link to the html page + * @param bottomBarHeight bottom insets */ @Composable -internal fun AgreementBottomSheetContent(url: String) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - +internal fun AgreementBottomSheetContent(url: String, bottomBarHeight: Dp) { Column( modifier = Modifier .fillMaxSize() @@ -62,6 +65,6 @@ private fun AgreementHtmlView(url: String) { @Composable private fun Preview_AgreementBottomSheet() { TangemThemePreview { - AgreementBottomSheetContent(url = "https://tangem.com/en/") + AgreementBottomSheetContent(url = "https://tangem.com/en/", 50.dp) } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt index d91a122117..678775213a 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt @@ -3,6 +3,7 @@ package com.tangem.feature.referral.ui import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -24,6 +25,7 @@ internal fun ReferralBottomSheet( config: ReferralStateHolder.ReferralInfoState, ) { val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } if (isVisible) { ModalBottomSheet( @@ -43,6 +45,7 @@ internal fun ReferralBottomSheet( is ReferralStateHolder.ReferralInfoState.ParticipantContent -> config.url is ReferralStateHolder.ReferralInfoState.Loading -> "" }, + bottomBarHeight = bottomBarHeight, ) } } From d7f661a854b3b41a568266bef2075ddecd87a5e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 23:11:02 +0400 Subject: [PATCH 137/223] Updated on 2026-08-14 --- .../local/preferences/utils/AppPreferencesStoreExt.kt | 7 ++++--- .../data/tokens/repository/DefaultCurrenciesRepository.kt | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index aa3171803c..26be12c002 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -6,6 +6,7 @@ import com.squareup.moshi.JsonDataException import com.squareup.moshi.Types import com.tangem.datasource.local.preferences.AppPreferencesStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map @@ -20,7 +21,7 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.storeObjectList(key: Preferen /** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */ inline fun AppPreferencesStore.getObjectList(key: Preferences.Key): Flow?> { val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java)) - return data.map { it[key]?.let(adapter::fromJson) } + return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged() } /** Get list of data [T] by string [key], or empty if data is not found */ diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 7951e83c17..167c691cec 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -261,7 +261,7 @@ internal class DefaultCurrenciesRepository( launch(dispatchers.io) { combine( - getMultiCurrencyWalletCurrencies(userWallet).distinctUntilChanged(), + getMultiCurrencyWalletCurrencies(userWallet), isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } }, ) { currencies, isFetching -> send(currencies, isStillLoading = isFetching) From ac87667f88c444857749b7ba83010dcb553db693 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 23:11:52 +0400 Subject: [PATCH 138/223] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 30 ++++++----- .../repository/DefaultNetworksRepository.kt | 51 ++++++++++--------- 2 files changed, 44 insertions(+), 37 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 167c691cec..dcfd66e7a7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -442,21 +442,23 @@ internal class DefaultCurrenciesRepository( } private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { - try { - isMultiCurrencyWalletCurrenciesFetching.update { - it + (userWallet.walletId to true) - } + cacheRegistry.invokeOnExpire( + key = getTokensCacheKey(userWallet.walletId), + skipCache = refresh, + block = { + isMultiCurrencyWalletCurrenciesFetching.update { + it + (userWallet.walletId to true) + } - cacheRegistry.invokeOnExpire( - key = getTokensCacheKey(userWallet.walletId), - skipCache = refresh, - block = { fetchTokens(userWallet) }, - ) - } finally { - isMultiCurrencyWalletCurrenciesFetching.update { - it - userWallet.walletId - } - } + try { + fetchTokens(userWallet) + } finally { + isMultiCurrencyWalletCurrenciesFetching.update { + it - userWallet.walletId + } + } + }, + ) } private suspend fun fetchTokens(userWallet: UserWallet) { 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 index ce8d705b9f..bcc65da983 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/DefaultNetworksRepository.kt @@ -183,24 +183,22 @@ internal class DefaultNetworksRepository( networks: Set, refresh: Boolean, ) { - try { - isNetworkStatusesFetching.update { - it + (userWalletId to true) - } + val currencies = getCurrencies(userWalletId, networks) + val networksDeferred = networks.mapNotNull { network -> + fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh) + } - val currencies = getCurrencies(userWalletId, networks) - coroutineScope { - networks - .map { network -> - async { - fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh) - } - } - .awaitAll() - } - } finally { - isNetworkStatusesFetching.update { - it - userWalletId + if (networksDeferred.isNotEmpty()) { + try { + isNetworkStatusesFetching.update { + it + (userWalletId to true) + } + + networksDeferred.awaitAll() + } finally { + isNetworkStatusesFetching.update { + it - userWalletId + } } } } @@ -226,12 +224,19 @@ internal class DefaultNetworksRepository( network: Network, currencies: Sequence, refresh: Boolean, - ) { - cacheRegistry.invokeOnExpire( - key = getNetworksStatusesCacheKey(userWalletId, network), - skipCache = refresh, - block = { fetchNetworkStatus(userWalletId, network, currencies) }, - ) + ): Deferred? = coroutineScope { + val key = getNetworksStatusesCacheKey(userWalletId, network) + if (refresh || cacheRegistry.isExpired(key)) { + async { + cacheRegistry.invokeOnExpire( + key = key, + skipCache = refresh, + block = { fetchNetworkStatus(userWalletId, network, currencies) }, + ) + } + } else { + null + } } private suspend fun fetchNetworkStatus( From 4a6dee8e568f07b865121042f426b9f68fdc118c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 23:12:51 +0400 Subject: [PATCH 139/223] Updated on 2026-08-14 --- .../balancehiding/error/HideBalancesError.kt | 2 +- .../kotlin/com/tangem/domain/core/lce/Lce.kt | 39 +++++++++++++++++++ .../com/tangem/domain/core/lce/LceRaise.kt | 2 +- .../tokens/GetWalletTotalBalanceUseCase.kt | 20 +++++++--- .../CurrenciesStatusesLceOperations.kt | 4 +- 5 files changed, 57 insertions(+), 10 deletions(-) diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt index 7438c20918..fb0e72e862 100644 --- a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/error/HideBalancesError.kt @@ -2,7 +2,7 @@ package com.tangem.domain.balancehiding.error sealed class HideBalancesError { - object HidingDisabled : HideBalancesError() + data object HidingDisabled : HideBalancesError() data class DataError(val cause: Throwable) : HideBalancesError() } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt index 0812a3d523..2d6902a58a 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -107,4 +107,43 @@ sealed class Lce { ifContent = { null }, ifError = ::identity, ) + + /** + * Returns `true` if this [Lce] is a [Lce.Loading] state and the given predicate is `true`. + * + * @param predicate The predicate to apply to the partial content. + * By default, the predicate is `true` for any partial content. + * @return `true` if this [Lce] is a [Lce.Loading] state and the given predicate is `true`, `false` otherwise. + */ + fun isLoading(predicate: (maybeContent: C?) -> Boolean = { true }): Boolean = fold( + ifLoading = { predicate(it) }, + ifContent = { false }, + ifError = { false }, + ) + + /** + * Returns `true` if this [Lce] is a [Lce.Error] state and the given predicate is `true`. + * + * @param predicate The predicate to apply to the error. + * By default, the predicate is `true` for any error. + * @return `true` if this [Lce] is a [Lce.Error] state and the given predicate is `true`, `false` otherwise. + */ + fun isError(predicate: (error: E) -> Boolean = { true }): Boolean = fold( + ifLoading = { false }, + ifContent = { false }, + ifError = { predicate(it) }, + ) + + /** + * Returns `true` if this [Lce] is a [Lce.Content] state and the given predicate is `true`. + * + * @param predicate The predicate to apply to the content. + * By default, the predicate is `true` for any content. + * @return `true` if this [Lce] is a [Lce.Content] state and the given predicate is `true`, `false` otherwise. + */ + fun isContent(predicate: (content: C) -> Boolean = { true }): Boolean = fold( + ifLoading = { false }, + ifContent = { predicate(it) }, + ifError = { false }, + ) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt index 2040b0774a..1484205426 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt @@ -46,7 +46,7 @@ class LceRaise @PublishedApi internal constructor( * */ @RaiseDSL @OptIn(ExperimentalTypeInference::class) - inline fun withError( + inline fun withError( transform: (OtherError) -> E, @BuilderInference block: LceRaise.() -> C, ): C = recover( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index ce783509f9..0f4d3477da 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens +import arrow.atomic.update import arrow.core.raise.ensureNotNull import arrow.core.toNonEmptyListOrNull import com.tangem.domain.core.lce.Lce @@ -16,9 +17,10 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.flow.transformLatest class GetWalletTotalBalanceUseCase( private val currenciesRepository: CurrenciesRepository, @@ -28,9 +30,9 @@ class GetWalletTotalBalanceUseCase( ) { suspend operator fun invoke( - userWallestIds: Collection, + userTallestIds: Collection, ): LceFlow> { - val flows = userWallestIds.distinct() + val flows = userTallestIds.distinct() .map { userWalletId -> invoke(userWalletId).map { maybeBalance -> userWalletId to maybeBalance @@ -39,17 +41,23 @@ class GetWalletTotalBalanceUseCase( return combine(flows) { balances -> lce { - balances.associate { (userWalletId, maybeBalance) -> - userWalletId to maybeBalance.bind() + balances.fold(mutableMapOf()) { acc, (userWalletId, maybeBalance) -> + val balance = maybeBalance.bindOrNull() ?: TotalFiatBalance.Loading + + isLoading.update { it || balance is TotalFiatBalance.Loading } + + acc[userWalletId] = balance + acc } } } } + @OptIn(ExperimentalCoroutinesApi::class) suspend operator fun invoke(userWalletId: UserWalletId): LceFlow { val currenciesStatuses = getStatuses(userWalletId) - return currenciesStatuses.transform { maybeStatuses -> + return currenciesStatuses.transformLatest { maybeStatuses -> val balance = createBalance(maybeStatuses) emit(balance) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index a4265dd621..0a542d0874 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -33,7 +33,7 @@ internal class CurrenciesStatusesLceOperations( return transformToCurrenciesStatuses( userWalletId = userWalletId, flow = if (isSingleCurrencyWalletsAllowed) { - getWalletCurrenies(userWalletId) + getWalletCurrencies(userWalletId) } else { getMultiCurrencyWalletCurrencies(userWalletId) }, @@ -105,7 +105,7 @@ internal class CurrenciesStatusesLceOperations( return statuses } - private fun getWalletCurrenies(userWalletId: UserWalletId): LceFlow> { + private fun getWalletCurrencies(userWalletId: UserWalletId): LceFlow> { return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) .map { maybeCurrencies -> maybeCurrencies.mapError { TokenListError.DataError(it) } From 5c57cb85d60848be84e46a195652ec251f317fd9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 23:14:29 +0400 Subject: [PATCH 140/223] Updated on 2026-08-14 --- .../domain/GetMultiWalletWarningsFactory.kt | 65 +++++++++---------- .../wallet/viewmodels/WalletViewModel.kt | 2 +- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index b0513c7656..c28d7d5e70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain -import arrow.core.Either import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.promo.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase @@ -23,7 +23,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flow import javax.inject.Inject import kotlin.collections.count @@ -47,11 +46,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) } return combine( - flow = getTokenListUseCase.launch(userWallet.walletId).conflate(), - flow2 = isReadyToShowRateAppUseCase().conflate(), - flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), - flow4 = shouldShowSwapPromoWalletUseCase().conflate(), - flow5 = promoFlow.conflate(), + flow = getTokenListUseCase.launchLce(userWallet.walletId), + flow2 = isReadyToShowRateAppUseCase(), + flow3 = isNeedToBackupUseCase(userWallet.walletId), + flow4 = shouldShowSwapPromoWalletUseCase(), + flow5 = promoFlow, ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner -> readyForRateAppNotification = true @@ -113,7 +112,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addInformationalNotifications( cardTypesResolver: CardTypesResolver, - maybeTokenList: Either, + maybeTokenList: Lce, clickIntents: WalletClickIntents, ) { addIf( @@ -125,7 +124,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } private fun MutableList.addMissingAddressesNotification( - maybeTokenList: Either, + maybeTokenList: Lce, clickIntents: WalletClickIntents, ) { val currencies = maybeTokenList.getMissingAddressCurrencies() @@ -141,26 +140,23 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Either.getMissingAddressCurrencies(): List { - return fold( - ifLeft = { emptyList() }, - ifRight = { tokenList -> - val currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } + private fun Lce.getMissingAddressCurrencies(): List { + val tokenList = getOrNull(isPartialContentAccepted = false) ?: return emptyList() - currencies - .filter { it.value is CryptoCurrencyStatus.MissedDerivation } - .map(CryptoCurrencyStatus::currency) - }, - ) + val currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + return currencies + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) } private fun MutableList.addWarningNotifications( cardTypesResolver: CardTypesResolver, - tokenList: Either, + tokenList: Lce, isNeedToBackup: Boolean, clickIntents: WalletClickIntents, ) { @@ -182,19 +178,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Either.hasUnreachableNetworks(): Boolean { - return fold( - ifLeft = { false }, - ifRight = { tokenList -> - val currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } + private fun Lce.hasUnreachableNetworks(): Boolean { + val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false - currencies.any { it.value is CryptoCurrencyStatus.Unreachable } - }, - ) + val currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) + is TokenList.Ungrouped -> tokenList.currencies + is TokenList.Empty -> emptyList() + } + + return currencies.any { it.value is CryptoCurrencyStatus.Unreachable } } private fun MutableList.addRateTheAppNotification( 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 b88fbcf1f9..645de9a38b 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 @@ -7,8 +7,8 @@ import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.settings.* +import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase From b14f487b212b54bdd27362c159af927df7086219 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 23:17:35 +0400 Subject: [PATCH 141/223] Updated on 2026-08-14 --- .../core/ui/components/block/BlockCard.kt | 4 +- features/details/impl/build.gradle.kts | 5 + .../preview/PreviewUserWalletListComponent.kt | 15 ++- .../details/entity/UserWalletListUM.kt | 10 +- .../details/ui/UserWalletListBlock.kt | 108 +++++++++++++----- .../details/ui/coil/RotationTransformation.kt | 22 ++++ .../details/utils/UserWalletMappers.kt | 59 +++++++--- .../details/utils/UserWalletsFetcher.kt | 27 +++-- .../drawable/ill_card_note_bnb_211_343.png | Bin 36912 -> 0 bytes .../drawable/ill_card_note_eth_211_343.png | Bin 34834 -> 0 bytes .../drawable/ill_card_wallet_2_211_343.png | Bin 28664 -> 0 bytes .../drawable/img_card_wallet_2_gray_22_36.xml | 13 +++ 12 files changed, 196 insertions(+), 67 deletions(-) create mode 100644 features/details/impl/src/main/kotlin/com/tangem/features/details/ui/coil/RotationTransformation.kt delete mode 100644 features/details/impl/src/main/res/drawable/ill_card_note_bnb_211_343.png delete mode 100644 features/details/impl/src/main/res/drawable/ill_card_note_eth_211_343.png delete mode 100644 features/details/impl/src/main/res/drawable/ill_card_wallet_2_211_343.png create mode 100644 features/details/impl/src/main/res/drawable/img_card_wallet_2_gray_22_36.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt index 24f64babdc..49752f2700 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt @@ -32,6 +32,6 @@ val TangemBlockCardColors: CardColors get() = CardColors( containerColor = TangemTheme.colors.background.primary, contentColor = TangemTheme.colors.text.primary1, - disabledContainerColor = TangemTheme.colors.button.disabled, - disabledContentColor = TangemTheme.colors.text.disabled, + disabledContainerColor = TangemTheme.colors.background.primary, + disabledContentColor = TangemTheme.colors.text.primary1, ) \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index c1f2899eff..c6694323ba 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -37,11 +37,15 @@ dependencies { implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) implementation(projects.domain.walletConnect) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) implementation(projects.domain.legacy) /* SDK */ // TODO: For TangemError model, should be removed after card domain scanning refactoring implementation(deps.tangem.card.core) + // For image resolving + implementation(deps.tangem.blockchain) /* AndroidX */ implementation(deps.androidx.fragment.ktx) @@ -55,6 +59,7 @@ dependencies { implementation(deps.compose.foundation) implementation(deps.compose.material3) implementation(deps.compose.shimmer) + implementation(deps.compose.coil) /* DI */ implementation(deps.hilt.android) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index a8906b5061..92b6f46de3 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -19,23 +19,26 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { userWallets = persistentListOf( UserWalletListUM.UserWalletUM( id = UserWalletId("user_wallet_1".encodeToByteArray()), - name = "My Wallet", + name = stringReference("My Wallet"), information = getInformation(3, "4 496,75 $"), - imageResId = R.drawable.ill_card_wallet_2_211_343, + imageUrl = "", + isEnabled = true, onClick = {}, ), UserWalletListUM.UserWalletUM( id = UserWalletId("user_wallet_2".encodeToByteArray()), - name = "Old wallet", + name = stringReference("Old wallet"), information = getInformation(3, "4 496,75 $"), - imageResId = R.drawable.ill_card_note_eth_211_343, + imageUrl = "", + isEnabled = true, onClick = {}, ), UserWalletListUM.UserWalletUM( id = UserWalletId("user_wallet_3".encodeToByteArray()), - name = "Multi Card", + name = stringReference("Multi Card"), information = getInformation(3, "4 496,75 $"), - imageResId = R.drawable.ill_card_note_bnb_211_343, + imageUrl = "", + isEnabled = false, onClick = {}, ), ), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index d6f947b063..1569f48efb 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -1,10 +1,11 @@ package com.tangem.features.details.entity -import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.ImmutableList +@Immutable internal data class UserWalletListUM( val userWallets: ImmutableList, val isWalletSavingInProgress: Boolean, @@ -12,12 +13,13 @@ internal data class UserWalletListUM( val onAddNewWalletClick: () -> Unit, ) { + @Immutable data class UserWalletUM( val id: UserWalletId, - val name: String, + val name: TextReference, val information: TextReference, - @DrawableRes - val imageResId: Int, + val imageUrl: String, + val isEnabled: Boolean, val onClick: () -> Unit, ) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index cb235d20fa..794ef632c3 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -10,15 +10,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R +import com.tangem.features.details.ui.coil.RotationTransformation @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { @@ -46,6 +52,7 @@ private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modif BlockCard( modifier = modifier, onClick = model.onClick, + enabled = model.isEnabled, ) { Row( modifier = Modifier @@ -55,39 +62,81 @@ private fun UserWalletItem(model: UserWalletListUM.UserWalletUM, modifier: Modif verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - Image( - modifier = Modifier - .width(TangemTheme.dimens.size24) - .height(TangemTheme.dimens.size36), - painter = painterResource(id = model.imageResId), - contentScale = ContentScale.FillBounds, - contentDescription = null, + Image(imageUrl = model.imageUrl) + NameAndInfo( + name = model.name, + information = model.information, ) - - Column( - modifier = Modifier.heightIn(min = TangemTheme.dimens.size40), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.SpaceEvenly, - ) { - Text( - text = model.name, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = model.information.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } } } } +@Composable +private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) { + Column( + modifier = modifier.heightIn(min = TangemTheme.dimens.size40), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceEvenly, + ) { + Text( + text = name.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + AnimatedContent( + targetState = information.resolveReference(), + label = "User wallet information", + ) { information -> + Text( + text = information, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun Image(imageUrl: String, modifier: Modifier = Modifier) { + val imageModifier = modifier + .width(TangemTheme.dimens.size24) + .height(TangemTheme.dimens.size36) + .clip(TangemTheme.shapes.roundedCornersSmall) + + SubcomposeAsyncImage( + modifier = imageModifier, + model = ImageRequest.Builder(LocalContext.current) + .transformations(RotationTransformation(angle = 90f)) + .size( + width = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() }, + height = with(LocalDensity.current) { TangemTheme.dimens.size24.roundToPx() }, + ) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { + RectangleShimmer( + modifier = imageModifier, + radius = TangemTheme.dimens.size2, + ) + }, + error = { + Image( + modifier = imageModifier, + painter = painterResource(id = R.drawable.img_card_wallet_2_gray_22_36), + contentDescription = null, + ) + }, + contentDescription = null, + ) +} + @Composable private fun AddWalletButton( text: TextReference, @@ -110,6 +159,7 @@ private fun AddWalletButton( AnimatedContent( modifier = Modifier.size(TangemTheme.dimens.size24), targetState = isInProgress, + label = "Add wallet progress", ) { isInProgress -> if (isInProgress) { CircularProgressIndicator( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/coil/RotationTransformation.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/coil/RotationTransformation.kt new file mode 100644 index 0000000000..ca7f0929d0 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/coil/RotationTransformation.kt @@ -0,0 +1,22 @@ +package com.tangem.features.details.ui.coil + +import android.graphics.Bitmap +import android.graphics.Matrix +import coil.size.Size +import coil.transform.Transformation + +internal class RotationTransformation(private val angle: Float) : Transformation { + + override val cacheKey: String = "rotate:$angle" + + override suspend fun transform(input: Bitmap, size: Size): Bitmap { + val matrix = Matrix().apply { + val centerX = input.width / 2f + val centerY = input.height / 2f + + postRotate(angle, centerX, centerY) + } + + return Bitmap.createBitmap(input, 0, 0, input.width, input.height, matrix, true) + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index 992fee20eb..94fc6e00b1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -1,9 +1,6 @@ package com.tangem.features.details.utils -import androidx.annotation.DrawableRes -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.CardDTO @@ -12,6 +9,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R +import com.tangem.utils.Strings.STARS import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -19,11 +17,15 @@ internal fun List.toUiModels( onClick: (UserWalletId) -> Unit, appCurrency: AppCurrency? = null, balances: Map = emptyMap(), + isLoading: Boolean = true, + isBalancesHidden: Boolean = false, ): ImmutableList = this.map { model -> val balance = balances[model.walletId] model.mapToUiModel( balance = balance, appCurrency = appCurrency, + isLoading = isLoading, + isBalanceHidden = isBalancesHidden, onClick = { onClick(model.walletId) }, ) }.toImmutableList() @@ -31,22 +33,52 @@ internal fun List.toUiModels( private fun UserWallet.mapToUiModel( balance: TotalFiatBalance?, appCurrency: AppCurrency?, + isLoading: Boolean, + isBalanceHidden: Boolean, onClick: () -> Unit, ): UserWalletUM = UserWalletUM( id = walletId, - name = name, - information = getInfo(appCurrency, balance), - imageResId = resolveImage(), + name = stringReference(name), + information = getInfo( + appCurrency = appCurrency, + balance = balance, + isBalanceHidden = isBalanceHidden, + isLoading = isLoading, + ), + imageUrl = artworkUrl, + isEnabled = !isLocked, onClick = onClick, ) -private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBalance?): TextReference { +private fun UserWallet.getInfo( + appCurrency: AppCurrency?, + balance: TotalFiatBalance?, + isBalanceHidden: Boolean, + isLoading: Boolean, +): TextReference { + val dividerRef = stringReference(value = " • ") + val cardCount = getCardCount() val cardCountRef = TextReference.PluralRes( id = R.plurals.card_label_card_count, count = cardCount, formatArgs = wrappedList(cardCount), ) + + return when { + isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) + isLoading -> cardCountRef + isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) + else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) + } +} + +private fun getBalanceInfo( + balance: TotalFiatBalance?, + appCurrency: AppCurrency?, + cardCountRef: TextReference, + dividerRef: TextReference, +): TextReference { val amount = when (balance) { is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized } is TotalFiatBalance.Failed, @@ -56,16 +88,15 @@ private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBala } return if (amount != null && appCurrency != null) { - val divider = stringReference(value = " • ") val formattedAmount = BigDecimalFormatter.formatFiatAmount( fiatAmount = amount, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) val amountRef = stringReference(formattedAmount) - TextReference.Combined(wrappedList(cardCountRef, divider, amountRef)) + combinedReference(cardCountRef, dividerRef, amountRef) } else { - cardCountRef + combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN)) } } @@ -75,10 +106,4 @@ private fun UserWallet.getCardCount() = when (val status = scanResponse.card.bac is CardDTO.BackupStatus.NoBackup, null, -> 1 -} - -@DrawableRes -private fun UserWallet.resolveImage(): Int { - // TODO: Implement image resolving [REDACTED_JIRA] - return R.drawable.ill_card_wallet_2_211_343 } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt index 5377be02ea..e141c4034e 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -10,6 +10,8 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.BalanceHidingSettings +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.getOrElse @@ -24,10 +26,7 @@ import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.flow.* import javax.inject.Inject @ComponentScoped @@ -35,6 +34,7 @@ internal class UserWalletsFetcher @Inject constructor( getWalletsUseCase: GetWalletsUseCase, private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val router: Router, private val messageSender: UiMessageSender, ) { @@ -44,10 +44,16 @@ internal class UserWalletsFetcher @Inject constructor( emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) combine( - getSelectedAppCurrencyUseCase(), - getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)), - ) { maybeAppCurrency, maybeBalances -> - val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse( + getSelectedAppCurrencyUseCase().distinctUntilChanged(), + getBalanceHidingSettingsUseCase().distinctUntilChanged(), + getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)).distinctUntilChanged(), + ) { maybeAppCurrency, balanceHidingSettings, maybeBalances -> + val models = createUiModels( + wallets = wallets, + maybeAppCurrency = maybeAppCurrency, + maybeBalances = maybeBalances, + balanceHidingSettings = balanceHidingSettings, + ).getOrElse( ifLoading = { return@combine }, ifError = { val message = resourceReference(R.string.common_unknown_error) @@ -65,10 +71,11 @@ internal class UserWalletsFetcher @Inject constructor( wallets: List, maybeAppCurrency: Either, maybeBalances: Lce>, + balanceHidingSettings: BalanceHidingSettings, ): Lce> = lce { val balances = withError( transform = { Error.UnableToGetBalances }, - block = { maybeBalances.bind() }, + block = { maybeBalances.bindOrNull().orEmpty() }, ) val appCurrency = withError( transform = { Error.UnableToGetAppCurrency }, @@ -79,6 +86,8 @@ internal class UserWalletsFetcher @Inject constructor( appCurrency = appCurrency, balances = balances, onClick = ::navigateToWalletSettings, + isBalancesHidden = balanceHidingSettings.isBalanceHidden, + isLoading = maybeBalances.isLoading(), ) } diff --git a/features/details/impl/src/main/res/drawable/ill_card_note_bnb_211_343.png b/features/details/impl/src/main/res/drawable/ill_card_note_bnb_211_343.png deleted file mode 100644 index 033efd269987b4b12f2e69c75b3bc71f713d1610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36912 zcmZ5nV{j%-xQ(;1y|K+V*x0sh+qP}nw#^MU$;QUUwt3(0*8O#-YHCo^J>6Xo&v`mh zK~5Y278e!-1O!1+LPQC8T>t?ARsR76yyH~#E(Ts;93(WIK|tWp{yRZIvU0G2FF~D^ z#DzfWW`3RmA0W&HWd%V%8sp(U3?V^4;(tqu2&#C1Ugkpj{5)=bnysN@{qG=i`)O7N zmCZ(7;v@asCVP5r>pf+gRT4;HiEGvXF82p`5NWoVYB${HesgPiw`ygq;jbm}%KKZ< zS++9W=RNZtbIo#%p<<0|e%!+esS16LAU?O#SNGmP+I07Q`TN2k?`{gz)m`f7u67zp z*O_i;kHFHyU`;GSf%~UEB9Zx{I~OZ5cpN$cm@u39>|H;@1Z5dmEt zygpWxW6Iy(_Q$@c{~u_YX)iS>7=#2Qw@K(#tlPUBPO{@AXCjyou>eT>cw=-Fl$cJR zvRz(k?q?=M7&=h^7Mp_Izu0k((F}vjlp{MvVB;I8L(E;AmvUwb+LT8$7-S-WUkyX3 zUtO=_Gkf|^?NzJMtJ*lIM-+^6wYU}LBUxWIQ}sPWM;bGJCD2RZ}$ws5EZVA)YWHV1c7 znk>(6-K|PXAKWAcDf8QJ7KG{WH1)>d9P~?HXEW#0QKboo%cpT=I!W`5UxB}x%9VYF z#{nmr8H60n*x;K8G=yK)`?!}h6!FbK^ z+0Buf1!yAsHYq%~re9p`3&SnwkIPK${=jN8i747@i0OUDSnGVQcx#h&#fP z*uZp6$-t&;|6YMM-R#0$sv1X&*CR7#<#eju?K}ptM4{P=ECZk?C)B8rj$6=c)={c=3+ZE$Q(a@&ZVt!E(9- z#^2VAoq0Z|N!yVU6v4Hl@+&w;LqIVOD{n6n6t<-RWyRZHJSws{2u6k5M(WnM*nd4k zcx3zk%;h9$H?X+rZ;F0|F^;yls^`^gIZ4b5wpM963C_|r#?`H8SX66?CO%WnVUO1> zDUx;9X+UxvP0?Oh#ZIdSOhPX!*!U%@On>G0++UyUh#u$t*U`gij_qB=m@zw8NS(8P zYT@5jsvSodR)LPefd8})>d8h+43yb(RL%2dEewKg$~&qcv=~5BwySF)l|* z>xX`=V-n)o@<7CqQ~*dSejm1SIs4kVBDkiovh`(aHo9gLW=RX{T+*v+A-KdJ` z2CaF2p5zu!A6U+YhN{xJiks}L>q@{&?uEVd=|RCtqC;t)Q(4HY(r{Pm zi3{nD-}j6*7CG?e>l+AEtJ#01#GBcr;`z(~UX5f%6&VTp8Gx~Z`K}Ytw^C;*ggrBh-(lJb*5GPnS7%U@~Kj2 zRE1ayyp>^)2awn*wCM|p1I=Qsvevm{BGD%QYqiDA#-<BBW31YBO7S=hiRUO2A=yPDNpZ7G3C&WW@S=kfB)f;86^Ps)E1f}e6lwzRz#@YEW@HdR%(Nv;qgwS zKxL?^(@9uUuK&5etR&R$KtXCTknex`dDJzab3KRNf2d7hTOa7~m-MzNaP@izMkg_a_PFD_wobagy**$glUqTbWav3w;;JU3`vll!aH48+W>@i3F7%jo%c^JmaJ%NG==U}YtnLrRCFDO4n^=GC z(+E91teOukrPN=}lP~Rf6#-P30gdVI$b?bnQ=3M#dJ zp30gIFVTx68-EDM=+nIFsVZmA@!@U#`WPbd{Fe(j+Cf{qf8DW<<62_8o|b3FhH|*r zU7i=JywmM^?f<&DI__1&1LD> zBW&ZMXtf=NSktAl;3VpqFEt$(l^T-o=DF>Hk+Dc#;DUfybsP3tDP=R%wJm@b`A&6- zfet?Z&@&#i6w?=%V(D_VD6q`-;d0v~Zd-4yGhbgYjcS%NdRC6R>|H{+3H$+1r;Jo-EGZZwgiu^fG-0o#7*jYA%9JhlQO&)u32V zy0e!ignR$_`RvAd51+_58nPt(%fV*(VLa-u0t>-K70@RhV(d}cz&Bfw!r;dglhV=k zZ-an-i0ZmdBOd%r_c8rVW%%5Ey5MetrD{h~CZ z-6w)l-w&=C=@a`u%_G@tHI(06?QO;*r^MV7;%v_(0qw96bl zTB?V=vm6rsq$h}XBj6LwC&Obqw`id3mhBJY2n(O58;kBTZ@w&pt>^Uo!BPP0w4=! z9XfKg(w_C%a`&}=RFt1IcW$3zwXbFCJBvA-Y}rzK3f6#xCbyoYM6Fx+&6bJ&tL@CR zg_q7Fn)lC$5N zGTutQj5f{ftR_J-hIMjLQNv1fgjeH)ug`}U+iSCC9f32 zf4`4SO&z4m|6%1NK^`zO4J|33&v4h>5*DkL=T^|n40CP%z5Uwt&Lw>+1LbnxR}G2L zpNeo0-sk&lP^rheQPVnmY?~f#cS>0dbfzktR^dM+wqz9JPj;^B6I1aJ$MMx&(!V_| zItam5ly5=<2G+|NR*4rL;0`%?z8d!t4-twvlzi2l6tIzomNmZFTKuzc@SNKOG{riW zmdw1w0wyCNapOtrD9DvttuT_6iyC3+wha;JPre1J%A7Z#?)0A#*e`ThKy7eu$~kXN zIn*-3Xx3DP=DR%cPq#V~Wq!`h{4mf}WyQpjSf1hFl?QzQA2Il6h83a9`u8vwZWltU z!Kz+qyZg}K$`OIUhV`mdK7EwM(S)0nn}+hmSf;@lFQpN0vAOc$WRa;CH+e{B;un*2 z&Az!Dskymox)Dy0?Hx|X&rA9`lqGe=u;@hs`B0Z2Xr5Dc^ETY_d$OpvXj)j8q#xem!b=%e+^3ef@@v@G7e%C&`L`$SI5~0)YIaH zW2A(x_ak;r$EsTh-~JSWo?RG}f;Os^XxPdDF$#4MBUj*KGWSvaU*3_YLHIph?}&%e(G;zR;z)?8c#KzP(w+_hAQ&ip zxC?uL_k&mWoTqV%d$9+5V*bxbiQ?cY&ECDH8&I$pP<^$RJJN`8L2;J4h9OctpX;I^kXc)qI&OMKk&=@$~%AW*V8#aw%2B>rLXzh zc#oQ`Lw(+YrC`mR91M6=Xcub;i62Ae27;-0%zl@ejf*WY+^SOQx3fG|=*v^_9D@ZQrBWuF+X3(a z)LR~p&+7jd$1h~bhL?CP(Y-!$j7iMOMN&JJ_1-S^ILhLzmv>siwApO=0UE$6wWoxg zCqO`Hh^BH3QEM6o-ivBnMaK_Ptn1pto_Lsd|8JZj851#-K zbG*I|PnVgJYT8^@JP)eI)z0qWRhR}n+q4NWUxE7A1hNM)jJomr6Z*i0IEH4>eb6 zJ9a_TqLT=fwI{1g!s15hC->!lsY$L(z`1`7V;OgyXXPEy5@fXHS?s;p_w%-uEUnP~ zuOK3`0`VC%Fk{&NCyvNKB?RFsyptn>Q%!aV1~v81Qxyh-;KZvy9!0M&uFjpL5>nmA zQ>HOdU0AW!I~w~BS|Adj_%XQ*4siqhpT2|m4XSg*hb3`8;{#ILy^)Tj6&*F45c5-v zKJ9ydEs=3^lRZB6)8nor#nqmBTESrw35bTXe~7#Ce2+Q z0JWOc_ZbYL;^O9aMq14p<@funr;+u8>vU+Yi9)@ZS(H~Tt|}S&uS?1Kj(ld;Z1-mwzSpJ{2bR@pBNJ%U(U)c0+f*bjNu-nnmF|Y6RxGP%OtaX$p z!I8O@kL=Qgtz=eWu2ss&9~=0l4@;3&vfP$zv_NOzfd)BjauTCTaIUAnEL?BD8}TPA z+~CxXib&7vsTdAxgKA}0?eVrId9XJ?{#QK>z7LaabLTQRfWdZw&aVikn4ky>;5}i7 z1869P;AFiq#XvgW5Q?K|bU3gGq#VWudZaEY0BtMt^pbLgd%2h?EYWpWS9zrI5#0v; z@Z+0F6wv!l&bPjo`o~iojYx(P;Nwm%M5~YQZcoRoAQya~P!X{VL_I>wbT?U#xy!v| z0P(id{imr`v|rM#HrLT!-A5OgwcC_{RGeJSqTKTSX?{hjdO1Btcgl( i8p03m5K zZFl@MS8b<*x5x=?nr9`B`ZnAl<@)**XuAvgHPuZNw6P`AFP|?&E}w@I9p<*+Oyffg z03dxuOLuVVOiow{f=V9(CX#JHD4)eWT!+0ZxO%MJT2uY1uPgimm?t;+UMHr+>$h0t z_i(!=!>e$-EKhZnno6MPaJIzuysuFVvqMqozg!&^28>U07jjz#zJ7@aCg#w=OZO&0 zDq<|le^1M4R-3h{un%m0D$8eTGn^`!W%zDe-=dl3PDXw_0hk$R%(4;V42F z_VSO&sq6k_wrzqAo>=hCF2*|~{+yiPrsjIXkvjoY>Q`qiF7fQg^?EuzLgqe_)%s+Y zAl-~*q}6RvuJ~Q*I-^YVxLYajK>XS4IKlo0#0}8TesfHSO()lEWEmfb`+pf8CZPFp z8aKCCE6!qd;hEWWqGHo$FD4ax=`!GK@zDnj(M!}AH(zJGo*s@W_7(~ z+wZVS*u9K;hw(jS<^RIV=rSl@tk>Pb65b=@&(b%D`H{Q#h~>(lE({hCDPNI2F)>dx z$vdqaWW4`0>?FX!nKU?_nBeYep{fo9Pf5|j+F5a>IV$wqvpf;`@^{LuO#>KdGw?+A z+N#3^XUQyVI1tP10+XVQPAp9v8J{RDm$70AJJ`KRp0lUUbzk#TP!l_V!*4ZOpXUF z2SJK$_uZee(4?PHxhq$jb%8O<8UV%gEt$ddLjqnDY~`+iuHUMpM2|{0?S9&ID)m~T ztPfv#Qq?ZR|M>9dbn{uTxoTufRQ0X_rJfaFO zqr4XF5TdM7rv@IFMqPJKvTqTT<<$N2xkjd-Ne6 zX*q+4U-!>a2x%QwY7>)n-qV2(JayUKnJQ*Bn+EYVFqNM zWG(cmdX0u;KSkjpj);zkawq8%4Zo+-pOG?nc~4R-w3rl)`nXzmJLei)Jz4(ryd8fk zOPIe7;=NP3SHEEHEJ^&-mNIno2?sA0S07h@r*g+Gq@juU48JqIt@2ee36J%b@G1~% zr4bw4?#kqz4;pLc%C!fEOA1l;OAv;?3=}(4iO7e6+A&RX2kUmW!mnvRo?vlgcOTcI zgwaIT1_Zt*P)$c9y3`(tzPSp@2f`QHGx|3=uQP}ta9hjku_0A?|@{1 z6dT+91CI}ZOA!4&S=`*F?OLvR6fsK2-VmTlU$@^%LikI&UM%xNvv=K`g0ETvI23~@ z0$-cB5^%TfOvX)`%m5vwV76du*c+;Ddkj&G>xHdOOX0BwCicz#%D0Z&;f?X(_6H0` z&N9kPXe$YK{jCK$7bqiPkB3Qx{qP{)qoQ0OZKhb-#hvvVA2f-)e%dw()T_^5*Oa|@ zRaWip?cO`(ViC+CUiT|#O?N=KZS}` zzh*TNFgLv|0&QNYb~avXypHJMcWpM@>hlo3Zb@ptmffI33v1X!}b3|@i#V-7vW&c5ObD&9X0`nu5GDRI?7DF0V@It&>blo4T_aoHZPwS#KCS&`BVItNoq4pJaKLPQ} zG!%ka45Gd&SZC2$G6d`8nVXKH?#W~S!6^^uwbU!}7d}y`)%wR5ofr(XgBT>{z~B+{Lxht8b#6rv1bev{@+YeD6UGp!xDxR_nZs zYLdYhYje2-eI3;WFLnHUke`N>f^xCk7`PZser)B;C1LAc0R&5=y)n9dzO!9L@yuEi z)^ga8hKvF^yLlZYcT)B`B~Y)!e|@uhT~pX-c)GD?t+JEEnBI?Gj2fL|BSto0OtCtI zN@%+4OZ6(g-!#3<^m@&+=-6G5@JrHN-8-JD@f}=bsB!$ScQ-MpmE4ab8)df;At0}L z%U&#k>ZB-Eiv?vcPw}TklBG@J;vD{mdd^aMfHoIb#->_6-E3@*%=&DGD-uGP4YFyx zrKB)(4`eW0L3AeV2Vc7lUk!eJjLNzZrDw#-eW9e~^bt01LA$WN_(gEmo4dGNHnJc2 zXc*zzYPT}QN_c1!lH_SRj6UNb<=Z-jEnY7ljRpPS2f`F#Au(2?d$~t|oa=k($Ir0W zoFbbI<$6YS_u5eSvn95x^V+Q>(rC3yK<|e5$ivpYN+{7OX6jgAQNN=8z5f@BavffN zkFgTPYN^dZnr^u1!V`}yH99U9o)&?7XYpv>cq)ski$Lx3nMxZ65d<{Yspr^7%V5w;MT#RZMG*4Lb-Om$=b4Qv)l z9`vTE=b{&M8T52bk@ay^dN5+Z^kJh(1vU%DDnBvXNm#4Lmwt1=;auvG92{VbBN6^I z+bjh_HZ*zyNq716^-LoIB^M@dL-^%#6&8PGph5-X<6{3{k-_e*^Y=HbQl0xhEulC{ z!t!0W>53;uS!h0B!CM-Se)K#L!^SPTDwSXw{P6UXT9-7!Ig@gph|oZ+5aKMR3P2$ zD#5rTM6!5U)@`*wtc>I$oD`M_@tRWBjJ?rPVeGYn@KDfD_GGyi!CrdDZJWI!x%x%Z z9hLm>`%$M(QXMz_WL)#0JS?YSGqmxpP?MpX!WdQXX; zn)@xf(}Bk*M<7nO_(FE4_hxAzZ;SaTY_~~vvOQyqn?ch1gWZe1{q^rXLg$CxrIqB) zwiY`*Z77#;!)W~k$)8b$K34l@x+PrvCX&f)nfl7c z3(?|j_o=DI5Q!_8Fy>%~Z*d(cv;Ey#+b=G?HCrjhoB8#$hd!YUm=?1xgLZw=?$avW z1aPOnpVg;&SIXq5nr}#)$mGxtIF%P4gXZPJep^3fL#}p9pLWEjsYhbsfAN(*#-6W8 zXf+9flImZ}Z;|1%b>y=dHyeU+)l%7vU=-^>ghGwLD3QwzKc3kYLLEB-DO;v%`@282 z^LqDYxf>L9b)gReQ7*5mh{&G^Bn3i>xT~b#0F+#COEVJtVCIOL z`wf-|lv(nqCaki=t!f-+TT8QR(Jh2fN4g|V-W~iUG`@DDg{LFaq;B2ZnT3M=I8=R7L`KUWNo%;b0Fr-ukCTTNvNT-cU!F^JlipL=^L4;;Fxh)QVd27vMTo z9xQnrMYQh;h(wr>QAhOtN*NnD876SE(exVGcngP@!%@A+pkE%s?3`t4^rVP3!_D!c zTfu;y>#9E|&;rC!uQSXG^XN1^^q5R%+?30OjYq09H61l`-4_$qV6?l&C4Fk5|12_4 zSF73Nt8o)@X;C!;{5m~cG%M$sT}y%dgs5FtP0f3U(UxFwsx=CRcFk6fv6tSahh(~P z(7HR{7L~d@RW2zlLsM*`-A1nY9*Jm8we;VC!6MZo zwhl&~`yz-tYPm#v9zS*VGHDp{TFZTdBD#q3=0P!EcE9i_f*t*uBWnMjmtKpGWbO4T zs7Ar9I$+sSX40nx5LhTS`>dWqls%en?}^WAi**SU?b=RCE(-bVg2^SvV7E@XUu### z8FjL_K6-PHMZ^4T#Mb||$mJp)Ju{a`g(z(?zWYOex1%p5%hCzjh@TCDMoAg>P*zFy zWLNay{hqe1!e|cig$_NV)g#TP3`Gfc$JX!VR9D@9DCT#ZH@Js+(T8na0`u9I^P$vV zE=)&Ps4YwagEQgmm?aQ1vX|MemiG3#koEF8!yCNU#d9(v)D@`ECz$+Ihl{uEuUp?i zc@MH><4}paT&s5WjM9ESHklw4!_-BgT8LlU%RsQE<}563a#urtSA`2#f?WF2(UACo z6c*wGJ(<0xs7W0G%7Z8+PXTXjE*I*?M`v{6Ugm7Go@ng_({$=Km%N0q=B!XJ`%Q4T zGe&~{A{={O_x|}F-r@K0OBQ0j9@j%PwtgdtHl6&$G*&vKZ6>Hl zevzCkKAFm`;FYwgP?A+1_pV4VNfJjEwSy}eP}fy70th44X)>#?u3AzA*E&tVx0iF+ z6xf{V@9P*jNFoX#vgbsI0Oz{Z`}W99B!9fb_0MQIEht%mUjN|YeKo-UG&=g#R##6c z!pL4d1s3rn|O;2mMu#T5GN41-ErYfEjUWI@>$TLc9$E&Vp|e)V$W-1Mk|07 zbOrl8>snWbcl1J4%4;xOr7W1qqfi+GW=7?Iy zJThgEe@ql74l_V#J1R=bSVrOW!5{*%QL>zUdkGy*?aKA=-M`?9l#Rm?vK$JJ>HBjc zT^xIb5e!qcnxY)7|A8u8uFomwd6fz5EHDEd%#~jND^WdnLv(m!BAT#i_c)_CZf@}BTptR$VHIN!a8CG1DRvtd))#V;c{tdCa{I#l|`)k0#4 z6Y4=20iPu53cgYvaLvWmeQs|#mm!h9*HftdFkQ|O-F;~%TjNx8qHDMEyzxfe{0_v2 zPl584UgOiApSRfY@n3jy?x=rlB2wzI|GD8h&%s1mOWUfI@cZ)II7g7zT}NNnGe7WL znTu8r)uNBKsGZ)3G=dk&$;C>KwYYC(qGxr8?l;-sS)Bn9ddr#wQ`Gu&E4vBX@YOkR z?W}o7jYc5Il!fQ3W1M%{9WRxQnW-A4B+mAg)h5+$&#cTC_F#AQX79M=5E|)yw&aM_ zQyfBMS5)fgJPKz?H#7gYv^%`Qb1N}2$zW#W0s)j zukdVx-LY%S#uwEhvVSc5*sSkuXucqt9;bm$`gEeON zEsc3?QlOG0Cv-B8tgJRIXRDwvPr3+X_n z*bi@e>{YJA`ft>z&~iBOUk7f&=$3eukr5vGtRyE3OJHy9A2v$1vVVXZ8k-0Lp#1S9^cum~> z-kW173Ulb(3>pn(7A2VN#-yKhR)1Mlshw0dxGe}eBR%2&GF+9}tgBvhh~V~&=ix`7 zLeu3)8bA*t@g^Y%T@|#!lP3GV985JL(4T%YjCzbhI3zlF6Jccms#LA|RMt#hP2m)Qb| zz_U&((OAlX(IL)x`Qs}lRk*{ocoG7d2M~Ob*xD?wYG1=+a~eoH3}5P#;{l;j+io36 zSeEl!9Xgix6r6&l0vQm}2k;J|i!`di;z6OIM@?4<1U9K zzKIE5h86u3FRLH@M|^!Op1tMXpq5VOrn(5$5?TRd@0miO>=YI1j9g@s3%A2u(+v1q zGb#^zNo>{!5YuoE5ZIg5bZBDbM3@sVg|*XU@Imk2RFKh_f02N^Zn96wXzq^$iv7j! zaITBB&cf3lBay+{Y8p8?dX!=vpHWw3G6XdIxCcHz{uE;|NTHyg+6BBzP)k6ddp9wQ zHhWc|*(tYYblm&rYZ=IGXzW@k{aw{t>%3)-(PJ^FNI&&8xx%#T(!S|SBuSJS&^`Fo zwI#qJ2psSxe0$ROn7$^14uczci$#YY#Mgn^*XK6w0-fn}*-vEyEKtAi7Dzd#Tj}

ISeXtF#KlaM~htXjw4nf}t{CGN8u zsnq-SY?H-EQr~nn7OWI(%!-;a2E!}lCQD00S-8~Ul^~jurMUMLuHYLR43jXF(5%w_ zJy#qbr%pzU$hFWP_&g5T)R9}*rKJPNb-sua=mgnKzSu5Cqt`QpXOF{-?Fr#;2^&Le zku3xqj3FU#KX!a_y=IZF>Av||%!G`lWXhda6b|w`t|=hFs0YCRP_Og%4>uWn6sZ@p z26?cz)m~0ySG>R&hAODcpZ~zbPu>eW9|SWtEO#pQv|G>Mbs7Vagxp374VNe&%Bc9X zDq>(S=C@v{F(1diGdV5LZyzmV3GiNJoh~b_LFikRNVAp@P}-i#sa2^+7#*pTj%Hw# zl!HG=CUb=0c&KiFUfFUAJ}r`>6WmgKkQ{{x{U@^j%k|kpd{gvvMMk<5Wq8QLGjVX2 zM66lqAk`uLMQ9?h%dn~`U14T1#St|T^Q8C!?-LA1Yl~a%9tRf2@&hS0hl(Run@jgz zA_2i}=RO4G@*q+@{dCWsM!&nbzY?qXof33IaF!CKOJx^H&ZN1S}2Ax~m}W&&6}pZi`jnil2HV)WIl@QXkRM zNree`dOBP5lQaYh6t&@bGS~(ZJ4n%3QG{(tV$4(n3rkBVQ>ftgeJqv^k!230T~mHV zLZdQ^O(hHdA0(y@RCLjhl+uop&n4Y;XNGQ8_2uxkadSoZ^1&?S9M9%&eHNNJdgLn| zZZ6nehDPSorl#4tKNViY!zp5D*k^)b1_5#6yw20(w8Sm7M}rt=wUm2I2rrfx_y}_S z-SwxIazd+sJwgq>dVvd{1#;a3gxE2o1q3|-l3uU2e)9=U00r!YM41@)1OlnZd}62o z``=Sa%oHtWw4_#XImpRwM!XHh?N6WL>M`NN8I6`Hh#s~QvN2)ez03@OIHMBLMH{WG z)W7(rK=r~?6=oanpW9@?J?Nr(i%vOKE;$KZ5;+<d*vFph5A9l0T{_gWms6-` z8rm_m1%$1WobIujN>Ii)5GHpI;;0KW;Q7|Mu(ez^evl~lu2r3$=4~>m>D-vaN)(~g z21J0U!*U>|U$@WFvDqkjJIi{6GMmpvTo0=<-~;FffZf= z<2XPVs$-%cf(o>og}jND-ZSyi>~;smlGMdBiU&n`}Aijer{uOWDFZO%7jFaLP+V!(g~KF zp)SP9_`0b=)@Ge4?}B4Qn)b_b?3p_E66PPI0a&F;WPI@g5D(R0PcSc$PIvu<;SP1W zG#X8ev1BoPAwj~F$1q#2az8onnRqjM08Lb8^i)&D1I5lk;(T3c*lnuz`;@+(j?$Tm zG46gDsuk{autr?YdbxAkn5$@VVR#tWfs=7>wu(VTYrZu}kW=r%$pEKip0|4{i!`*X z5t2IXy2j0fr8uNP`Kl=k^;^)>R$Vz*7efhq4w)aLMOX^T%|b6`@TbQzd#95QH0w_g z)%K09s%Fr-%TJC6MdM#|_i5agAQ`fIlybGudopW8^bMBf-J9^+AR$Ptx@94{`+$Xi z?GU#W7QigyhtPqiY#j^2JiWxn)C3(;hMxCkO{;l(vJ@l@@h^M%SFPPX?+dCYfw5>F}r&7 z;f?bAkVe+Zem&8g>e$1Eiu06Z)g1gVWsq}D?asa4w`C@#aIkgQVQn{p+L!~s__@2P zT+y4UI2~b9)|8oGrhiAD^wC;f_RZr!RRz8yQ!^D2SOgb$1WEgh^NPd~?54x!I&=8*4W%9w1{}%*l4)WB6(9I0o- zV4ZIShTl%H`d<&ha8nN;1y-0qB9WH#&-2UV^+{}qSUf>X7!7U}x;o-;@yf^znXcX% zwITWK!lzY4g!j~YcCSl1&hvkKW3dyzR#6XB*3#& z9_}@%mZ5yHv0tMsDg{{u_f6~$q;5`Z%{sa%BdMj}vt zb4$)-$&0M{@t6KYm$wilk)+FHMGjJj2z?fX=H)w2*FpjulZrYl5O?>TSr~I|QQ5v` z(MKg$nG7^cCe?=JVwTI09pXU71NOF#a&~xh;?(^$15q-cF-HJL{&U4XPt!oIBJooz z?bI5G3;;%^0`QOW7L0p)5*6p&s^9w3^la**&O}Vl$u1YOnc-650u~|lJN#t$!zEeU zu!}%(vrtb~OXzDu*M5%+HwZV;f?|@A?~S=0uQ&7STP;npKQfR-D95S==8AwhZ-p?x zxT|s)3=A;lXV3z-Eoh3pk5Z@Dp^!+&CZj$*$Zmz4kk5>YC2?x%N2=_i*C_HzBRr2+ zVh1|`3(pWoVy$l#PU>ggI|Mk|Q6@_$y7LfRdWmXjs9Mk_yA0mbYPbwy#7A&6cwGnG zgcA}3C{T6^Reh85#<{)bJtXF?7$2S~v$(aZMxcB7<}Yi`A#rStjiFlE5C6?w2~ZII zE$_2<2vDOKjg(2tD93T4lJ}F}hP9N-hmgH={M1mmSlvBo6f(|$SKEi$aZpTGwnG^e zMa_gFcj1pZh7!q;l0%)WU2w%9V#B@+igvp2I8z>{^~mq2-ub#@d+qn9Px|)s$cQ1F zr#K9W7crRUmQ<%k%p@;F;zyTeh5}XZe-su|Q{s z*_K|wIX5w4bPQpTMQ{Z>D<2C#%~KI%s>O|!ya{QW$eg9YHFv%ECwTw!`gQEbputx;O?nS|9 zt&${sLjo$boEy<$`R}Z^w8b7TwUAgYz>owQ>^wXI^>D=NF*>{gX=ZxRHNZ;r71@#1 z;hzS`M8JliPpi`>aWj_#sg-2vd6@q?~mH4rrH-SHKw5A$xMHyF3+9hux4X7e5$u;*;&|e*zsE2 zB!-ZA7LvN!qFcZjh(q&oWGa^Vv#&dXmjEILB8c^&hKg@Wc`_EGb2Mm3M~oY!exPkp zZ9`^NA-F%-DA|~~!X=nw;6-bOjoEExnfq=HO%qR|)1$uRpioCUnCo#$pN;;b_Wzi4 zyCh`0E7^U2kpccL!fSX2o!`~&$N_f{KVRYPWxjSFHrYv~*rZsGy0e4n2fcZxoW+&1 z-DDa6D-jGFB$Uf*mF+9)toTLFaF|niCB0ZF?0vcKYPPV?Rlo2Y1xI+=8Ga7UI{DM6 zj4KVx=by^0`9$msAcs8Xu#vD5dO< zl@57*fLhdE9F ziD6;ka(SwIVwmHZ8N8YFW+=^RwZMxXS+sPYU2Lqq!bxF-h?$o0g@m#w?W?ykTNY{^ zNnm=u*~=o|jA9TjS(!HSBcG02)3rVs+&D0rS$(S~Vl_E%>bxC4Z9CV2EBZHVb+y^O zYQYXoq`|p*Mm^ZQDTZ~EqkEvwKXq->6+ioF<-I~b`b5fp^rTt!wYzr7On1qm2{SiN z`LAcaSW02l{{dt`o4?JM&407hI6`_Ng!CF)RZIS@zq2$(v8u?0Lt2TUS;eV>M)?v; z#6a15(J0d2j#8XRSqF=s_!Xn2GS_PZ$&s2o$E{7K&7+y-tCQB5*J{?#{|1%57!x}e zCkX70A3iC>02yyiD8^ELFnyC6Y==`WKrAM!&wW{noJCw<1jQ$e5qvWLfK(6flP_J= zNaWB0^Yhu$56PY5vL&=@u|D>sKCZwqpaj^HEAq9!tN*@VGDag^mrs4!k|CW$znp98 zhe5sg$sTE5!;)`ZVM)?kh4H9i*;yD@HDh45{`A4aAOCc^c);WI&l4ZA_z%$nrH z!Z+18bTzY#NZ9#-;eDH%=@c}-N0C5(JI0*Ef&?u)SzIAxSY6NJV`3E^6{{#utb)V( zeZGDjH^zc7h4qAaW=gSK)JJn3#LkDDs?+xu+A_L>C0QrM6{x`e=D(CG-YOZRiC)6z zzZQ}{C1piaSO4HTOJhfor$5Wwp#JuOEuD4ET{9$!H9>LOy3vGBpRhc-+DTh-ckGfT z(R_*;UhzIXNe`{iV6nP$(p9N0c6NQSt0)s#1;f*mfUgpFo#!HF#c2}V8<9lp!d1lJ z-ajWfF?viSQ>H*nv7S(we^C>;>81|i!MA532>e)y0Kbf>lLn1T*KIKWMw$-^@xHo} zu?7{g!ZNIDmRLvDtI-_M)8m*1ahgmn%ySGT3HGUNy{ATV<1ov(R1xImJI2w;gKqbi zZ`d+zRptx5k*>WL9|=j%m^him_p@7>yq@_9MFXZEzG%y!j+V6c*lMF|OHE%KuuI-i z3DofFs-0Lxv?9(g^`1+C(+bgWG$h?Yt^hyQujU< zq%U6naIGfnldnNsLAubsr8&l%GzaBZ{h4pZ1ciB0?^1+?^8=`l3k!=RJ5(gyTBsYf zb+62tohzd|>q%2^NS16kD*1a)NVPqy#Vc0ruw~|2k^9G6WrFHUS<+1Z}z6B4?Gq& z#`E@kTfXyrSwm{oP~_H;nX#waHDq~Zwzm(Ef71Ev5fFR2iM{4(2t2r?jEzX(w$YZf zX^5NbxYppnQCk{D-Pc|kJ9bk8oUu0MJ=%CNMutV4S899d`FH>=>F ztle=^x;JM#OnnoZ=<_YyXd53pf+FG3x{`S8iqz3DeI;HYCIR!i3!T#_ zk^9Et_)HftyQW(#(D(KG_atq!(FOSUgCV(NjBY}j(wVugwCA!z5BU!k+w$5RjV)sC zb6fq}=as0qX}Cji&si^NgA?$!Q7NeMC=?V~=6cmHN!83*Z<$yw8D^_7KwdP{l-&uv z^#|3RUbso5WkXAbX?k+YKF8R0>t`O>-ID0gr-{aC>(w!S@|-1 z>0aqxf19qsRgPbg7O8iGKw~IeI>?NG;LekOfj#47Ik8__ zDlssB)e#w7%O4FCEZe?3F)f`SUDochrKw)fhPtCq=wrzfpV%^>gRL%vtt09kcne&EeoVLm#OF$@;n0z><_ajG7n^$#1 z7}-^1x>4UIwwJ#eW(jaOLaj##WXB*eAeu{i6 z23xAvriKaR60H*@?jJcbWfbkI`eaobi(Rv|)T~z9jNf@z8ROe8Pj3vXcVjAfQ`X6Y z&Pl^D8ZPaBU}Jg5-3O(qz{T0BiO>#8RZmmm?w)bZ&sm7It2=Sfk8eDEBoPbbQ>e|*xl5fR@Y@zRIZ?3a;${<|mtlyLL8rS3SkfjOf>e8%wGtDj5kdLRW zl#9o7cf}xb>b8GO&Z=hF;p=l4tevJv1?SY}>zsvu3VwLnfiz;gC4CMLMaG z9?_F>&L=BnR3A+`*J!0*&Gi&(cq$K;2xZ@n5&+imU23)a^uN2!f5qeUJa&!4dJ%W3 z?Y*NXLU*O{KUG>cU4x=?6YfqOJz2)bR3F!>Zp-asLh_Qbd)E$gUi;rOY-50(V>GX| zsySb+P8Ew_RL1(K7D~7MJy@heI+viG=YJecnk+;y*V({n=|_ zdEpV7tCanHCW+nc`#D)e+Pf_K;dG!nqD~Emp-W^eFY3Mc!r0XJ9U^AK-qQpHz%cJu za)!JX?)=q*^>y^#-8jNBCn<)K$Q!Fy({UfuLH~B@md++cbIf>O251#)w2*N1mg*Mj zZkAoPnxKlIs0(v6KvS3Pgc{F@eGdDUA!tSLvC_s5U+jFKnHqo+un+E(zCpE(Q(W7! zz9k5Z@wZ=`X`7w!Rt=^byJ#bgYx0&dWnaC-xhc3gOO2C{sh?5JRE^s}~(Jjnah* zJGD0rI)9VqG1@j!rYDYies{_9apdDuqiAqrNMNSAf|v9?y}RMM)Nv8#vGBHd$ny~+ zy`nKvI)=na8|%(2)Wq(?PlOX*%Sa6Ml}p_(BFt`88WTs!ImJ6ASqF30ao@!?Aed&Z ziN665SB>jw>B(i8le>o+DU)#KZ%n*l3{Y2Z_FC}_NR!!J!K zan%0|2VwCRTUk8I7|CU7j8jez6E}ubyIOi~<2wsVUoaApwOlIlH@Pgah=^AE zBpCz5iS)?Rb{Y8)G00p0<&X`WFcH@+8q<_byz|e=4T>1b?FqG88M+f5)DWmF(<bBo!9CP%SbESmo)(a^A#lZ@??#_%^PS1*rII^eqX zY3J@j=BKD}!jPW%vikQ)YUOZu;~5es(qo#C?H)(SOguNS@dU0S-k31cgKi?Oh zl)y!Flh{3~pNlxeZ4u1|-6${YQ~H{1-S5bOyiB;{B;%d}PTK_=9JNEVi@58MZM>gJ zOXEKW?%b1~gmu#im2uTOHg%Huwc~gmc^&oZzc4QGA|!vq z1#TQ81EGQxCWvON1d6d>!RBawB364gxUGES4& zRWh?=i&FJ}4JW0ze{xg&SmtvzvitrXE+y`bfD(*{ zp0LXp671!Fly*n^mIx8mc`{%aZaZD$8pSf7%eOk}aS!vPPl1zz5P;e$yq3*-oqzL1 ze0!}f_{jJ1zEEY1>0z0*@yYU-LD=d#?vK&CZre`|Oi%j@{V^oq*_aL;wT;~67R}|j zb+Mqe_vXgGkryi?^VR~6@YBEFs4f>5C6TKDQGmkR>rF#!880uvQQy=bZa9I6hGfM3_0Cy?b{8mGZ7!XYs{? zs?)HCY`NP~@44#}7y1D<3tXFcC)5IsL+^jg395;&IFVwD0$~fE`f?~~Y=#lKF(iP1 z)NN`35++&>9=jXk&aK&fItK{`w8vwx&9&UP z&$}z?2A+%wYsWCWlsN-#5Em~I-_P%ni2O(Sef(o|UF0ba=O=zV{(4&vjaFyt!XDQn z{sqL%Z0KQg=+e@1-OnywpyJQ)*e>}qKafast>e=^ko7jK|ux8Gf8o4TW+T`c1(W2he{NU(0)TQ)Ju;`+yzLPp4&2=6bB zUr?9~tv%$0D2r)cat)yPlX()+Sh3wU$_NzlE4Mo-2_!$U_n5WDV_T&+OwxEf*9^8y zX-$#F*Qm`Y3~f-$KY>D-*Zkpa4SwqO!Ohk0d~2R9ecQX=nIk};h}gAf5+wkE^pO*m zSv(z@Iz@l^1(w-SFdph#BA&&jy-uPy*@B7y{T&7xq3*eHD4BO2auzGD5TpV<5xCUN z>Zg8AlF94bcknfS{9HBuiG36Aw5U9&X_~(w@$=xOKJic}sq4du^tGq$G6jXnz$gCx zbs-On*HN?JhT$cgEVR_X9OLCWGXx=ejd0ER{D<=`xqP5QQowk=s~aYs0k7Mt;psm> z#eUV0kh%9>+vvbC7}T5(!vorqNu)cxMc_k7#L(p+`MFN!18UjDL`AjlUf_g z=&owlunqe|Z4Fd3E(U%!?{C&xcRSa_KF&h$0&XNM-@m?^I3ENlvEzU(Z!akOd#?E3 z9tg?64vAmGON2f7@9aI+XfFsGtFM#fLWKiuI+pUQIy*Eeg@=9HIz$92;!FBDe}|!< zxFAW4{QlSp+azdjA7hzQ4T{u}43&QU#wEEK7{q;t-F+r}`Rdu;Y*CN5%aob?c!m)=4onsY;Z~$%CIhux4>1yO>|tv)7Ox8 zAb`whtof_m9oj8zF?cNJ4TxTBCI7iLgj zmk4RuXKyuz+BFavdfS=1wQ@?N-2iD4y&j?$E5yf5Bb+h`vi`jOjuN3OgvI<#>~@xM z4=?KD?6&`XeaPgqP+i2+-r-V4S7RP?*})YV)4-0B7GNvm#Zb6S-+9hX`W_%s9(=rP z9x9y}DFT+&6@$DjE|TiFk;xi$LQK3h$2M9wPJZf|@GK_u5!1Ma4e2U#G)j1*}i2U@w^zc`0`jjLH6Nc*rGs_qy&#sL>PwNo!l(k=!4Z*yTj}dK|H=Ti`Me_eej#szx$8Ntrtv-Hg>vI1pPShKt3? zN}LpN(IcnQbJJ`Y5#H#D}jcT^>V*G|D0wNk5?F4d^6+ zqCx82feYP6>DqZQ-HF!djq2IP=_x=t^Z&kWo8mjzrP|e<$04@7ZfNSZT`bJ#?ybaX z(7cUmr+$B^J1E`G&6l(Q0YXqt&hCrAOVJuW`1Px8xb~L3F*{viVkh>tZn7;b-iBToWKV49IBPv z0xhI41~(O+xP{T6kHm_t;tIqVcaHPUY(@i8;{G_w1`O=kuUMvI5Tr!BJk+P^20xHt zV@f{50~5S`fKT*w$M=ctzaG2tscDg}s~{ zlpDaLeD1Nw^$i-h(xVzSs7lOHfG#bfyTbp!cBs3CitOm|PSkoe-S4U_bQ)s%g+v8~ zUJL!)fZ(3e47Whegow9$Om`%xaNK=VEFIFA!%Y0brW{GClOJM(WP^?X=6HpVM8tp& zPR|QJ;TzlDu8Ej9XyK;RZ;+EE+cPPzF4Y)ztgwlXVxJEd=zzzaX7fI$#aApsmbZ(H z==a(o{w#90Q!p|GyYsiN3Z-o57>&uww$W-UIu2U5%hq&-qt~pH=IpS^OUo+xIf;g7 zlJ(iooqGY>x5<4A@suD0pb)aaM`FX*)D>-R$@H&sL777+tl24^K`j#Pl8a(a6sYf2 zJG~hJ#Ye_bPQDrw7nHzr*1FSW$caD}->upp z8c?M8KAep?{1F-@29**DeqmD9yn>|h$q160$cZFOxu}p^Yo`QiQ`B|gIyr>K*YYVs2Q7C&0ht(nj8K811+RfycG zzVnOIQ}3W;%f03gNW9WurBidKg>D3#3J?@tp3hFul?f_($01u!S=&>0RE+%5o4l)> z9Y3G@efdCR22raD0K}B2uj4RsTMjJ;?!Y$uV4}*QVmQf$D;OT8PbB%MtkU*xv zWv|xmo{L?Pj;mXDS>;!R{NBp>5FE*{A2w`=N5>L|*vmuIpm80i4w;FWL1 zmm;MCU4A&79-FMJ+c1X>OBdv#e*dnM!obC210-KY0beZ0KViJ2KXBz~bwDVQNRaC$ z&lC;hYIsPUnr9yKmdnWmxM9e6qFeo<-u>6q(|z;EA?3T2?|}8YY;!?r8qcZ3^Kc`O z)sri!<)Q5t3{sPr%se-4T8TD}tUZf{o5qRYWgr;JjmI8vpU6%I+CuwrB0;m$@V{P~ zZs@A*&dK=OFF39dz0lL$(P`O5`|MxcW*N;TX^-iv#gyOVMpYLdJz<+Jy+ifAN2FWG zm-Zg^&(&iMxK-q&Lb;qw5#UPDgr41kQ;EfndqAIPc3Lvp)Wy*IE>+JuVlYNG;y)ML zmJC<_nda=KYv(#KV`gCVI=jsIPd3JFC~` zxSNg_0gCET-5s@io)4!=CZLk}w|7~dO7o*ZPX*jE7)wqNybk1BxSy~RG|NerxMstv z5=Ah)x5()jL{195CCUd9vk0K4tQ3<1L8kVBK91%n(Y0eRnf#t|jd+`t*OLjr5;m`Y zdMZk~C|6?wxC5Uoa|T(F|KSZ&gA~%M|LSa~Efmc(5=lkqDl=(`Gjoa4frSALAF1}m zg=FC}KIY(2C#ZA-%#xbb9g3ElMp-+C?BynLs?o(3 zBab&{sl35^CpSgUz1nHe*0^JAB3xf$n`p+4{tp<#j)P7UNcxS}kD1A}ez#LfLPuV% zA#NP*^~LgzhSsSiOV^qy|Sb_4v#liUhtaG+Z-coqv2}n>NLfk_7TUP{bz-oH*#d@y^eWorurAVOZ4k zfBC7f+&scva7{tlR1JXX6-T{GY(^h;kf1vv(uAgknno+=g$5>x{M5six; zjMU_QUpqLlCqhwRvBpIYO?1Au)eBj*!a++UMcZ#G?yrMWBN>4E#f40 zA}$ne1B*V|-({gK0PX&a{5pj)D7_l8wBHTXi0TK*+M>7rb$>NJXE_rswdHdddjv) z^E3jCIc0JLYLnVYj1l4pb`PO0yE?t@_~Miz|HMf|tQ0xPaoK+Nj%`ZS2gWoV#Q^?2 zLrkqxGC2|(949RdF`p4(bN5zdpDP_>;yrHKmtMQ=2+GyXV|Z3!_;e!^c1oz`sjJJj zCkFHqzeg7uyBjAAMPm`MC$3+e8mUW;IRR)vMen@`-AU(Hv|}V4Zy)WPF7d`e#-d>@ ztIX@MThq(JOUXhkc)lw&mb!cZ^u8qvbeH`|Sx?je1jDH)b=+98_Y9BY6z1Rcd`K35 zrk|^Zl0mnOl!UG$n_UXmfEs^JkH~RP-ePojjZ>yLiw-yCg{jW;x(Hiy@TeU(rx6R1y3A*d z9_GcyK+m{Ty(ioN_p((Z=j$5N$IagVLMF^-eR1C~7-T=cbMC^QCl$5=_LSdUGI#CqEdi~yAh zwQsoY-CIgVu#Udn(O>Co320=JcRaBipBRb7IJ`^p3xNwkhZ;^7g6cZeOFy0|89NR+ z<4{=O+#G_`x-~QgTSSvbf>oNI7mQ^%Mz>K<2^ys!;*1gviQgJ$UNzH zw!pn8E_RybM8F~?IZ6zRJ3O>YW=%+Pjyg^Et5>m2(I9QwU|$W}dC+N~(yNX8YsmA> zSnH%%qI-)oNP?6{n?}k0)@eiu%LprpLfwa)pj|G;#7_5u9`ARLOU`^jXZL7nnUg3L zV8I{`s_664Dk>3HYARL;l6Z>ldIyMdYvDY0(QA5AU;$hVeV)u3i&e4>rl4sMhT3IL(v$!B!-HM z5D)chIKeR)^Nae5eC0CBOw+h>kV70WgQs&dcQ*|mKG7^DSU%zbc2 zy#kh$cmbvS6Q4RI-8T%gG^X>jaMRNU$YY_3q`xc|tK;=xRZ-0hTz zkkh1N6$8K_I(#-H1K*8lKkQUEbt4mi56+EyX{P(bk$!DZs|Sxd?UZ9F=En6P98N!9 z%lcww_K>Y7kAu;H{L${R- znh2H4@!`Ptr-rG!)C1E+PJ;Z)-zIyz!1U0&Np~6CEy#>W{ShQX?7Z1ttsOFgPDEZ2 zj;RZGhHBZS@|R$pyhp@%APFAPlaPKD>voq*D4G#yi-uP6@sCMXTc)Uyo`R8l5T_Gk zQu%y-f|G->oV01m~U``o`2BVRK5B|JEJl|NkYma za^m%A<8)R^2gE)ww$VB|Vy$(%o&4bygPi81)z65wr!;&urn&3W&%Yqofg(;eiarrY z*22iNYG4`N7fesA5||V4o(n{N{+c)SLC5B&n;2l}khfa4E&VrY3J7O7tXXVHKZ}2TnkDG# zcRQlyXmAfGr*GnO%D^jpGbY?D7?ezWQpA+#@5pPU1CIL}hNB#}Oi^EK>;h4M-^_4o zx8(!v8aoQ|J<9Cu$g$!Uvq7Z166aKw9xFixkmgjml@PxLbh-ws2hGJXW! z<9dqxON0B1dV8A}6alUu{`B#aPM(NGM3a47Ph>kJcgb(KHG14OZu)FJb+1X#G=L;;;_EAo!*gM9gr!uImugz3)TpLsbEM#1OJ!FJ%q(~p9`CULzE6kqpln3{Cn$urwPrL{f>H`IYQA5 zM#z=b^C((nA%CH*M#7RWEvuI=H-;|v2dE1(fqt2i=5}oavdLL%YfP?ie zdvqrQhF!IalVI;!(hK8QNjVCY0Mr_xZiptI_&s{JoTEX<>Tef zyxa(tI;OjmT*bwBHc96p;w@qrGdf0%V2#TufvCYhU0(Kisnm@mvY9y1t(BADO;mn? z0n;arYskqkM_tW#<~!_MkxTM~1ow_Z{|~n1yw%%bw&blvy0ysbb5u$}u187!?;2cWT;7XCZj4XkTgO1WY8gy=D zA>l?cFgY3M_H^|SXR1W6lF|dz3uOLN#T{88+jDG^2voMBaCu{{$nvcWF|(y}v$Bd3 zm{Jtok&_(ujeGI+XToL;&=Ve64{O7%cs5x_qKblYFAg*>5^_$Mh^xRfIb1NebS(vC z(^ff)s$q&zFWf{F5x5wpAhB8UVnT9KXvL_iwL(q})9PK0TM|9l&zWB5?hOfX?FKAJ0^1K3CvOeL?;t;jjo3vGx){Vp*#Rwb zQKIkGJks+@1#_yAfTjlmt{BhhrLP)>F`|pp-vSaN?|5PYgGB}Vi8>*kdzc6qCMNR8-h+Nn{x z;ufeV%42M%-#81R3uj6klqoyr6Dz~Y5`|1%Nh&;~yWmrwIX&`XcX`BiUqL4sQLV+DrIi$ys&bFzXjr(3p zMyb;G5W`C~7n#?~t|DotTFxZOtvJvvASTeeLElo&5N#wVy*X`g!x$EgTUJ=)xHi`f zJAD&q%+jrR z(o(0>XS*EaWLC=cab_~nhg^kqd5)Xn( zSi-Mg9x}VNms}6Ixi4ta^PivEGPHB%jsvGXc_5S_<9azv910Vz5tAhv)Jp#M_^~dNpvvNMbVkl|&-DKRYDJ*qYTH!R+S!PAo;OKYi9n`mxg|>L*vP7Nmh)h5_WX zfYg||&Y3}ro8tzsbewLKARxDCTG!iY!0EYuk2Bt;UxqRo=z3anp{U?QAbGrIms4>X z?fP+zm{H6r4vmeAxaELzdNbi31LWFAlD3_rh#8EFies;?B!tc3{IaJn@3&X0(=Dvs zAUK&~fXl#5CWgYbz@4D?o|~Igv!tG?^{Jf1=^Mm_W?>R@LASEwK}R<%iN`R+8LJ)I zH@4L;zrSIa$Wxy>3#@Yrb`MQon-X`Gn2VF3S8MSE;>a;QoL<;SjEJMMUAjB5;O{s! z*y$txJ$l?Wlgt4m0!$!o8gb-bCfV}eH(1JSaw=JJ`?%Dv0XsV~jd`}HAezRR%)usf zVK1pPsBkA!1PeB)uiwdO6-7KnGnM=TCr6PkILS!-k@I3_=D9Hvx+oEkk(g!2MX(xC zlx3^yh&r0x_|xS~$qSk0DRik#miK_>CRT}T0JUqtUb-Kj6AeB$J=r7kT59J~> zx`~dN$o~;5_Vn2QJ2;u*AiDIN;1q2d7ads&u3v`?I&VgU_~`V|F6YK|Xkwf0JSU2r zesM5?lynd?kaWgnV(`LEAy6St+)wH1bI z{(47+&Itsij+%j<8o#`~v>Jg}>KCuu@`G#L31vT7W_z9@2;qYZPP3cXv8Na~pR+RI zj3#Z}f}~*0s$~OjG-?#$(Lx!Ce>!?UH(=3oGkdfYvp`TlBSE^;^h>bRuq=~5+N|!^ zuivx{g^#o+x`LP3g!-n}HU1zrp-8V^hW!X&1nGN0GWoE_nY$5Ouk=4;$huvQ68+Iz zER%eVrS9o4#lVFEm#v%8>3OL$LX$XUxg@M-;)F?Oa>v<3g*=(aJtXRG#G+=fF)jr; z512I)lYVy8;WZ!`mTYyD-c0XcW%tafT%T{N`bSD3VAY-$Ea5$`Nf1hz?Vm-MmL@1+x*Qv_tf2*p4!9(BtN?+ zD3vBAx@CkDi?nIPj8E*%k2yOzhKsa` zo(i$9ml*<=Kur$C%qjWw6is3eaM-*=h6DqOoC+d=(~9ONv8}XXL1V|Eoy9fYU+4^u zOH@*_?V!_IIUTIpp1iE4JW(M}CR`}DeV-|;dvKy<7AUfQf_^_lokkC+F412r4TY%S$3%sKZK_k zDXw;H4^_Z0hOnHmM1`X#baI3+uI(Co+Z=f`e+PA6=PbF0XI)Md56D!QuHdyol%1PG zKzd2zi>EuEC<4Z-Job?oIZ)FCiCPSVg^miI-VUfX5SGR}J)!~KjT#|j*@j^0mp~=K ztR&(aYO`{WS?000R3vseiOA7J=<+#gq{B;EIWrgQ)!9y&2f4V2Obe9>rR7y)DE5fj zo-&Wv0U;RflsJ<=S~PSL<|tO$yI}FAuc5uQB_m?!bBvo!hH7L)$DH~#hy z6T@i~DSy0I$}o+z-Wnb=aSyi?b^Q0QP28-6lLIA4tP3@Ll{0lD6^MiLPg0Fr_II4( zoSXn-x@u4;?3bE`Q)8Ids(Nla8j4Tov!{9aG+K$xZ53Qo6x4fQld4&56Sp ztewh`_#5ho=+?>va~ijlF(mzcs~BR3O(7D8C1c|*(!}$}F(o48g3wl`>d|rFh%Fy3 zp%+K;$8iGIsZlu%2~@bZO{4q{&HQ0Rx? zI4TT!WJ5CG5)t_40{f zO$_#v<-~JgIiWGgukSpq*F&I79w}Z0EE1^b!Pyw<>g|pZdcoLUjd;SZJY)wRN)RtA}zv@C~Az|&0)PZ>&d-OPZ-A8VEFIOhw7#_ z%SjNUON$4Xv@ec#en*2BT9SD+B!GZ;-g>9+dUSziEXYlDXyPdO-GJWc&M4B#iwb8< zbOU9&9Cb*U5)sKjAkK+_ri|$uZJRjm8UZzng+f4R)*+_zjz2dUJsoc1Uo?01(8cka zGg3@QNlBv^6*VH<&sI7m@LwvKDa&IxjX22`ZgNK1P-;T|i|9UeZz-UQS>O-PhXF-P0a{;%M zm@8EY1`Q!GT9T?Xuz}3rG&4N_n>wZVFpZ7sNZLT`TfK^9T0+NSZ?0qXUiw%dM26%B>S-kfOk3j&7Lp%e!R3@p;UK}hbgN~OtVn`rc*5_AA5a94aQ505 z1Ys_m`YypBipI4<7Uo>J*i zsl8MtLyS2^r8-6iQ?~1nWo{5t0&yQ`rb6;{hwlEcMpT!E&QiMi=t-w)Hv-BsAT~I8 zHfu5%MF9Zm;usT8FA_KyrWlNyj&e!W}(2{Hjas4 z(Pn+G=fJ51DtX9P7SPCuC>Idd_2Wy@DlyG5tkAdA?$6LM)hnwL|j>NY9v{4 zAHAH!xL!2-q^9V;@lJggpkKIcmyUzL>y zW3jTxDY>V_;I?vdZ)lD6{8XoZx?4lygkhK%)x#6|l>I%dD&2J>rR0>A~s>VZbq;`$#c4p&->zp#E+xa2x8<@J-Im1iJk7fAbmr^ zWlXniSw)3^F;jo36o5`=m-TBg~G(-P48_6Rr}FAKN0v` z(^uIhhZdPFmGH58%Dy~XY3}CENtEgYL!B_lJg3uXwoe(8cu?#UD#2t6 zFF3E7qnh^gzb8kv#}S|oQeah_AyNEI*y2PYwoXWS49dvYqrWD76w&6qC!Mf`3aPdyYW z_l4^Pv|gUJx_pgkj~WpJ&S*G!Uz))=fK}Un#7XELI_6BCAXYI2i|RV|Zf%|Fc)I_v z)8FszBoz4C-A*3s@&V~3Z)ty=n{Sn>tmQi+G97P$^zev zY2|jQ6u_N1cqIL}DHVt*C~&0Q!;5f}+N6U=ong@14_Zdi!L$kr&}kAv0biX2=hSV>T)?rWb#@};TU1sY`Hgfl;-i6JBGc{ zuo9P(`LXGC?IGb!qZ+qLb6$_RR%}@LCmlrw{RCl1>1#o^*z&9hI9u?|nErZys8miV zlE00v#Qec^A>*ci3(jIg_J-xC&qFOhi9B^Y7z>K+^?RHchm!^>ouL5ambf-`t4mtV zVS?l)m4pG(H`LGEv$?YS8m9dLpA25Es!xnzR53@oH72T$#+Xi3(YQ zQQn|$T}0iGDKMD4mZBTB|)Qasncefjsxrrd$j3Pimmk(sOXwadg8i4F~2 zhIT%?hGbwnF=gIy@f}>IuusRyd1*{&*PNEW@mHKl#2&Owp9H91^k|1m;u;DRyQ|R* z>zceK3fG1b_(Y_DZ?GI?#`@i2s)dwd=+~OnmvX^O&H3cyQx;*#)q|b(z$8+kzB4HS zM=eh6*PxD4A(53O3Hy{skEVf1jMs$<_LEcGZp9ZH5Q|KnrZlw71U@Cya~G-dAY zzCbE0xL5JyYj_l`A39CdGBo`6_mo|8lQe04|3Cbv3Vd5WUw+b-cyEtPI}@|_7>J{?7AG^F?wMxI5L(vq%jR z0%ZiTj8{-!M9DcFX1$Icyv{Rlz!i-+UcIlKuN&lz6UQ#ehqdVa=NV+ zhpnVKIIqB%Uz&x7;53wpfc||jndzLGX9_IPM_;k!$xlx&Rfe#kT&I7BX`++ku(1Hc zfsC+A9?k|rJ8Mp^BQd!u)G$kb8}g%HqMh}_+Zy~-G6Xm3n%~-nWg@XglMGsL5y&#E z>;At}`fXMACW9G)n?)P49S0?@VjCp|JL$@u&cg1KMo)!gppOHm91|(%)q$jmAqm_k zcJ7J{-W_Dl>vK-WK_ptK)MaiIW%kj-!Wqj$oz=Dj&i8C&s5#;dyQYWxWj<{rz7V$f zrj!fdqtE$O$n3rh{l%@}A~4krxy|{G2(x!0rfENQNsJKf*2X$(DsJ=wSTY7jKc(&px;N)hP*53?_A&p7bH8nwz1Wx%mxD=v!BcFYwKnetL6R z%Z38}c&U?w4C-P6oa^w-nCNL84~!tW@&9a@v;OS%eF#vwQL~z5T2Olh^N~*$$r*uf z#`Nnq?NjoNXfPXypOb3}5FlOr{fnAxw%x}y`|?EoF*$4$ABoVSzQ~s*TBg6BAMnkX z{x&J`u7Of~Kn7Kt$dy)Zw@uP55n45}O!7F$*_@kjJ34gn7F+(QiP{erCq9u7Tt9qq zNUj{@Z@+W@jOl^+5(oOo^=#|Ro=pdI$9dvoF~f+H^|RhEEo@wYKip%Ta}D7GBnRSE z#*9Apk)tSy2c%SVnSJo0kWosMN6l77d8XKwB($13deS*T_&3SPn$;~MElTIT(?RsB zPle^VuZ5&RiV|#G$-hswP1D2gUK1Qo=L2$%*+%^O+*C&{6c3bc&{Lht-h13dr&V>*{X!RcQ=v}M+MXW&{qv}>&CnoBIXLJg_G87g>8+nDH7`pRsPOZq!% z=s&)18=VwQJ(z|-*G?MFuh$)eCic`iefMflFJW3(MGFz_Q~y^Qkt3UvFcb)C&q*L^ zrOh(UVrHyP{5blW-#;N_#^jvo&ar75)5(%Df{1b#;o-%7MV|Re=}*82lH332xL<%K zj)3Y$ufoLtr;K4drNwXV44o6B?*k;pLr)}U<^bUYHB3KwIjmpe3hcwre$|q0EzhC? zf?%IxV#*3z`nPjVq%8-S5oAlFlHS%u>h3MntTm1o!cGkEIX$5YlqMhE&6bf}y?p>G zDVc~z+h5uO|Idw<{PrC+)PQzB=iz1sGQI9;9O?y@l%Bhs3DPzu`USLZ$f6c`b(V8l zP|-N?7xt-!Da1`hC2h>8ilpX*!NV{wz0>UNJg|e968JScL~b78)CL6zP!W)}F;NWo zth#iMs}D;n;Ug(JWD*3@ZIdoIxOcm9Y^A!r8Pr!Uai*MO$WJ`v{Oy0f5jtn5Dg-!< zbQKV(z$(h}DV35@o{84Mf4tX{X!b1L$Bq0JW1^=U$+UX4yq`>rYQeq3jqIhbJ-D+a z1KR6J)hS@_&UL)}&C=_vBEq+%?QT73-dX5$yMb&#+; zK3Q(dlV1wSY$Y3D+&)<@W)#b)?jbpL!Zz)uyyBe&V!E3mPVC#($!OQFo&I>q0F?)8 zcR8JH%LBbyr#Ivv7QPtM?p)jCn=bB8OD0hRa>T6+`=Z_MpFa>eu5KSwxC@JHLn7E| z7!%VZB8>()^ET{p{xzUOlTvALy1* z&Nn1*QSMS_1jF~ya+121VfDP8Oqcez%yU((Qu?(6oL_*^Wi7-*bU$(=LX=y?g692` zmsH?kXK$qFPBf?kw74Zdy3Wb4rW4~dsi(}~C${|Rjgs!18Z^@3lVt8$Za^5|45DY7 z_&rBJv@s#Tj`uhWiJ@+O!?z{)t!I-P6Wxg}$rgEjs+gDwNzkb7-j0=jOs0m#GDDD` zn`X-;x;y>lL)*+Px@K^2L) zew)aL>`;W$QoPjztA$Xbtd+8iW38zKiF}|F^Jgq1hJD=Nb-lnWhW-?@Te?z z3*U^1VN|iXBM65`ccR1j4n5B7A$~qM4K@$P@Nf5vEZQQ*fFLy9(+736LiCER!==8^=K$>|7&#fA6-7p>Q%7r zME^`s3c*j=K}L3U61hXWNJdz&*_I!?V9TSoI5T^K<|*d~m{$MpLT8Op+Vi)`wtn$l z*>y)le|nHm=l0PTih3UzIE8M?($NT2l z zI4;S8vYTpK);JRdzoSIO)&sVo$5Yj(^wk*YA0LI~`eAaa>t-D_&c8oknYyUx*P|r< z=danPlEneeJzO!+GL;8!&$H#Tl};xt;_n*O9Ao46cs!B*iqCz`lG$r5nYqR`hDGeo zM&934Yg^L2rTB?l>*zb3KGRpZKr+O@?}p?4oE?D5*k9cilGgg)7iZYAb)OhXP+b>F zU4qS@y&5({(`r^r{J6b({14Z-?2*d?V+wEqEUsfB@wL+b*`3 zg*2^I4l1Pp0nRssKQ{r!(YHG+)`KeOq7WcJde~h1noS)|lo1Zwf06(J0;B_F0IWy( z$0_PdPp3ut?Enc7Ab=N`Q;C0^;=c;R_VxN77fomb1PB21NXjV;hsQ_0KV?h}uG_LD z91ed=TnpR-1URo@+u`ti*iGaIr7|;huiNsh8dPvX1qhG?oJiP-+RIzfKaVRH$a%AU z`|OZ4MStC1g2`b40-T0ze^zMQrB0*>k{U|ho9*ujg{&W!7*)#02MBP^!nPNS75Y={ zYc~EX(L-Dr$I&<2_qRi#ahl=nXN%oloRW?DwivZ}#)|WE48ev|d68?09U1Jo#S&{v n*pelhJ6c-jx-Fll`s4o(A;y=uqtsy;00000NkvXXu0mjfgzPI# diff --git a/features/details/impl/src/main/res/drawable/ill_card_note_eth_211_343.png b/features/details/impl/src/main/res/drawable/ill_card_note_eth_211_343.png deleted file mode 100644 index b338ee1eb55de99d8645ad790a940d2c5ae96b77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34834 zcmV*zKs>*RP)@(Lq!K1bgrG_9E#@wqz@^BRjV2k)6be^E{KW{8y5hOvX;~OnJtMoy@pp zqR6sr*^({EQ7^ag-b-Fy_TCE!f+TwHb=Gg4vvD^z_Bm(ob2f;={px`(38I|xt#7UM zuC?BkW<20afAu#nXJ%yXo;Fi^rl(CqnlTOf4{4^@Ov^`z5X%8+rbo_mk1^9d(9j5(*(gc*C^m zo}5nKXN7z-VO~c1)LWq;g$UsTX{JQDg>PDaRbzs+GZx()B18xuNZa!lU;F0g`BTI* zrJMiFga{EL8Kg~_so&B+eCg}o+LJL2dlw=^h$JzcHZ}EXx|y1K*@OrYA|*_xnLX+0 z(_wc*h!814Sn>2->FMcajR_GVL`s+z0xwga`oX(`lh+ z3K1e@Oq=5Lw6NWSRg_Rk6yfTT^e6!SKki`O;r~(@alInT14k zFHVjmXVR6ALHW_@^7;ZCNne3^z`<39m6atC(A;D0jG%%*V8#?8#6qF7yW4#Ghp*54 zJxwa8MTG@sP2FmB!QLCAloHTy1QWDr=xyeE-O<+V@H*Yiz;)#li3Qc<2hteh;on5B!L9?bAgo733 zW%~JUA-;x(M@*0i#F%)XO~Q`29pu}M_w~aKt?eC3N>r{YGs$2~4E1qqyMB;R`Y}k} zfU?qJv%0q0l$8`2I(|beO_Z0Fn!P)>$NV=woH%~+jJXrzTqcV^jOpRy7X9#xn|JOS zznGYq6yqJ7F(N-Mn^<~9UaqOED3>r`mB|+tD8%xB-^gY@3NcF}XX1sTPSq|hn^3N| ze?a48ub|FVBJRIh3;< zG-)J(92F|vEAT?OIXR}PvfNZxR!Clyr=JV4{4gTtqNT0F*Y`|L22VmPs4+czaK9Y* zT=T;>-=6h(*q8RsZqxjz#q{?N`XUuD7`)Kxno3hzQmh6W8qxAbnY_=>9zNjfd(K|C zEa&U72@(tInWUhWCpiq5oWu+DnMW<{;)Oajtn-RUE|e#w9-rq_1V;%+-(E1S6)_PLM%Ouj*ZC~ znq7qDk}fZeuz2==qyChUF+a?0U|r)4^>xNCR<2yB)cEyVcT;L~C6~e&Q$dE15}`3I zP4x8knYWM4CLro+s?E#KKcj!P|J!Fi`Ekjg^UZu9psw-4q((5()6?cOzlI30R1t_V z-Dzw}s?PMnk;5iTZ9>ck28TyXJ->cF^gn<6po&aInFh!Dx5qO5erkcNhbwQFWf z=yFR}gqmZv)RhVgS86|Ip%_(NZMC@-z089`80Ljo^2m)A{xP8M{P6Xd`A}9?ruoE2 zUeR%zyxd$9G{U)q5KA6bp`Y9+9m(=P%*u=~I9Zt)`r@I{#~f3(#LZBq;biUd0cVJTQ7@Pek7OYbV;T}SI}~G{erUK# z|Jx<}exlm|gX!q#GA(VL>amD_d7*-kWunm{VpN%0hbS#6GPO08+HD>pL=adsY#zpQ z>FQ0rVdg{AjGss=$;9NC4wLot^_vHeTFs^nYfNo*r5Ib-*R+&?iYvN{<0&c)5ycc1 z6lljps^@M|#5y#)p1E)(=@E=%K^S@M#$D6e+G#>89YAq^{FRqX{p#AdUzaaY2Q?+; zonoO#{{5#u^0Fx_Nqu5qQFx|%O%LZKA@lqqQ-$CA(UeU zH3#+x~58R zkldVXb=RpU8-0n;+ zQp?!1EHeFJ=L8q^Q~$;ru5*3bzUJ~6$J z)ms8U^8t+q${eWrc6N5FKtEa_{QcIQyJq)}ZL_`yxwxU|86}b$82V843NeWy1wt(>{T`}dk3zVWu{7nbfPN8dBg?B8b&iTByF zbDMf7JQ6G(LxKGLt#^;h8EDt{o4ks`?(q*!2huDMh%qsLk8YmZMhXrnA8!8!<((fx z!;G8bgR_^UKv!Z`$csVM8*1E-fFKGqyh!|R)`8MO)EYyRn%@y(=>QS}Z}hU%k6wE2 zuzB~yDKj)QY~DKdzK*8XN&cj}n#5?xu^vfY)%@_0dND%%9paH#igxAt&6rKvei2v# z!GV3}{gZRLa=AUWZ{8$N^28iUdhf37CMP>0W$bn{P?WO@a~Aw~k4UY_+l^BcE<0Ot zfOt54{-Qa!cefU0&t1H%f+?-ydH8SxD~0y%+@a@^O@bG$Tr)u-Fk^zJ@0G%*b3t@v zMWhhg)7v|zWoUeC!W6D#gg7T=*ZG{^<71VKA6U##h@}O(7)FI)IJjq*`Pj=ZXdUtX zgJxBECGX}crCo^VslKM#)Yq++3p~r5yL3fZzgvMaxC>5g;gv`tc@Y*0$mOC{9%AW% z$OC4syQjx&*|nQnM z`FyP9(XGPoTR9f{a%dl-9ss*SLn1;MmM--4^pxgH)6?Geml8fk+O^V@(aF%>z6||6 zE5(Rd3Qae0W>#3l4G|2|{`_m-Sd=|9s`W5ArFsvO(XMZ+QIS6<0)_xBb*A(jThl7EO00U;32lxo;CU$vtUA)X`xF{U-b z?Qh$((If+AOx}L)ees*H2+K+=*a-*}Kq()wd5c7?BW%DfYzgTKQp-x?h<%GcNOz3!5t|~Wm zwKck46BMj0Fr8gS|l`=v%Gx5lDBctJVFLU9_Rh>5}D=XHi zT7V(pwc6S{OkXJe7-E?q5Mye7)RI7YxOYKjl|qDgk_d#&3n}gpA;L!25FrA?f{$Pz z-^JE~C&fQNkkC>TB1F&#OoVd%+BGH_Ape#11gBjuUAq|?Q;2y%U?P+(mvY-Lnqd>6 zbUGjQuP0q?IB&oMs!77cGYnA(je4HZMeofbi7mTp{n}w#vS!FjRC6ks9XR zn9#oK?CM@LTlt{j-4+Ok2$3oRF{a+WK3!|*CNxQ3Uh|_zuK5w_tpkIDCPawkfIy51 zqu|30kuBHmUg`F|MiU~$azRKyga{E37My@UXgx9VhdR={vlm&A0kB1 z2*jAi#wK)~<%+yK&6$EgmKf9K4eMh5J8epC#>66kc;9#b!Nbs)Ld**SF{bX`9+Q*3 z!8~{PfVpw|jt=}yxwIC!VQ6^7gb1-55Qs6|f6#2|s;f#gk_`r!v5H6CKQAZ&CE5h5Uj z#uOq%Kv=S4>si@ZX2;e|b3X63TQ@|ACxJkWi4KU;Bqh5>$3|ThU9%GiQ_`JYTkU`R zJby)=6m{orRWDqbZ$gAf1%Vh-olxQj_UtmrfU)(qj!yHWP+nH5A0a{njX;d)(c@P0 z-pHwhU&j`~($W&GMHLoA)PT~`!n7^K@<1TQ#KJPSrDe1P6&Dwpin20ORas$5ONzDj z#B^iwO~8IUrC10!?vdn<~o7O||4f<>H0t*PDuN3T$j@*6o6w-Q8wr zWMt9Ee}xDU1OgN8-?DL?StTB5r6lr%a};AcI=jr(>o-k%SGVc!A5bZ)@EATJ9K;Ba#920d9Pbjr*l#mQb|#(-jPjrUzuc0gkc&wB59bfpN1gRq#AFtZmSdeS6a`mkF%@qn5V7JaUG3`L*@+;&Fx~AMG|{ zaYSo~5Q>Wm<@dLm5Kjcb7!%zCr6nZ^zb+>yTMMSu6%~5B0+Qu#eeZ{6sbY0awO$5p zn&~CQMH-5ws*M3lT2Wr6OzFeNEqWPw!TPlgruorh)#nH?@0bpRfT*diHV5}6*8#DV zQBzfECY3#NUp~++VICw5p6i|v0)9hVyk22JfrQJ&rmL$*ym-5>@8@@JTEEs@zjeoi zn12LfOfBv0=J?6eCK)VM#*g)(gk29>9>@JUG%@arXKIj~>fxgn&!j+ZPL9;-Ds^=! zx)qa2w3A66pyKfrmR_kOj z`>JArdJ!HEu!@InkyOyy(P=gb8PU_%7Z%Cq8DV3bPjJ}R*4L@G@uQE9m$=vTz~l%4 zQ(d{r^=m4GsHv~373QY1({_{d+-AZ@<#@dAT{B$qPFm zK-;T)CY6K__8vy4E~1aOucYqY*a$V ze%B)W(8#EUsDnez=4nJ|Ode5JTVwj<5CzdyK>e*uaxQLg4lajtzPm%lqy|P;27{cC zB54wm3ZQL&18QNDyj?Yfr=Pq0QpM>^@}t9T9Tn3J8Ohke(!Evg_z3m8S_(UCFnl>Pc| zK{O)s{aIO=Pi0?bXiNz}M?BOrY+PzTCtr71K~b|z+X4p|qcQtrYK*GvjU2 zv^R28!^vSGL7K&IM#XsNhkfPJ!Q+L-v`nz7taN6QmqKZ$6g)Xdes5h8cJho{cN6@2OtWT)_JYo0xV-&KNGnSn(5t7k(dMA4OYkpE%qT$u(=$O3O1DZ1=I!I8yt7~e^ zz55T6I&_3@wKY}haVToL-PAm;TnWD(JaRAml6x?jjJF|7%g)J?pPL~DIjJn#uv`#~ zUHHjs@K}71I}OakfhQ02(pt70vFwbgOy0;ljg2#ZC+uLlmxD>7O~^*$Ee6%qO|z(b zEr(i?7EP0A0m7gu>eA#IYbyw=N(&`~q;C&keDp5nugF&gC0)}q9Vz(2_zesWn6Ylj zYs8R}DZWBD)K*pL@3|)z&3r29CQ_DKu2@#jns5o}Ft=hV-i6#`xFxc6gPXawx>^qz zi9flF<$>0$u2sb(zcrZ;^P;}Ep&{OK*Y<5XK2ayXD?&X_s^=se?ds_cbRLSZmkx4z z`v~*q8yOj)F)ev8=HXQ~43BZIX-Ex_{EAhglaqL%H1$R>TwXyah7nPVu?n%VKnSV@ zTQN4mEPmH~ogpk9Mel)968YRPdJG7R$I_iD#Q;ZwI%ZrKzWVqfukAMl0>n z!mJJoynk^~!3@DLu3l_Ja-=c!WcCgU3dK(wmW44f&YjF+Y7QgeNb|!-v#4SY`&jhv z7#n#}{)${*BjXly$gi?e8(9IrXH{8A+?ggXKuZIaT)aS&cu2davs80~0?Z;83m+D} z!|TFhpa!Owg_HvDVi*(Uh~kn$)7svqdE3)ML@WzqT2)%6^F@9^Xcq~KW-qBGuj!D( zPH0wAR3wKYBK+bvf8fB;Agkfy%#A$}37wJz7eKWb5hDF)yfH!d4*HY>CidqI zz2E$3&cCtXn)zdxuvAEb)wR{8t+PvweZ$%{`scnVrG|I$1H!0EsbdWZjeR#d_hfaO z-@0?xWMpDgYveIEM;N$8a3E=)f)v7-@Id2JYRQ!YCjlK zdX(UZSHl8~GE@oGjd8H_z4! zxPGm(6x#z)ng5|*{Lb!h8WWEe0@yke!v z$}U%x-pR@Np~ZPFye}}Mc&?E}Ca$@(J(IY8CleP7bRV=yco6Rmpp*OB&D$D|`86iO zt95c?(%@Zzs-M#1f^`qlsGjuaRndrdYU6NN!N8YRLNp+Y|axWe^3><_2#!cjM|XGhF=wdJx5nu-;pg($xw!Dmlo@z#V*$=j zLEy|+D47bDq%o!I4V<0upp(GE&czC~?8vsY58<73)~L2t3$F2iL2$D>DPLH|Sh^83 z)Dp6(_h@R0S-jLFd~`%}m+KOKVo>BTlme)aaY?LPk*{70PlI80$w9`zxe&&qd$9oH z_jshPIL)Vo!h8)L#kv@_Y+7%+gq7qqS+jbz){ne^yp#(Dqhuv3e}|w5qK*`#VFrmM zspwiNUMUgrz_cfVkvGMV5*b}&gnM?a3ii#)DPlouBXanh`IXgH?9ffO+bH#y||E}CC8r${rZ?LbJz!Ka-G)mJ(Q=)%B& zC|~5EV5rpgs2iDpr}lunn0~v9xrrkk02%66S67sq z%hzwt`WjYKLP>j-n_?t_Lql#sC}Ib!8e%N=gf}9#Qmy@f&OSRXT?_{!!QfCI%F2q= ziy$^4-yiuNJk`2}Ix)6PWzf*&qviztO#d3slY<_s_^DK}d=DSo*vv9*-LyepS43Rg zio(57!OLCSw(0@Lqb2e-yp9x*r&8>t8V|qF{Mu+@rB!>D>z9h&+MFrZ!<`r2=6ig+|s~E#B5vm*toc&5}7Xi#6 zdu++0AQwhPycLt3fKlT>ja!D6L`Qt&+osi6@wN~Lcp^vywg_6p3V(;(U;_u23i%mc z%o_GXQ-gfZDs(>-FH3RR849NY9w$AFd{j!v&QdtMRIE^Le0=dP7GF}-IJaoKi;IW3 zgZjPWqu~`0B6?k*JSGQ>L)YY>hAH%-2f-oW;CFO(s!9@hPBN=i=-1c=#j?H@o zo{7$NE+G2m2%&gf{2gyezGMwlTH_LR^7XU{k;_5d;%f|JZ>yZ1x&f(=!d7O4&JX~;epU_{(?;u1o)nghG0{HM+W~+&r})w=ga#W+^SwU>d7~96FdBf>4HHDBTfm;}CWbK9$Sip|yuwn=uFu zR;r7?2(QSUSRz0^*5gBI7SIu~aczS>zWW}@nT}{n5(ZTuxf42n_Sc!`Lr81)dKUOS zyk7Q>5^ccE%8Cs83FFELg}f^AwbM~8^)ZzaBZPGGWc)sIWl99A%FDH#>``m$q9Y@g zgfT&R$D+F_pwjL_p5Zm5KpfwhK&NR(MxIWqrpf;(wnBk>rP=WU-O-$iEfBi)MJ@oS z@~#wXGu*_?NGXjikI^|65f2OyLWB?|dmY19W9f((?pk?m@ov!k_RFV7o)e@C;q8Ru zb+=}s$Z>OZm1m~5PLYH4*}BMieR6a2%!Y<~(@PhN{N8N&+F=+)d4E)Ob~K_5fqO%C z8B&5;8m|SBL3?z&;bOp&FecuRSj-kQHS;q_c0G_B#>>DM9pP5zGa(zzs+vHH$Li?p zmf~cw)>ix$C?O9xEPSME<0&M?dO4U~diZ+!`;u1ViS*aCYSEP(f>6sVWlmUy7xC*Q zIm}S%y#5rfX?T73s3kgrpTd3ZZ+YeEdF@q`;B-sl+Q$Q$QZ#L)@DA_}P7@EkzG#&z{|x+bPgau~lN zFc#H*pA}jLZ#ekXicVrfm#$TTYGkHGxZW~^0QzDX;-E0M0CLB4??k6 zc^M?iN_oBe2O`f~i^)R&-_hEv0GP+#&Ac{b=hPkaE#66S6qBkaL zJC8lof(V5$C~76#T+V4!Za3O=xb?WfxCx!{J|WbqvT{!&8(z^Vo(@Bz9f*&t=<+q@ zr+DKD0Y{>JcwWI}K~G)%>KgS{@#uX#F2o5&4W%8cwaE>ze`t~GFrj+wE-5Dz=xe<~ z@}7r}AL{|;HBZDiV#L(WFeZ$e@qS)sx5okj!q^7wSnZvOCw2+-y1RSjdnz@&=JHyI z&hkZXOvK$o9t*2@FG4*+n$^`+>Rs~W21JV?LqZH%bBMDEE1Ykl#X`{;9ptv~zQF;r zzM)~Jw@&AJqRZC^jhIp5u#($wBYQmrwD&YgC`xxcY-3l9-aQJS98R83vm7#B8?TE7 zAYw2g7)k5(;B|!^#7HZmWB|kgo}@R)Nta_6bz4pULbh}9z4xO7on(d z(~n(klNAA>y;MCUn2{r$MoDexW#ICF-sUlT9WdnUs#)11 zUeGHB<+(Nr(Sb3MTjB+u6c)8HY3qu|#mS625W=mIyoq_B+d}7FxqeGSH;dNgCbdS> z?bx<)qml+B(s(wckk`ne&I)LqBpxJQM}$>R!KhI8iAW$-Q1fA-WQ)K+f-m{9)+Pet7YO7z3|oMAyQ*mjQ&c7P(}# z6?p~kC!rn}2t$yBjh5idd|=TUQ+{5auZll!vK#G8Sizf7Q&llbHkcfV(2k+JR+lP2 zhnmAoa-GVUwFM~R{OA%gui&v`SUe8?ivi$O@LE|}a}Ncq=KY5c)uT}};~=~B;*py% zt?UJiZ4ml0ZJWrew3AE8YlQ(*exP@eXn_``$WACEll_NX(Cip1JBX73#a$u}jG5OC z@+XM*)QLq)K%jbh|AF^boX)NuWzF2`Oyo&~BM=O{kf@y!_n%KTp?= zis$M`pl^rxB)QJ@TenRjz$$9NHZx0Gh$xJcpOxwF>C*B8VQVrV3@1{-u-z^{FR(i0 zY5k%;g@izmtly$Eru67wo1a!84i$-eEYLJcvnt`j8oAy1`dfErGfKCGs!elrKO@9g zA#^$IOsQ&kugK5S+S8OzhRNOoNIt-P6(1NH3kV=gRANiQRr&)bqXUw7_bN%?TU^H8 zF(O1WVFF_vC_*(}(=i|GI2oYaw`;q4qes!27C%FqRmHnxq|jYXk(h83gX~K|kB@T1 zsJy0C60IbXfb)YzX-udOb$0q!1kIKkkwn$m2w6~&uhnyAWH=L-u3gu!VTqR=vFbIA zLl5*O;~=Cm0?^jprTb>vlDt`+z~gc-_~3ctX}Gz`GZ3r6n^06GG;Fl(h_b+tUsdHQ z4S}5C=TTH;oP-uDub>dlCCG2Gejni>#*Np^_&$h{iny?{)}i877J078o9+90uD#>5MayT)zXUvy$@RUk^Dhm9Lq zH;3DPUPg3t0ypEM*5oHwwd=aM+0<26d#cl5bcpOj1`saMsc`3BQ&Q_f{X(~;kI!z= z;WB{kU*RA@Xke{%cpkhklfqUraE9YGO$3aF-^tn=C&<;%p1|-i2v7L{nTCJBGK985lEfu&xljB8t++W;E$z}`KQ3pMJEnJOq& zszJ%2bUR!WTB-0u)1%W(c!MSdo?@Ye6D?K~cCyH(^M}XawSm$GJbnlb`v}G0;IQvH z&6VpnbvV-L^EFP zaXCQU!k|$wwzS6=ladZ0(v4ep^tC|&$pQz9+=M|6C9f$Hpuu@dHDP+XWhw{4m62{1vY~8d`FO7+b#Wh0h1zZGN3bZOQqlA_5T;1k~Mm^Hdba}K(J!l|G zv~a;qM!gYiD4%D*kwcAP@mdZbzA_pmO{OCST2cWn!wq7DkQMQa1f#qo`Pb(4Yt8+! zv6yG5JgO-_Ur5QAXaprRun*WJ0}duDUHmjb+5tt1;0bbHR_%e&b7Ja9ygu!#08Hq^ z?TRuE;{}8g4;?HVKbKgYiO{8JNEYxy_iUmskT!0rO6Uf9S!xaJ1A-VA21TzSAwN?9 zFo=kMV$58`L?tCf1?t7Cfx<9qt-=*kye<|anK-z{UMXAk0rPK_MhuQfGm6{&?RovzMNivIGo8`{m^ zEf+T-JZ)2i?#_Zc;OEiQO0zE;3@s2+a#kUR<`XFy6AwP9CB5A6J0wx%5PJ;|aobGK zGEzM&sZEPeyzGsIeg%*lFjf=ektJOG4h|!R!|)(CGE5vHMLZfCa(B2Szp+ zNGujl^U$%JVKREyF%p!CV)fYZ`txD$ha8jFh+-}I6c<@!AhKu9YXs57rAd+8>bpeI zXg=6GCsHydTyo5W)hBe%Qh&e1z3Y@MWBp330w=#0CTeteR6{T)J5*d)q`i0KH?;io z0uM#Q3n*BcKxs$eVz)Lh(B6{Bh+!vblBCCPGKCFz{2q*EWO&GojtuLv0k1l8mgnO8 zB|9)_@>*eB=w#&Ntxz$@w6xR=iwlih1%ZF7H4DZpk->5)q3}wcN+*WfbKs2@i5xTc|rV|1gF6ejFa?8n9 zCT_%oIHQhR%f-R%n93?iJkXHbxMN~OK~U&wVDdbNhX&O+GbMalk-t)&TTV(w1&M@m z5P1q9=Om|bLzafUwHk}wmWw9Qc~CJ*F^JVwRaz=>1B8;@m>4=_DIK{RB-e$@SM@#Q z_u@sJ;xZ6XFnml2+`8BJM9LK@853auEL>2=gjuW4TG{@;O33 zXj+;tF+e(+t>*$&k7uLym2AtuJ}2=S1AR|c*A1$9e5^x5zm*d9CX-W<=vPSM{veur zAV`C8v9uaJJ7fje6hEV7E-c_^hqo~mF1GTlQq`EiAc;QgIV{lymu9C2kr|4xd_OfV z!avRhbu@D1Gv_a2Imuhw35s-hg2AQ_Z#e1`5CoU6U)LBL zkHQEoEiq~y$HOEy90GEozMd{U z*b4#6kS8SHDJU)xFXeA!oYxYcMuVIXMZwYZ8mq;>`ChcYbTAbGJht3^bC1Oc7&lk@1MQZ^=DlpyEoXTme_c!XaZ zZfX-4t#z~GQ3%U$>F6Rb_(yzCU8p62CU`txguDT0Myx7dtF&yghD2D`)72^eO>S-{ zNKgms6}GgXs8~Fex3(Y)uy53EhAHk!xJ*cQ<=RbGZ!zU#i5(;o#sty9`%8c3O`+qh zf~%h)LZTRqTqWAXxEUg3^4f|f56(Iz|r}ChKG$=d;?Jbq^0%_Qf`LCLH1L$$1tg-(LagjxFQ!9A(}=1`)vT=;Bso|?DWO8c>b_YM2{RC8)K-!htsu;_(5>JF7PH12jk12|%1KDc zCI)o&T3RXEjw@qzVDB#T)_W)Pmg6x^Ca4GHWM`W7wUvw0kfPx67vxDvpu5lX4aPor zJO?cfGps2g8HUG(3QKAkz4J_oMrdZm80qPwe#G}>LbYSDqD4-eBt-VLL zuaj8X+&cYq7&ufnHymDVL^m>w>O9g!w8GB!gL`+FAHDJJ%$5c4?7@BJ#OZUM zZ%A5J>Z{6i8~9=e>Wp{;Sg<5wSrZS9)?81Z`GdwMpGa({QIZRs3B=* zhIlucgLCsD1A;50hzh9?@pu`^E;2U-d1P#Sqk5`$Fnd%C<5}K4f-M(d$Ie+=O{yS#}&vkg;q`CK?*&NujGny+;i}7cx_m5jI zn~o3>UeXP#t%=&DeKA2Q2y{4*;u*y}zaoJ=5sZel^n*hqD!x7-@%){}W^KJcb?)L! zRU4I1>Il?-*gNERUI|S;Mya83-xZJK1dksh_S(eC&#SE{3ABUTYQhDCS=KJ3`9O7f zvFYg_^4wn;d2F)}bR_RdY>+&sy*sySd)G4u_L|!gojCv0;rP)YE0Z(-1Z#s zSeDM`F$|JT62N{wgti>i?c9;j%=%tlSfsn`;TqiyhxYGLb=242e%Cy6aKG^in6?*= z9F%&-J;}x94#4S~y?eWP^XM_JKhhMgWMf`%;T=pNYbc9WMsE5CtzD53g3=;Q_JamQ zL1kH?X>Ro{u;~;@fd%zK0I`sav3SBezJ9kv0EBahR$AJ96ydG+n|$@$rK{%PzCGsH ziBo!BtbI*sDpZ=G28FjP#K2apP*1jd`&LiJ1Sk*_UOKzvlR@C`4sMnlHb)z|R;Y!R~lx=*s#L&Dd zuJVRflobc!HTaFa;vw23RjBLpm7&aB9g{Dlxvh;xGn2HPXO%wl^E?YcPqJ2x+<0!e-yYEM$0iF}NE;C9+MFr-i=MJ0qPn}i6uoDu5_Y`yY z?%Zll%X?>?7rm6__giaE(Pg!E=?)K%MnrVvYsi0f^LtKq%n%UEoY0G9|UGh zN;ywX>39NF@*8p}Dey6->>Oc-W%Ih!m!ZGyTQ*7&^^*RbgTz+3uYCRQ%uCN6o@u(| zh9a)Ub&om5DeC2g+OTeoIePq5%(eGi0M%8i%-fQWc|{RkDK~i`L<`hz+PeBoyL`a1 zK|%9c;w`M_fyd(SUA+UQYE`i*l-D?@&@xMMm9mlo^SIsHq%&TmLI{^fk0W(Gyh)<_ zhx~`!mc!2BR$2io*@ym|&_-V|Fr?kK zY+i3poI0q?NT;W(NuG)l7$mgZ zIPnY#@R-kEyk`8Mq%d{TfrIp*rAz-yPEcA9(U0pCqZt_+clr7;)A+E%loYHmRprHj zg?^=l`KGm_*OOPKNWWJ?Gs~F9Tsl3{b;c@Nsc$-2xDZvkdSXHsM-#H0xo}y}3zx#? z4Qs_~oisoDiH}NHx7*ykcV9yB51B@~!}smpp`I*}jsp=8eS#Jd!NoK@IihE&RhUB_ z8-|phhf%N4v%}02R5|N#?uaoa)5GWaolvWn+mYOcIU?qXIK1RJ&iNji6?IWre=sB6 zXmk^oBiBo(#T(I_hTc59(B56!%vZnhomsgSaqx@J98}Rm2#rHhfSuB=^ilPeL-GLV zKo`H!7tPfmGzdj|`nl8$>y5GX$zz8= zC=mlpbN>q3guR}cvr^$uK8ChLjX}+R(g9G#q#7$_;b)z#h42 zPH6)o?)g0z=FBS)PTaR=hjwwFy?A-n?Mb`K0|^7xudUaR5D*F;+_%eIyLDSak`IZ7 zNR1;)-*9dsSR`E`xqh8aViGsQAdQVBF62>D^1FP?g3_`roqBMs!v&FH8E2~)nV0gy z`1quHwXWVlsdJTxXUk40^eYx`=f#)+k-~F__p1UgjjFntqd%fdH7wrKT6e0`u3`*{ zmevtvFqcHW#JM|v`I<6zyS8oC_v7-lo0=Y+xo}B-?*WN4PR8xugWw=+Z1=$SM!Z*q z6yt--fuimQ7cZJ(i9U*pizVkS)C)S^r8Uc#xFP8drtqqhtiApE1pIRF5y_@$DT>vZ z+A20F2o-v0pLze}S(6Mfo^qkAanp1I+rce`p&r`5+w@Daj@NnS&|b~4dU|HxUcA8? z6U%e78e)l)`I+N$e|-I@(P{#l1uJZ zQ;U!jE7enZ6>xL{p-C@19U^vQ5G_=8l}NHQ=J#CsD&2@4TPz=i#-gD$C84 zYqw@TM=1FIsdFk7@Pb4cgkgkx$Hj9!cX*$9M+_>~rGwX@k1`pMqtU`fr-ycXNI07w zT_BEnZ)r)98d;8m)y(8z(ZUf~`_ZQ@@TnHC_{FHwF#Y(p6h#4THrz5WIDVltsCX5( zbuPU^a$vm2#ZaF)xL1eE-V?^iOPvT$wRO{a^ZnP~)&v@(@;U^#!sj|kRn?IMpYY82 z`c{+{NQ|GQ9{*umS5mJ(-_z0EFUC5gGqN!?=(tJAU$9paWBcQO_OPuTPjKVu9MiKB?g_rtBA}0nw^Y0oMse zCICQ#%er<>3R+nO2&`l2?sqy(WCUUJr3#kd!Av*Z5j4M4zLn84cj=muA zF24Yw61yurf9aYIgO$eF@aF^vfzfd&_tA-Qy$$UDhRd}uq0Ot_o)0%F#YemRZb5;! zO$=U_9CH95816Q=x*8IIk;8+i=jsejW|`3M_JdZ_ES}DfdQ^@Nc97S9MT~t_ho!K7 z`ut^IUY1ag+$-Ke-mtd8T)iF%1-W4PT0-^Mk1^d>Zp+VtS;Sbq!kViKOt_zI*3Ss( z0Jm>7yWSITv~JDnnXlpIfvKskuJFu~5E@>)9Z?!OD|yiVy*p?Ajp^E0+rJaxSuy%E zwik?eI99mggcmxR156X?6?vj$7a z868Wan>=M>Qt7SwTAjInN9qK_wwRYZ3^vCOjN)(p?)&D`KlKqU?q=yQn)?TPgt+B2L-yxZ(PYAx3x0l z;Jn{=8|ThBojQBL91%LZFuF~}jt*g>olC!+AVi~d!utS=m@HDN_~Bw{(IHI1nw>5k zR)~a+8iajEElK`~(-+LIe)iMK*2Kcb^$jWmwN(t&0%7Az&mPnloz8~qx8t|5xPbzq zld7+gIQ-_FMo-_L5o6OGAts)vqo+S%cvD^?q=S$$d>P*pMAvd6z!Vbtl?v+zN#U0F z`5`S>|I++;c6U8qoUt zar)fcaR-LH$iYs^tz$=?)RMUQUvz%;|=jqB!&)j%}tmeB6Rne)0G zF;yfL=E2~6RV=;L2cp-A(w$zylxSNqhdhaGQ)XUEH*B3`S=CY$C<1yFETY1C7yP)wnaM}$P!lzjP%jFPkUEgJ&nN<6KnA`!p48gns70K6C8knVCgtZXa9{w3#70K^KW3gl2* zSLBi!T^)mJnjb^ z{!*zuZCP8R|BYqv(;nrzh#E#lKlG?vY~;AfPFgz|M|2Cxmnr00OVYf67BhaM749~ zqQAi7!6RiRpoz!Ey&W#vvFvxs{@vTmxl7(dZs)Y_J%@k_9p<}tY?b$Uqnw31#t&}a z70O)l!8%)=6K{u5%b|U{BpMo4?Z2Sm%a{<%zadGwU*URd+#;5+APnDeapTV)KA;Dl z)}Ch$?o-8^SU-=Cz5(yvglhbSE7x8B4aHJFH14Pe+LS{}ix3Am7L`6I%o;fsfDp(H zSd4}Kxw^_yv$3vHN7TK*%Mt^yQfhMTQTCI%;jk2&z3Plp%zyQTXY|qpgka9sAEuQF z*sF4>PEPvs9!yc6ks4Qn{2q(pON&le!~WWry9Ehf#x$kc;>k5qGKO3$j7%(0u%nVq z57xCCw^bv0Oku*ESo=7Y`KTSPBrU#R#T{hDbUfywtyjJ#L@FDU}rF zn+I(dxQ`|{niTz zzaR>JEYS^N9Aye9cBcpDePH@^EXhrOczn8B4({8jsR@nOAIRV5Q-&>L*MVn#$Lkl< zu6{%++cY-%y)B?6Y5lOqlifgvI;uiz>g#l7g^>%iAP(%=p>G3LjHzgof$=864j~TDh6IMvpj$ICGS%py`xC7L)y7+)m%U2(;%%8f;%C!QZO;X# zqFVwnHmjA#PYCUNuCx!;*OZ$I@pOJ0Ug<|`5t`mhR}AlGS!szj2lIRKrA|aB%>W+c zkV%_1kyTP3Zp_%XXQzJORk=w zqXT~YZgMEj9kOi@Pf+i)8O;ZLWxcR1So@359@4zYH>M-~Z=-QS2@Ew~4%Sx9o9^g{ z`8)5QQm^*x!M*bD5hJ4wh`5^?g`1cl2aBYB+m;P_IEcUL8{|Mo=4=LZsFB2!St%&^ z3{6hA7%%7(@_=SiuTXNLv`{BxAGCF9yN`v=o&lXnVyvTD$OwwPQ$86no+~-ps?y}g z+G%<6I=W8N@$g8KUPz4E@GF@|!poC`@_FcPUT;VZWZ=lDnjSo|4ed>8?(&a*^%r%- z!UCRZaw;+s0imHog_Fq<1^f(WO)=3KOcuU86W~EWXYUk#-i>082j)_srlBz7fjR>- zP`v1t9soBQUXA5dOvN%^b?wF-vu|fajDt`H#*o(ehWgdou}%STqfnrzg74-R`{uXv4+>moO! zH7S*OrXIOm(39noUi@Cdx77y#WQyyncmF}+IIPL~6u0HqKvFro{_)KQ;8 zUhNk7xV#4x)h}MTp`L|&6L~l1=uIrxJ(C-{db)LN#I2b&RV2m7qCUhf99oFtv2JeP zM^P{B&b@@S2u3Q%pD-G0^nw?cgp@iL9SejG$Ubp^7{A~ReALpe!)m;V3A%wuKjrAb0`c8lF0t0YMcP5f6kXV&sCl8ErmYeS>OHm1V^e(hesiI`V|DSS=(6 z>PSK8LG<)_?{lR6ii00d@R4GDb!DV;oIyUTCaGH+i4t_GcuaFBdhSpuYLl>xiD#>e zfk@HVBY5pa(G@NV+$V+L5~R7>t`6B7zs=N~?MS73=2@@1N7F#h3xraQ3D` z*Z<5XUX`1sRVQtAt){$jgc)=UCu)R+vbXn_F(2h6Ou_(q29npR=0GuENKL9pF9e!h zBl!wUAfhR;z#(L^mhWAeAB?aHdD`HJ_t1yv;A)(UlyJFN>P{a>uHu}C%jKoT2?zGPqDL-aeqXX09G>m?TzdhucTKhTiCf44 z*f%I{np>UB&MEPb2)B^GU6&73VsClu0C(1v2zRD@^yr}?6x0G@rM0We%w+WU=ZgVB zaZ~)o&{8EBVu1h1MIl@67RHL?=WLD6l4y;&Q{@U0Zd6fcN&alreS*`R?{ zqXD%lucQbvRZ}j5dwxU+VZV9?qt=;OI3k|6kDX9b$@0t>q@cfMwYP}D3uq$PvMKS=C)@JI6opESzFTWSDT93R~L2dwlM~74zy#&+6X^$IDAgO;E6w z1mp!sYy^7Uk-YN&X(&tt#4%!xC&xo?Z=d8eRWl)g1!_mDYy90@bgH+y$a=B!$*Ud& z7?wBsVS8$WWeWk=NV?a(fR>zo-A)q8tq6}{^w=mE84NW^f$slS@pNwep40-VC89zE z$kTWnLE+y0W>Z`3J%3Mxz+(sc>p1L*;#+3@nd$cnDAH5np=dX;x6AOt*ZT|u3Rf1m zGFZc-BSKC4TO>>uLw8?tdK3!*6q6pdcwfguk^bt9h>!{4-t$uXp}0*SWwM~;!NTSn zadsgqYbaqiQ`gj@*l-aPm_DdZLM>?Ax`l8xN1fn~=Bo0Yx`zM{MUl za$wKQ=cye~UC+w$&UD2JZ|GnqY@`PDsn=fhL_zbjkk|@>-J5TWp(y*f!%X-JO(PKp ztv*axpSyV3_Z}`gXb;=JXNS3RJ@F_6`7-Tg*4$Z8tRB$C*jd4_qw``+P_6Vs;huAB z)36why*ZRPnKlxy;AdGy*e(=jh2liGpV1915DFXQbw?=%GT@RdN93#WbY3c5ioS14 zEdG36-fS4#m!O#EfC$v$tx6k|dixU_FJA*OKt(!Nf z6fyNFw;Y;{6->^Ot5I8Q)@IeN? zAUDT*YX^#+tf^_JTP-x~Zmn%G`$YRmWV@jEo0bA$Z~yEW8ayeFmxP+1i;yM;$G#u0 zIn=m3hI^0N7o|b5y5{zSHf@FSgUFVOb$U^K@he|9dD%JI>J>{~D-qC)FDWiGU;Df7 zt7JCCUy8EN9@?)hNLD1kPOU@x_GnmdiJdUr?PcYsR}eyG2Q=mEyV2DTC_eJSFqZ^j zz|0UyBo%JD4Dd4PndwST50Ata72zgB#q%Yh-svVr*ZSt&``X}&dJ)v@$`0*4^c1Srwa3X`z6VY5yxeRZt5^)6k+$)ne?w9) zBA%IxkCKC>wLmj6WeOB^8PbfkwH*)|+TY(7o;e^t>#mkGES(34C6uNhg|=*1uj4Ru z{J;M8d$YcmuV-kK_s&h*E>WYa8%pGNQeX5s8XpVuYD@sH0b_#x0!2Aq4C)lL0_~I# zFE=Mw4Xveh?%G89?npRMbmdk=Ifr(CR@KEY>J>;OvvtFd>@lEA0KE_LQ|E*L z#=~XCRO_A{+f=Ej+a})6_{Glco6Y9+YxS@~9MSpB>p^js=GU1;brKPf57BMThI=kW zinlbqqTotXEjh7eJkaC~a)G+#Y{?iCTRIqLr}mTz=2yid3cLw}U)*Ar2n!S|lq#Bf zKmE~{C7c?Nul`} zfEZyMLx*c5AL2#7E=EKMXHA`QqoLz)FUIYGK|yu?^haNcJ3>vK#dPN?G2$Of2+`W+ zP7f1$LdRN@!Mw%cx+x>M+qA-b>wBSG1S_vO;H>vQDeNEZ;2 zRDpc?eauH~ksOZq!6_`6jAv4a4OLW>C!Dg5hk3s$>E=Fv`KlK6cyqX+NGR>25yat4 zS~9b;W7{Tuv3No3Z$_zjB?`0gfY;-8IkJDR3JcnAnM`>Twh6sT4;jRQ?$wRckH$^q zq+ofIUwip^t*>}lSz`y}2z`7MNg3v&D?I3Yir>M2Uq|;qLL+bkuN_kWJny5&PR10@ zp~Z*rxVoxRzn1YA`XeWzc@td)G(Xc?#Kp*Kb@lpf^J~BK^RA+yg)1Z5EPn9D+i^AQ z2%QP(`DkQY`;M&%bsN0`?@Q#93g*?AU}i8H`y?h~8_?nO@;U1~xLaCCpkUdiPIyPs z%b_QZMp%X96Ymyl2^$>^gch(*csD@Re20V=y4OdF%6OOY@x+NZp(u3-dzbn*rvTe6c@g98V~%Cqen2lA`D|m;1J{lQ zbRM>BT&ufaA4$=j_lEb^`L&4SaB=dwACzc>To z!L=75!F|_<)`T18&6sF6S>Lc`j-)NO)eR}aJ%41sO~go9MqI3?W?OJ;a?uUz6P8$@nuyLtzo24njrZZCd-Sn-*%dyC<=<7nDJsgdp}D zjEf;NJyUlB`4O&k0t3ZM@xeh`k;8h>T-DrTR2w69hJ!qQz7L9fI6D4;A&rgu?}@V` zPcA7+7OVnt3X5tEyM|(InS^PBI=JUW77c@L@95GG3y6!_rKRyk{Po0{b7n=Z|NX!i zCtWo6k|Yjzjq8QTz{?_`+>$~OT7RCIClJ%}!n^leb?bOJ2Oia!Sb&m#OIRPmJ_@tw zi{FasPg|hCig7Smi{vd`5ac$nZW!VlM~};)8<}-m!!EfsxHk)LH>JT3hCK2L~+MH8$6;DocYS1G8TECO$Xc-cDN!6+{4dW24 zq01jHpUUqggt{tTY2*4e`n$Go-l)BPx8?mzrr!>!F)r9O68-owCN6o1FfLjmAo4Ia zb_B7fY+j6s+YFJCD>q`diZF5xeM+;d(?IOtNkVt?q!C%6t%Q)kS~!en;-f9P2pFLZ zbgNmrQFc-wlyZ4kfM#dLq*;mDIQKk(- z^@cM$$p*wH)QEmtEsO zi&wAfi^1#+w;OdKC&kKOAUmOWv8Y)Q13SnSw4}h)WvpMOO=+Hq{P?L+y5>T@~fl z@19f@QkX})=VAgK9kff~J$DHk+0xb~m%%9snXk?iWx34Wk=OJi(Sf|6ut_{P#Ly|p zH963*x_(4>oIsA5e8~cr8R-I->prPfb@ue?`*TvlN-soHPzcmBt*x)q1Lb!$W<-R4 zP^EVLwsfG|)*^*hgePHkklckeYL0p@3*2IiQ{ctAeXKnptn3%YjmL=LLLU>#S^ID4 zH(W2ohh5Z=TM%|?lX$gc^bmpqp%ugoo`4(!LFt-Xm9pg=tA?R$+H5kNi-4kj`j_7#4~}Kl6!?N{I2u^Oium2{?#&q22QQ z+SnZO0S8ShF__44&w=CxN|Ftv`wz>kHFzuWl0eEiiHJcpFp zQz*qjmJ{>bnD%r6${vX^=g*`C21bn06Z*?IV4EZy!6;dPfI`!JwHD<|LHP8AOG?BL zTKRoj5cH%0xyI)tDw`|#U}X#*oY2v2Dc-0&reuU5DSbSA9K5t~5D1LTBLOdvTfx~e z;1|?02&FKpWF|SS15X%HP`J_@kr$u%nl}`Khsqvy)SVLjJg8!d#j(6D2L@(o&yFqT z+@;z1B4H(BBMie)zai1r9<$60?L(y2MlqD|6e@ng#bd#^`4!`528&DSajQQsZwED? zm!3T&b+rpK-Sl>F``bM%BjA)GQ0)EcH@>4`F-@>;uQlyQ9c*&^~u~pLmAbzMDbqVEU1n7v@-!Va1T%C11EM)H!1WbU=_dKo)!;)%KdIDqW<- z#*IW^V37LOAARvJ;u;`HFd-^$_?TlQ451=5sL%cVpEbP_o?uM06=7I>@H;q&2kUl8 z$TDi~+-=gh-}-$N!2PbbWX5Ql6h^)3;l%>to-ZCbsNUXQL!_l<{V-!u7=qLrXf6>OV; z8lFfoKZDF6TA=;Q@(L6|={N!8JHPS^pH?Q*5?En$VOa=UPoH$ zYHM{&XR#viIukDp3>Ggsj1ULXzNE}6c=4fZk=vyv;Wrnp%{?i-@6KryjjX8IBM0ZP#{95b z^apxj896ijeEliTmL3e9GQb}OBOb8z{GSbwb>8@T~QyE=BllZXE zy-nyzF*#N6Shxh%*4OJ}CS=Bg5t^(yE4!)tKy+6|3XU=RuWcrNrY(%?$7@_q4I zC=b!JoXD8yF6TUrCJ=&Imd0d(!^^>f6+-YGRU1EH8wZaz6mBN3^&#ejXsKtv5gP%W z;GjZR(@F6QpZSy;52MRgxIu-TT+2HAe17}Zjh-|h_}S!Z_NCL2^)_Z!2Q{9X@QJw} zz9-&Ag9agCAgVx}Jw4iGf~rr6ye4DgV{s1{uRDgD3MpiP$H?W#CB@Hu^@V56Pu_lS z&YDb~KN^PjggGVDCOW~0C-o-w7wr3h~H^JA}OZg&+lX; zng&V2M7*2VCQJ+Dj5L!n55;n=fduU>cwe?E(ltQ%XJ@$Vp2-b7Uh+#Gr{;q>xn`Gm zMzk6DH`bEouUHXC^DWc1u(gcA6c-nocJUVEgXBSU>*LvngseDw=}NR%JU!>J;@JqL z*RQQp!;hCU5hBvO>UBJQ*@t|g}iZii>;g1 zsmvHMWXa?l7yw@H_$hCN$)RYW`nf~7*tUlE`h4ba_#*aPQZK;s`4oI%B98Ddj=uhjEUx4 z>ULi1Y)`E*0dfik`+9nNb=-mRiFSFR33YfOky)mRlouyciiG)jdD@18q%Pu?wCYgp zXAqGyVh@qnWnlZGZzWZfy;!;h1YY;M<)ScA|Y^w z7!(GYm6@f3mgG!5@;X`ux>X9r*Il*>x`F3LahXCtuWh$WK8(=ig)7%RUjR={&J+*S zqL4VIuj^Shr;CS}juuo^AytxHOWzvh&&bB^h+-=B4sKpN7l|*z^tAjl7ELuCS~t;~ z!9d(PAp%xP7-LsXqC5GU-~FMmexFn=1iVa}$GQz22;chNkClM%qoOks6(qq6zzU`+ zU&(W)-6<<8TP3(KhMikC%ilB2Sw=A8EYYU6!|Sp~8&jLLBLZ>;vc@XfTCgGkKY$>) zcqQ`KoS;K|R68Y>#_p>H_e@R@>QFUjinT(LT-tA9z1iHnyx6?CgkmsO7-5fgM^BlR zk|%I*2-S$|kvxX&gQtcSbG}Jf>9Pi88Ai6^0NoP9|dL{>~g!+3weoKeGxEb*Y;q%|2L+ZxAJKakLsTc0Ug>!8vbNDm&w3`UHc^*6rz zBeP0!RhTzgp%Q_*S2B-}trl*cd*#X%bKZx=j74cofR1mPA=%$U(H70>bjd*|_$9OM z5#Yr}yT)cDdx3SRc;k>m=D>aOZfxpKeEif$UNT4DKk1q$WQmbN zY7pX){8JS6Q>$_U1A6!PNo_dA2)t@+%>EgDdO)dX@thz?xL6>0oV#k7c>V`J_bKz$ zZ+zQRc!^QG=Tg9p2g(sDr7ZbezGLYf`V=VtE+s5VV~TWp&kYwi+`M7bl%S7EhTGcx zF>GFdvL}6W99kHrG&xu#*9ci+By@JC(TFP^^OGNYMQcd9Cra`=SV?k{tCC|K+Phm- zXqz8BQeC}dJ*ow~CgPl*_{fWD1j&qMGqw&f0u_xwXsjk;wnESbhUa>ifad1HfHCzm zXGAoxPCOs4OL5`KIX{n5N2;&alg6SnCaB=97(y734z%@f;1Mm^yl$;Ib>UK=$}@~* zT)2E)V|mC12K&xmxu%C@Jla*zVmegKhdLMD5>Lf^4jt0P;>{R;;CT?jkVoh!MUsaj zBmG0obMg``6(phgRw##d?~d)JVpXa6;p^|1mUj2X-6IEfYgvGnrH{SzoEFLBfpGBV z9dF$X0P+HDhibR{d+3u}7&p0{0CPNWC8qSG%Hr7;m&(KZwh(B(|NLeZl8K*0~K zgVE8{MIZrn4^({WSJ&z--Wj9LgK^_6>6n<5P(^k7#E8E0qc`-xGy1)%yhL*l7)2(L z>6RwHz^lfir7d6tx?Ns82s^Du880B}IJkGGIeqqGOxBMzLuW4d5Ak>{pzGg$?*p?CSd_*@ z9389w&Ixh=l6v~`P$i{aM3FLx+Qjjb=gcqv{Lh#_`|EGU{W_h!5~J89wHr23{OHYh zW>~Ec^RfFn5D1-2aeDLm2K9VDe(Sx2UG!*7Jab^L8Y5wv8m@S`8a7c#k$ZA-S`D62 zi$^xq9=s;hHslQyJu0``g&xnz&e3AF6I^1f_PHjxo&DcgFEcqksfxH3R;nuQl>U4| zTdzRB5QW}8TcazlUk&>4V)Q&_rl9-!{k3n=PJvVbqxK6m)}HpcfBM_=BX5}ksO3O7 z_^qpuP-~B#)RhR&Ql(Y7uV}Z6H#`Dpdqg-GIVJ4HwkS%ubc-F0n4E`lj?BWk z{;+`n&q#?PnSn+kfq?~(_=;kBQFc<5$#ocOzSs0ng$lRH%}@8TpQ4`yZb%B4 z-}>H9w8rB!CTdoA2-Jm0qAl;j!GcPM#->*=7QIVTE>tuJha{g1q`IO)d-9<0X#>LB zMbwc>%=ce^E8&6Nx=CtgLqlf&o?Y6}{q_I;y;)=V99l~<+76O^8dbml`q7w4|4qhE zrf0WQsS%WwnakG_vy_aP3`rR3#zOL5TSU@Mc^oHL*E+rE?)ZdQbjC!{aAes3;kSbW zff2zNao{^G>LO_m-LNPz9F>0LDjmS@^(a=?al$G12IqEb=C2pq{#ekts%VuWi zmU3!lJT63D2)803;(&y2w^a8cQ@#!wo#Jv||FkKUasX6)B16K9t(LHFdTNLH;Pg3t z3{J4U&)S4&f#>tQ6l-bnjmMs`4iydg_Gni~8eTEyN#sLAqF=56B&v)#DQ<~`00)+>o4F<2ocs#W<4nO%0WDAr$y@t+Q z5YNbCV?`Lqm>j<-u6EjWrwJQ)cc*om5FRpW! zuIQpN2!cc-DX+fxj1oa!q^QXY`jYaBi=pzeNy>^aAspir6i<;P>*(w;cbl4Kq>CXvECK_ff+8$}k;#Hu4)0N{TnVp-m-Z?# zhH+AKUaG`{g+*CsLd^!ojBtl|XhIInfYeIx4uk@DuVlu^=}9BUp&rFS!^2=aG=S1H zOV~F31l~{`v)52^P;)WscM^Vf$W3mSZKj(W18I_c1+pU1<9q43BT}=vug$d_Xu`3G zI83V&AFsXof_kV_+OMd+As^tTctjzYy!Xuc#G4~6FlIw!aQA+4Ekuh1i?hx|U5FbO zSQE7?y3#xT#^<7~yB^!mEd*8v!NT;}6g3rG&H=|AoS*c@?%V6+^-ULeRL)H_>wk z`xsf4Z9Ieb)q1~|G8XL!h6N5i%n={tEcOLlj9tT|F$qe{O-l`-&zRiELt>D4GS%mk z&*8~(mT1)Uy*#~3*>(`zT{=IDm=n!j*lzqTTcjrn^`nX9_yQGjK! zBQRs)@VYG!gBG=h_$XwOujuEK%zF|6wr)T@(rh|b#X00aS%yYUC`~*WZ@!A+3&jny zhbNfNIT||1o#X@SSg3(9Kg6=ORGV}uQd^)!30;I!J~uk@UXshuLgVIrX$8XA|P z1c>!@G~puDVpnPP3Y1~bcP`!9k{y8=)9?Pv-xgMBt}*@Y|NWn5n52LH8^5ZeCg4j# z?Vfvr1v(7m?)_#7Rcf?&3bb26ZeC*ch}j-{Ncm7oQU5>6>nGXz&44dZu7Ue<6O&)eCpAyw>)DH!6OOiXIWT5Vs}n5g)#%ng zH}H5FFoev2wr9z~4_-fNk_Cf^&X94bBQRs4sJIlsEeshEB*`!|qtiFri=j4KB?Q20 zFF&VAJ0s%*1M_o$$iE;~Pzh=kg2TzUIYAF1#bfd!lyu{DegiHNh!YlBv&kOj4wg>` z8}H8^Z1jSS>l<9x>?~yjW=s_9+(039Aq%8VG&MiggXCokV5)%jC9M1U-~Avkg;83E z=&ml4+^?{((Bw$oMRX0-kM_in5B*=5t`UiIiA#r1V)2L zL`Vh0;_r(ohYk{f854iSgc{V0Gz3DiI2%v+4|H5Kf>u6xHAT*F{wuTw$(stW^9yDl;Le1zC zAAQk`iV@QxPNQt9mp_-q?j757H|YlP^bu;u0xLWR`jygG{q7|48g6qYQ3=89lEpy* zqdyR>7r~&Bhhh9YC+47h%M07U*2Z3;->pH#5Z*BFW9zjwRpy`n#^=pj?;MlZ`#rOm z0Gr0v_5L9-o_P>Ig!X>r`NQVu`=@nVUr;|2%9+t{7QZ=r+we{=J$G1ZTK?& z!f*bXdE@AD^ZhrDsv#|Riqwgt$4}~P(fdBf6{r_Ib6}6=MT|!HHAcF?E5)0k!b+2E ztN};7-ZO{xX~XN-QxY-E2LdxD>K(i(gmw1j)?q1B{?UK?OI>i>y@WPr2MI0@yalzb zE)TB(uOliyRh6rh`tC~Mb)$F;bH&m&^67~Xt!vGz z5%;{yG&lJs6V5>+5MzR|`^+alW}Z8IK*dXjgjr(Y7gL@{38R4r9sM+eEr+i>e zBd;UVt7tFKG4NKBeRNI?$c2Vh9%`~zuHV*i2@9LouT#3e^$B#*2L&$XZ+`bjb3V`O zXE&w?k6K*6j{eE{Fs5`90O(f!ds6YIhl^%LZs4DO?G;UgDQ>>{!n2wi7Up~EUI~#Hfp{hwI0;SA zp8m^ke8+t9V=tSJy!fnyeA&_J`LvFEKkZ;Q5)-N@Bw6AqgvHypZqn8ymQV*k(Z&RI zzB3oDD4|g%gb^DE+ZWMz%u+`n#zfe_7=!;LWGzv&>m%p#N3NR&UiV8Y(BA1`t}91 zp*^s6qQC#MKNa`iQRhJ?AVi2|is0Ibe&*w^s;sabkPy}=JFWboF@;zRpx*Y=!Z-uc ztUvtXSIpxC(r8`~m@)n5|MKNIpF?4X-Y~`jI+l^9bcm%5B&ye}uGK#imStxKV+Mg4 z)0qpGT|du=0M+Kr8`nQkq5cpdo-_h8rvK@meBSl*73F0b>ZPYIZXNOvAyP$P#&mGs z?wJ3^)aHfg8Wq355ZM&YURPwTVu7M3s~m?%f?3NFVsQZhO_Y*?fVI{rXar_V|K|U2 zAHkTMnlzoAJ+7M-5&=W{^iO@nY+Aog!^puHtv2{PnrUBr_K^A0Kl^ITuM4qs@%ex7 z^Ybzs7$gESCb<9d(qdCmQf%^aa!f{MhH5SJ3x_{#l4IZNYO2ioh6bq}e96{7tP;%`v>Oc?R(~PpZ&CUJ+D}i7oo%@B%7L?(&oyx zj!yH(fA*K=L9;*Iyt>*NbLQe@&jV`=z`gYRVG|<6LLg~lVkIM69?*~jAARXL-K=*( zinHWM7!n%Th$52nU}%1giII1^q7gB(eP1j7ju3BPD6)r^ubr9rSZ9Jp(#FK-07#FL0(3y1ym-<4KaP6S)=lfpzxu*I6K|ES1i~s|_b`KQDdLX$ z6lnbHr+>;E-oINVxu^xxK+EI!qc8o%%%0c~%M;)K$y+8UeEiiH7ndr3QEH)?LY?GIhZg`)^MRp>X4tzm5n*Ke(X~DWT(n%Jo0~ zm0uJi%`hQCEHsifrpI!i8=D^L2ryj^0QT*l{rcx-st4>I2V z?7@9XmqV4`yYFw)_Q|ssbY_LFim!Yv!r1-i|MF*MDjWw2u?Ub7HI8bIJ(k4_q5BZ= zNa?z;i@e4f+9FIN1mtVlI{a^-K(pXuFF&vO67xU-X9k%0Xkp)JY*GoW@z6Vk@Q7n4 zPS0w8>k4SLEU-EgjNM0Gc-B-`tx`=0@)&fw@j%R?ptOVhFguHcbJLC8gFbcOzWsyO zbt%~41AEM+Yd6dR$;VJcVl8B>-oOwcoItH?|w<1Rl{Z4K7L7Q#;x-}}Um`r=o{yjU)i_aZaJ#04FvEU2+gwF2d_bi228u-YI z&*Qy%V=3KVp+P#amuj>%~2D z;nJ)P;2}aR1OhQ8ORY64^TT=%-s(U6`~O9a2_s@zFl|N`uU<1_W661v=MU{yjkz9& zEEYdIG^P*>jX;cPU|_(Ukor+q^pGPo|1i4XtFx;|d-vGNK=BsOgi(<{C4=K9&zP_O z-S=mGE|khz{`lIrzN>wo@sOLHWB%Dc{M^jnFJHf9ej@o$BD8k}lXC~AoA*cm^(!+q zC73%){e=+E+9*zVR$5Y|t7Vc=HT%iYW7>b0lbxku<)jptM(25FY>34H3iOHKoOgoq zK6C!bvAls%ZsHC{i{QRdrq07TCCX5tK?*ICgubZLa zaE2+w!XhwZvTq-71M5n zpVlQ^i*0RWh^G&M8PlJB`LE`DE-N$3?A^6hw=U35L@sr^@xJnr$taqRjEv|?Pr^i& zAxDG>pLy~+xFJHgATVQk*QStZ2aTwEc5E}-HgA+acbFsl_iEn5a^t&$O~wzt@!cQk z!e!c$n5Lz>;(I@SOE;&7Sl-CU$e0=TiG{4_SdayXF6ZamVCD3wA~0k6m%sI!b3O+p z&JZE_4SSkM2wA$t&>sa?zhK+OH@@>DU1@pt!WDf1&W0N1PX+$RfBZj~`qkBOzb4Hg zf_h-@F5OHY517#J|Irs`*ymIbm@zff*UkF7CGUkn;fc;(ysU@6GesJbm67CU?0!0S z>WnT|3$gqG`9!|$2f1?gaua+H#>8N25Xh9D)+0Gbu}KmC`hWiqv;M9f54kyJ|E}%k zGoSppu26xxeC-v++l`P2S3RwZPje(hhxhN%QTc=Wc9~P>FNVexVzHnnQ_d<(K+4AC z9D(3(BT@ruY3tkJ6^i@W@!gpV=pb#g46;jIphO{i84bh56jy|5@|Z zZ+v@Z`!U%3N*KXX=y+%Cozx><(F8`gHm0Z%k^p8IGS@ZqZ-!>tZ zAO7UaUyT;~gGYaV07g0;@l1{KhX#`(P$Y6ghma0PeL*~AXJ@Ng5$ic=T1^Bz)1Q3l zFU@BpM?w>Caw@j+5KPb8$4=<#nh?tmeSQ9#qMtYT(~cWZvD|=1 z5<~i{KmLQivwzyv1O0>F{6a(eblRKpj}0dDQXxXvF#XnKn)!#~ks6)<64y|E`8U7t zYr<9ho2XGG{Q3|fmNCu=!| z{x@wp<&N`ZhVa5!s+dmeiB3^4+0ZEe-}a^Z*&-LM;GXWcoQLJHYCvy_l^T!^#hSs{C8oKf~3S;ytf zvv>@%a4pc|2b4*Yo`x5qfa->x{P;DJUqeYic|;_&1MIP*56Qqx-k&rSJ9r zH!-+sn0Qc7Fme2UP*QwN0zL*Mo~5ZSpT=7u~&VjN*+i0+;`4o^YPfrnP3&}!wk=3 zvsMI3aZhFTMoycrHG9HtbvyOTI)2F->F#NwwsV@LJ+|7YGl^(l49oR!-*Nf$N$Mk) z)%INdzAw+v?C+zR;9#YUVDiz<0v29i?`)N!#2*_zJK5oHfxu505jV;Q#*@o~FE@OM zU|qf{iS(5dYcu_5bwz!t+;N}Aqeh#w!KQlbp`rK6DY%A!I|Mf-J&P}>q^zxwO5Fb0 zamsXPxbaNsV- zn+1Uwku(jmAY61>7ZLWQH$FS+W+E=tsoOML?tJUTJIDU*e`9dD<6Ytqu=d5-+4-v6 zjBZEhqYsmZ)@G($ZDUABWSpm)s($e5$#M}W`wr?*PnRo9%nealxVW;YU*pLzOUq=~ zs~&iF56kb&TxLZn7_)NXs$|cHFc(t(vO4j^@$b}|(YG!MBnZFH^2b?+0|A%or%PJ7 zFKVvXp}3zu5riQQYJovNr}ZC9Pp|$IKEY^k2qPSIo*JD=>%yVj$rhP9pjf8QoP^J7 znyl3~3R>^5cwgAGPV$UlX4yZ9Pd6R;EAq$ApV+(nRXRvNZX$B~%{6~&x~JjT_Sd_A z_qFXiI_>Fg5EJh&eHo?xP8!su!)z8#6P<+?UoU*u6Z+wi4Suw`C^NMry7x_oU%GYx zZ9W+P%jU|Nf8Q>h@y}q^$gXsGwI&Yr`|5IXHta0+_++B}&27c~F+<(JlcU|&_%R`A zXT!1Eh+SM)BTZ*Z*z(+fKg3-#AMgpfG~*~qpS&V1R3qg4*LQ3Cc1*FQ0*fD%kB`lb zy)ww5sWPc5biWbT{fhPQD8z3C{AId&eq_HkSlPg=g@ZY0VX5o!S4l>ObK`ok$|^Tk zmX-zmCU6_qh;W!)%O9t%q=YG2&=tgQe9d=4dIlqIo|G%5&&Z69;k(cIs>n|(X+HT6 zMf{sQkazEkW{&bP?|vetHu&#h%;Z^Z+04AW67_pqHqpHC`dNN!6_oq#MfP~?ryrk^ z;&22Za^CXeG&YM?>4W!jIb~iYzI*;dMhcQh-tlgwWyVm3`O>JvMG&5{WV4)5C?}+Q zQ5fR2=1}@+MHc7~S)v~1v(F~y5nyn6Ublt52QESDagNmx?iOCqkyA!JNr%ZaLLyIpt<>y=j+g)o(8P%@@8#3E3|pvyTlT@d z#)f;jBT=UyJrut0MjsLPMsDG%`FPD}$%;TbCQT=e_Sen*OnQe|;CXR>d3glzCAK$4 zfd_l@Fippj!>Y&HOspVli_;*fMN%)V$}eh@Jvo6!+1hT*M{^M!7TBE`PqF2?OA(3i z&yNykDPdoraRA3ohC)<5CfkGS>iJH#Z-qsI)&f2=Te!Nr7fJW@XVH*qHhi~Hxd%p{ zHYlik!LutMytjUtJ`> zT{ezxbslHleoeWV=-?5$y0%;I5%#sCGs-Kp;{xoK60RkTSHC$&Mscg*_9`w-=915iBp^W ziRGMDUqT5KWCfb%7l6TDxb&_{Mn`*6)6CVnC z+HvVIls`6}{(Xcyod_a+6f^5uYK~<6@iby8D35-xNV<>p;xwJw1vAnq{7C*G;kcPX>rz;;O`^|rlI8hZd`L;bGVSJ8(5p>F| zCzmcB^+*l*A0~rS`p-AGg`KsE7bfm-{Q~FI8?i8*p**$fRUDaeBG&B6PdcC&<9n2z zoVJP9xW{rn_wc^O-ZzCWOA|lf`WYOVp$673tSdVFF6W_RTT+0q8??{tR29uB$QAQ7 z$!1*!BiYZ;<#QJZRbS>Z{rQbve=$Wj7Cc3togeeND7b_UkfNnQaJdV%d@skeWI0^4 z!vh*#ymg(|5J*#Abpi^PVZCdYYHOBvCl*O$7G<$c3u^iuW4 zj0N8HQCI_+G$CJ9S%UcIuH%0``foORvbd(Tgqr$h?9!>0(t=&~=D>d9XAYT_P)*9b zLNyk-vKR}@qM)_sO6JnT9*KP&47}Ggm}Ch{ni>^ct#Gz<&s8%_OCWrc6a$;90ipa} zhZ-sn2IH<&d@*Sb==ya*|KYYfrP}=GX|Xu1ET-hw*+NoJ6}Wm%_@9zjtqHag{4uT< zgsOaf+hEQvLBp&A)`sA&v<4d=wv50VKsbPEsY*MAdr z#8O6h%5Gm2bmxqUeA)yQALwJgT+flDEY2nRm7#N0a95nn<3uIcQs&MO;7z}~@ypsZ z=NN6ZG4<$`pb1$r${k@YtY;AP4C*7VoVC0u@c`Fnuqv@_xIbU(q@TaB(if6TWqOl; z7DoMix!Bv1cVp0Fyx9-X6LlgZKl@{SDWDLoeL5IOcD^jysaO)e=Xi)+6-kPG0L-w$Rav!r>`x?3P~q z`!?D*Esz4LH)M-J5RtiSj9Jtm!7M=a<6p;)mvMhzd2_#dNc&C8#mTUfHBXbUoSISZ zi%J=LBTU+$D0u)=g7hDH^=49ik3p(b{_gAPDSjkcZ5dt(O6CHA*kl%B{W6(2VmcN# zHc4j?G2dql0Ef>jgEpMWi^kl$O-d7*T@vM*rDp31D*C;CyATc;`=^o`a{x;{_Yfw3 zOk!mCa)-y%HFGSdvhd@cLlouDo#D7Kk4y6>oEjpHZNCIKBssT*pc#aYdgL;%?TIo4i9GJ1*@CN4+juR%q`K3FURVp)~@+iAy>!(K^} zeFNhC|Naogq?PxS8ve551^&JN3Ym&dIswm${%zaH@Qb@zp~>6ba-Gm#&78dA(b$K+s3rl zX1QkP&MF3{S@ELowZdXQrreLdqP1n5QO8~&Ghm?O+;!a_*2wneDnSYCSs?G*D-*gpG@vn+|)yS4m5NViYWMd?07 z#w;*cR52N$miHDKwH#drdS&p=ZMT6yV$)e460%mi&Sl=G3I0O8?m8scL06{S@}q!i zmbP((kLU^$;hhEbE_yW#KL3_+ZhY5}UT>Y_&9p+ZDwv?1|vAV zq*5SuUctHJv3HD5M40N>y)V?ygvw;@ZdizZxQNZqo)o7S6p`5XxfbcM8;TyVQ`o32nRJasZ{8RwJXUoLxm0*W!zsG4{S-CeFJCF66}>7|@7AgY zUPIy!?-{2hId>u%LQt)90G5|_o|fUoY%wE`goaxRxkac%B+nn0>H~35Z{$o@s2Tm9 zdo86<5z{@fK&R zLaiWxdiz}SvU7_j^bBE$SreywL-8j>TURwuiw-GBfkq|?(5DU(M9zucSF10xir4NW z8G0K@XW2iFw^#J!-LED34 z@6H(J7DMHkR8kM1tgY3Eile}d4~|?;xJve0E3_U8qd0quaO+S$T0GL z;gALLCosaFx^e^f)Wj&-D?n`Km9m4dv3JMHKL>%(a_nU17-iV4*uC)vY%D-9d*Fn$ zmcuWBqAj}k`J#7}qwr^Ca|7E2=;`19mww%vz zbv^AmaIE|VOdx)SWR6s^UHbSt-dMHtKp}hdK2QdW6)df2dMP9oXrth^tv@b-M*@;7q?Mvr!j8BTK z{xyN+p&44beS-@wDmS%n3PDbo+Y|LT`SQ1C8q&13qETBtJ-szh-ZtE!G(~Q8(W1d( zc;fZ!&q9*jvd~Z%mgiyR+QB z!c&2j)Ux*!o-KHU>_SzO->S7d#-e{upT7!u&_!4FzL=Ap+SYV|rU`LzTVQOm^!ir$ z2^Kt+pECGUKZ2#1v*L$BEQdaAgACh79zWx|U`cDN&~yZ27(h93N2Aw(a8gKPXZdP} z#;I=tEHG=an_b2u^3$pdc=jm}MwwDoR8uIsk$AE@WvFK3rM^x6qs)Ws?Ya;V#qat4 z*Q+0pI(WN8<-(G#x0KtH>>QdiQdWr?Jof?Yxo9n5?Y4R z@enpNo7LDysNt!*#(ZRZ&9-C)!~DQiZTx`KI8XaBuaq}thbrp*$ohaIYTP;gjJDkY zwPACULvQ)eQ}vgl>*Y_w(^R4&9p@-G;7PjJ7qs*1Up(QFaLuAdRVk=tU{U0C9H+SY zq`ZiT}!L-+F)g6A8jpAI&PFjs2}l6jy29exs!Z&Nj|_i7-xP?^-`Evkr7Op7)_ zjVFP4l*KKk1xrzf5CNd8?Pa|cP_PB+N+9>_+@;Nyk<9QgQTjwH9gMs;Hh-gNrpSZP z9{Ua1n|08w^rhp>-4#a6Hupi%&J}n7u9*5FiI=&H% zQ?=!)1XmNNcbuEeqO!@+N(X(z?VpaMo^Dt$3?RJdDfd+U+#-fVMUwE zJmH)YR_iMhSwrl88H1i%CqQ%D#4O^OgvZi9o!um}ESqX{t)w&` zSm@P6!&nk=#1wLZ6Hx9aseDMJ;JW|13FJiq89MzObaNhxWp?%nU42JuC8AnD1vxd_(!0Cx zcG(L^fk&-<5X?Q$vF^qczqr2Msb_xuT@-pZ(moAGAcawrqY050p?$K|C z%S6BvmP>P_TWID<{tAnN@yq2>G)J#u&5YtlidiTx&h{nW=q(QAwf0!(igXge^zT^G ze4+~SNuoQ;uz90+!G*`&KEDzsdPb#4=5xM1++!%N} zujQcM*({yxx?}x^V!!2xPTnjI7JW|{n9;g*7L?5CHnW;yb9P|y@cr0F!Y}hgqKx3$ zbapp=gZ|MeEIIs0FI28GML1BUYjY&yeyAU`2&s*hcFjliwQ)#@bDni1+URel{gC2duIhCm5R0Pll^z zw5bO+1I{s@2jpWAZ6ba}x5xZ^HUo?j%krU_$t8n_Vti>^Q&J+^uAk#L)sT(siBu(K zfIE8+mI^2_2b#SdXF7f$3--7oVR(RB84l!NN#sC^ap~HziqzgAx+KkcZEH;b=cj|# z@8Ueda*lx8i>`n0L`weSq;%1;ZpkAiy<0<{T7zWbr}anPe68bybb2ww`+`p;d!VTW zZhZ?1@}WAW3Ac!#dwnSRPCwS5H_eu(+s^Qs0YUQ$U%1QzdX|{3VvrWA6RJ>NRY=}= z1hZvOj`e^Vc%ue9BYdCPEglwzJ*@{=TdY1x2Z@r?G!J3Z3E-70n<hfs$pX!A+8cfkYLXkZ>Y#@!(p8VJ^X~&ojphOKu*0`hIMm= zDzx&n2&tgE{*l;JW@r$})mD(4j|){eaU7MrhF}-@uFoW4fQkGs-OEkJHwOLw0;^Qw z)4RO&V1tNmqlF!N_4cbIpyvYZ8LdXoZuXe#fR(@xDW&%97i2ORz%us>7mEFN3#E)g zFnWV7MhZjH+x9oPp0mSpgvRZKtXNwG%=Wc{ABzKW){0ip#Df(<{YAZUK-g;u4x=|0 zvzjUe$~oVvdj0g;hYHfZF+SkXt(811sOi>39UlHi$7Iftk-_P3yJ%K)gvx?elh`HF z+wKPDf3HsK#z>QA$oTYO&!aXRDA8fSsR?fot*3}sX z8Bt^%r0;pC1Q-CznT=QvP&VuyNcV#7RrxXrjWiX>-KpnA8S&}8Kxg-4cOxxR{_qR8 z>wRSk3uGS;Y>V^fmFNaoOYdYP0Q|hw)X;u|=DXNSKcbsK%*Ks8nu@(CtMO;e@kEc> zKg+YgELrnk+JlN7GAZ;&ZeED9I{9PQzJ0%%4+ety-MGl;auz`h%XUmy?=5tUGnS~Q zKjMcTrPQdr^Il{m+TB$vn{%jW;U=1UWexoJkL?b{51LbBmW>~O`d896=WQm$*UK(K z-!Rkhm3Pk4wovgRi=ciui)nCID(;9yVc>dZ7R#Puf((x7h9#S1SbXg4JXIRA+!vzg zZyu+MKb7B)Y^^_RYbxgz!*+1Yq#`ysS_AGdi9sxER;O_PdGz|hR|;6m1lfjNgYwZK zmJ_-tjJSmtf)o^<7JfE(MbD5NzKjn;?T;D5SZsv5Fw=Z2Gd~#qV5e|@L5UyLSqfso1Af<7K*rbh=T!W=YZvs+olu}t{WufuG zk+E;D<-N7#-dK0ao*BFMV8eHdZH#`QFu}Ay>qP+a?AvcADJOeR)PM_!wq`fbDTObb^m^`7ph)gps5!rkWkOE-!ZOT9n1YKXaE ze?ivcW3}0w3>^(A^FY^Xs#-BW_aRm$opNrpBXVwj1gq*;!mjkpm<8QAC?v6w3(AMCLEz5KI2G;oUkZ0GB|jP1 zEmJ(=!?0Qat(7;0?JFO?&(Wev$GhL<(m3tY-{Z#L-iHG3f=#lxVwP0gR}#<~X@t#d zcq>{#SU>OyNt>a=9Rrs%!QM09tAmIQ``-IkgQLChH&_Lwry0+jLKzCL%-`L;a%eK8 zjxj4IAEy|wFH-3(G2E)N)qC}adEkwf6#rWj*L^uEulCTbVIi@NZm5v7C|`%SO~Gy9 zKmhO(dTEhLCCgtNFh~0!ff_PCc$9V$F`0FyOGJhj@@^`_K&k%Cwd@j3 zddUidkvlHxB1lFg#iaN`MN?xB)O)(z7fd}aMT>LD`P>aHwsLZH)Z%({(?rn&&R1-0 zU%$zFzTeVx@uw)nPG#}Xe$pMX;$jUReF)ebGC z>L;uC1(++iDF%FY_ltoVQ7i_TM^E-K;LjkPzbhQ0ZQRKr4Pq2qDi2HByIw|`J>2;S zLzrw08Ql8;%rqPTa_u88(w4@|tNF{coP51YQazY^8n$1YEOWQ|`XNKJ2i&fTEe~;T zk1LnzxGjB`c2}i>PgSeWCR{EjI{Gb-D*FS!j0$EPHC8v;rlYk)X(LJD{p04`1rZHo zNx1Xf_ZUGVe&SLc6dr@j-6>+?6HSN~H zfHhf^2s#JQu+2bzX{Z{YQO z%ttM8cwQb92!!5za5GsvLN-m?d~FF-;qe2fWpznHvBnBbI6nMlq=uA{<+*Dzkb!$f z)+Tpc_N2b~%gw;xY5hwHgliayY%a%^&!LD8w3%O%IkKIm^(O7A%C z6|*l6Vx~E423pdOv|jYL0{FE#)bIW{el5A)XzS`ZETEDKLv^4NkOmH_tmk^sY~Qta0~rH?d&GnJ(5^K~%tT z@kx*9juqVHKSGV73_gWJbFB!l)Gf-g;#oCO0+#MPZ0?L*V>ckctI&)H{-UXfd|lx0 zzMEfXsI-{rqbXl;ofLn8VK4nd2Q%!_Z+%8Wiy{RHT_2$tgZE@Abx=#b7Jbh-e#1pD zKjE<=&qisMAx%ilqk9IFW7B0!|1i}rH>k9LB`;oV4@I8t`b53BcN|eA^M)R*nWzS5 zD7ePIvZZL4v-jm+O%@bH0m=erx-Tbb0+++fqslZ*lQftGFT!oc6eir>9pQAF%U46} zGYY~CE|6SCJ0CC-PsW2gOv>0oxa>knN)O-)@b1UBH0}(9oxv`M2nw8v6HQp1oN?># zyJ*>(ZQ0tLnQWObg3u~xi5$wEVr}i_vA@RZ8D;2mu`#253L>-*Ho9b>yt*Lyew?3g zK*yf}>PDx{u&T}7vP${6x8fdr_I8jI@7dpV`SBi~xm{9~ zFo<_nh*?2>IDp?RA6!Sj>&5yI4!Y&X1m3??KZ1XQ{qD=}f=$PtC%Y7EJeW?N)VyaM z8TSbn>8I{oVUFX_@={HH7p|%bc>t7s4=)UUqu7#pB}5=wV^!>xoO?f)nDJmsH(#<+}HR2crQ!~8J1vK& zl1fr#Y^VAzKU`j#;Lu&fZO2Vx2EWq2kMN_!5hy@#=@tw9 z4*e6!wwnsD;6{sZ^uMo)b>R6cTgy(v&5B8lUHJH7$-XHPy%(7)7DCC%UX?Ws7UQuP zDz2^5j*g?%FFBcs(rnzVNg86sY(us=|RGgRxe6gGlV_4bGM1fW#AJ)6= zf(;V23*1|75l_awL&cM1n@kl9aBPv_|D@MP=to#qT`!a`N0s?nc0Wh~_#bJ9Pd0CR z9rcI8!u%)c2XsI^zLGUdO;Ehc59oJPNKVL;uJlnzud7=eE-fK%ZLUuBvzivf8Re1Y znv45HDnOabh<6~=WE>!_dJI{(tdZ~?f7gif!M_)Sy22@z|8B_Hg8R=jqP2rG+sn|M z)6>1ky<}ZrBYL(ALAC!tmy0ePFKv7&k{8n7WNU8J&9^U~HM5I}Q*c z4fewBoOT~Q{q-WyHD_M805z?2GrX0+l}5@4Z|Hi(gNi5Wj1-B31-)$>MtF`H%4KMJ zXL@9nWOw-?@>i?s(|}VmvGN)d#ylyzovwQ*T6tyvWFn^l~~7EzL^;i(S%rN7JzvSpseUT znAC#o*qW$T`@TJb=70B+NZFnw0i?!|y{fWOgQnv0M?fsZZI@3d^+{7X!{&0ngU(=F63@?$eZ*0dKqfXDl zr~+;ol4MQ)(OTXY`*Gd+Wit#jwZ4*w+S7giYdrcuLC(tYu}F z$DLuIo%9FRx(~zufeO{$^FXTnLNO$OK4?cW)aB-SRMw-#QxqOvF^e2OW9w{&rq7>t zwrkTV52T#)0ckucw?7}>o+(a!8AX!K#^_lLiKI*pp_^Nh>m-Y%3{;1JwvpGzl_<-c zl}Pz7Hcjj0Q%ed9xdP08;~sHaC{(TAOu_8lszUt8VxNttDu55v=) zY!!?&acBtHtL+`jv(QX$N{7IKHAaf@2I#JrJEjHO3j3{KzgOq5mJ*b~GpIXo;_&uI zOtCl8%lkk1Xd6$}KoXd&;o(*~SI(T@VnPBzXP;(e<0N=aocT$ej_wR47u{#D`4N9V z$ygs7Re7Hh6ni#x|eUY3RB|RvtCu#4+f6iP!aptP{fbN6o={Q>Rf>1^1vXUrm9>Dts;l}hh2wgr- zO=G7i9UI7fa{&<##)e!g^!ge&b*+%vh*(6z)9!1q05K5)Lpb`TGs%0ce3%R5jnl&z1NP@9Bocz^=PyWMR62P%XxA|MeWpNIu(B( zJ4Tfv)SC2z=AZY(Tr7DZ1`SN*(8xll8Pv+Pg?rUShC!?N8B(x{<6jM5R4H;9>-gCi zrF;NRss@!}^%|e&4$qE* z{~m{z2OSnCt)N==Fi8SP!_LA%qG;&~rBM zGfF`071HQ?+{iQYaxKxi5su)%-^rpjcTXxy*5LTQVynP&DY9P?z5apQvhLh@tpDFT zi~ch0yV$jF3+oSF8wU0XI7`y|{<2quD5VV|gCmI~R6w0Y*RN=^q0X$I7t)=`i2tT( zAIZGHQCQsnhkA~Y$jsc*kHu|2R~)U*XI7bl_@+2ZrEc9Q%{A4Hxp%d3{%m8tT*609 za+=f4TAYwh2dNdY@NQ4;1=5|L&moHGBU)Ns&tjxF|IAnbUu5;|!r`~@7wj3iF2r4i zqC;SchIr`JlEBrH!E3~x)uV-d#RZSkm)j4d)2?0z+1CyXImz>4NaahH0*C31En%_S6EW>30zgM(j&9Iig3nH1`h&c~q?i-j z8ceJ3m-Om^LBpanb-F*&-_88nPcgFj!@tfVhVP62`&&07=$0vZ;Ht)_TOHzHh+=aK z?fq6Zk3^#Ee9qZR{WAVu5L<;JzUR=#EXnCG*yIoJm2)PRaKvs=d|MNP*jH3{pvDVk z3_mZps%4IpFuPD1>-CxQuf*LW?*u_dkzEK{u1Kh$#s1RyuXFpRE_LF6`Y~aCcGa_iBL7i@a%48ZQ`XNL?UnwP;r8zZuBq zFe~tbOQFWeCxmxEX{1Jir1-LeT&p$$BO$83zNJD*1goXssHBy*cY@}1$zJlP@zUa& zsIke{P(^czF+Zy2vGjm;JPB|LzZfQ~27xD#inLz=+DUz#9FY5%EE5K=a0-;rT6<)6 z0MKUHl@P)7-V0 zItdY~bf>V(G$pyRpst|dk*L;pzsXxT`vTU!#m+STV^nk#+g~u}A&0@jJBYBh91I>wOXJj>g>l6Z4G7`AO_`mg|0jj!G_KFbj0_ z*h$0=64p&!W|7rAqI>!>TYC>vEU#d+P9QAR@ovsZODKDOOD*@cZiJ!CN(XvzR+n+S zu|Bim&U4aEuj9;R%C!kr7t7pI8IaN|jQK|QxEHnK{EhE38;pxH%k_-K2OejhotWB_ zLTRthdt+`W2CirAjI&^Qi|W+QH|QwO9B6FEWnuRB`9)fOmg%ADdmREc-ELx;khVyK zf!Jk3sW^LfQdI)en%D91{-AQO?u7r}15A0qs(b4^@4`uUxBA`AQ-rfWX~yq>Scw>QoF-QKy{-mMKjue}44 z5li*TMIWW=yP^NGK4qniNvOs#W)NhsX=;T$jP0PTL^vZfS94qk+sHUZE|h24e7yPT zwU^ozD3rVOG3gJJ)ya?f@J5B;!%|%SnI5-DYq6+9pu=U6%ey~6i&F>NqW4Gq(ULt0 zMNtd4Q*;=L;z1R2ckE46!#T??iWfZzaicdIWtsP1Ud{th!*%5#!(o4ygR{xjtKBdE zugr9N*wzwm@v*SjZk>c)e7@yE(*`q z5j`7JwFx(u>$X;j*K~k>V5ZsSh1}u)S&ka5Tor3Jg(yBM(pM0xKn`F`99z{2dF>l0 z@#s)C50~71$BI7NhZUip(w%?|X$bMMdw{Wgm~_1qoxn#Th+3eReA3=E0Svfxo2=Va zEe=MKS`PLi1-2)6bnXV|j!*x0ju@&;cUGza!mnl&%#QKy9#a_Pkhw~L4=B16C}1Z4+0zrFOf zfHThG+b-BAy>Q|Ittp*7^9d;9L1wT+WIynH z)~?KO-+2eeMc3;1~Ovq*wqIbUTb%^-TlCh1&aC#)ZOy|pv6A@%J9Icl8 z;HK1cwt+9PP8LM}XD3^J(oV+WsAH+@9NOt_60t}lihK7FP~ zhj)o~&oqM3Gf924;&TwAcIBm4*V38@wY%%Ly{17}IpY+2-1_B4tXFXQ*^7LSmW1Mo zF`&`NhLbk~EOGhT;Uo8rw2&2q2>s?m?2nQYXS)~bGPpb%qv;ILV|&4}SA9#b-_+4n zEAcNt4y3==1eCv>-;ms?Ph(-$nM#`BGQlxr zv%mAP8x&i?A#gHf{vet%)L|2B+oRhMFLrHuYF<|KMy!8%M}zE=>15Z(>T0tL)$u%! zuGFofGp;cbdi#!Z(n6(F1LO1PqM#O`DWHJq!WqS7bo#f+M4!DVd3MQ5Vc_?1 zC2Rg3^mjne3YEH88ISn;?OS)>2z@I(4+Mta-uzAjC}Y15-rcJxy(`7a1HD7Il>nXbk)M?zuWkn?SMXkgMSF(LCg-Su2HB<5{USFSt0P&h%Gf-rbl=5BuD zcG*C^t)Zj4M?5_p+rIZ%@#*A{zvrIR-#w{V4tw+imfrb`N@dGS2DAdDCIo=~%c6{mVJ;>#p~WZOqp5gCzVJfA=Z%>S{3fZqH0U z66Nr}-&fLlkSuoN-J6Rh*R-7)ZN?=e#{EoOab)T{R=+dGGq;A#F{+>?suRD#@MPm$ zdxN)TQ0aVgb-&1~YU3Y37qE0zG_|rQKR+?fAMJ=P63}oZ;Hb~MUerETuZo@GFjd3#A>`v+L-xYEkb=54j{!F`)$!o#6-tcH$jPe^ z&LG zc|jDx-$)n8a{ux9LFeTe&fp0}+GvvNH~K>y9r3a1 zmf!go%Qsi4I%r$9IwhYcZk_ULU9A51^gs+dSY@GEy|s(jxA>gR=Q|fIi++(7^y2;C za3%oZP{9aH8^#kCZ)6A~H@QI;y`*#s>e(64y6Iy|c@V5uH2@yNtgeZPur9Vw=R4W(?Hs z-_HyPK<+~uYJbhn-xgCX8>yFBCkXQ;Iz5FQ+t@KwC&xl}Ks@xh%NAQZ0Yq528o^6^ z*k8hU9^UB>4AJKL;OE&;8gymcL0024eu_fg#XQ~OoIBBc;_dEw@xG=3`zhfGi!1O$tzp-(gYXBCeizsCs0^1Jx9f1kwzw%?uhqg|hl5P3t z3OrpE(t-;bHV2Ru-n3YkI1|ha3#}e`OaMr4$vEtm#z=KsC+?#(?t<1}w3`Q*YtI`J z*`fMW<&w!zHvO%CdEu+FGZ_DrJdmCd<^hQ)UvgWvLW$;$OlIo44LB%tPj;%44Vi5G z6uuCv;BY*A5S@2`Crm%_NrR<~hH`H#+V0($Dt)6i?_Haa(+!`Lri_N?JIqFZxD}jG z=CGI!7P7)o^%a(kS-cD{{($@Sh8s=I#SSsy6D*7@@@zZZ*gL8kRC-&nk@5F5L%EJr z&%bi`Q+Cii=)!5fr$O>VdBw)Yt&RVBgAY-`UdOQ7Yqzz&m?qpot|weef)oU4NMHvmkhDjPx6Bf%G?|&nkjqSO?7J7{~{DUiil`m@0FPFg;t#Ru^+x3ix9a;&vz9hIFkz6v7XL_w;2bA@y?B>QZP3)V<`TI%I zv}kZVa(~f{V#Wfw$`mOSQciF@*>P@*pvr$8t3-5slpbn5N;Vsnfg#EG-tYb1yPt_! zv@UOd`qQ7j9Iu;JpSgXHgmzk&y;uMG<%{(Mqt&;*VY?pEzrMO&W7p%6SH=7FU;p*X z90x+m2gha`quP=$nK%1_?WZb?1G(hGDJA_`Z*FIDdf%vka>24wUYTDi@oKJ1$aAPx zD(RiECb~ZfTHChu1-_z{Aiwr&zjpV1r1k6Rm}oD&@WLfI>irPXsz&SL#j0fWl%mCZ zCE2>qugI;l3tlD#dc|h~Qfzx}>-1B7Dn1gq#C2n*?W*mR1}WnwVO?-1Lnh~|>fm^! zWSjI|0~m?cgHY<%0T+GngCD$9rkan5wyxN%L|a$wa4q$^-k<*EU;gFc_kQp9?#Wnp zl9%o($Q7>%I5IEfzZ;~x_Eq!GVJpV%b$pFH*3-QW!oEePP8g$666)qpjn4cgh~W0- zlVIOj+eEJFTidfbUt6(tC9Y^~)9`}BpZ@8eUMf@4ck%k`zy9lcGijdPU0lReMk?SQI+uDT?9^Q((N z9y?iUiU&6rSJO~Fnhw$ZVelQ>_mQW^@Z};TT4z;Egly#hd1_uoWxtgP>=B%Vx z_t_P#$~6IH?$MaE*#MHuwkN(j1k5n8=DIRX0%rW?aXFP+wg0y-VW` zJ$OMY2zJY9LL}zJj#d@U8HX2`45Si81)I|i+mt;x=-0Cv@Ts{g06+Q3Pu}&fucAAh zSq{DrTNSO_4k@HB9TR{soz0GbcQVMEZOeXj3DG680yK^l9+#4}lf~m%#)+9JWsork za;KaNe1&4p;VJ;sqLgt}^>O;DKyh+i0CJG52SwKTx;1D$d>?iEi@*4byUOymx4rFN z)pyl>UHz*MY}eND;~)RHwGjr{1oT*L>T`DD#7Y3|KnVF%8}njam{$j;6G}d3vhSJr zx#QPnH!G8PID>O?s6pP+0fu{m~!&(M1O9lz<1 zFXn;suSv3Q&QrHri(gUbz@*w4fhNe8WI2>_<2cYdnd;RcTB@APTRE#<*Oqu)(R=M{ zUwb)UiL`D9st;LLa;)mL#;q}RgX^kRtF5-w>-pkq-jDzIk6)_WN~HC>OrQ9~C$?>>!yna)Q?FzTl*7tfz=%hYx*RYWJt zq3rs~#Clc(_th__*B?H4Xz^NAY*n@Ow~jd(Yu@7HKu-~`S5*-_cl^pusx2iSk5}V% zf_Vb4s2l32fU=wG=yXlFoGw&VwyBJouL8U)^W3M-)jBwk5<;n@b-xj{+SdoNfA9x? zARm76SYn-bku?E9s!WblLOT9QCw#r;Sn)%B9I4$4&r@>lRVIZp>Nrui7%uA-aUpM>m#*x^v!g8$Nb$^QEda&d?%}mnVqC( zJyP}UfcMh&rZ>Im{Gc&x8M?it>gy1GN*Mf%rb!m}O_7@eBAqMarY}RTE#2C>tyiVf zM91o1_xjZbzNpL8wyA3D*nr2^7?!bHXaHNuoXU1Fzv!kbb!ZHHWrZ1u zoB`GRE>So!_%4A70-q~=G z(Nbkla&~oAAcXuBf3>xB;E?Ru@xXc9pNaw>*Liwqsft^r)8%ufquE+bNCkuv>}WF1 zIg$GmPnF2{I+>f2FzLoRGU1%{HHvj-!%8OgaQ>q8+0vi<$)6nl?9cve`yz${%cQd# z*VzKcsUHPreV!Cee6bVToAb|iAKI2Osk$U1asUn$u)MLaUe)^?HNP{K8lUv%xr=t4 zl5|}6w?1zW2TF-i@?L#x(IK`a6J1bEfQLH+f0_M-Kb_f^#<l9C&sV_0{oIGg z^)Qs8^(*SX{oB8Nx$$+;kN)V7UMkR{KmOxCzI<YNz43dTzY2vdIVU}dZxqs-~ax*L_(fL^<}~}uK2FI$=9>tNJfBjREYsMU#I8zMo|`}zZGBRPMPbPh zj=54=&2zkxW8EB-=bY=#6<@Aqsq!#kHSfkOTEJ0X0#u2pWUdPp%&*3%xm-z9Wl`jU zi2Jy`TYm=}rB`R?#BVA;#k#YTim_7{kPd-(o(q1LtZ7`LNh0tX@Z8B)a@fep0X5$7 z<@QyL>U|GCBEZeTG3((hMf_RQq%w6N%KY510*(m~c0g0}lU+IxRsK@ss*^f)K(*RW zX{(eLrVPw{P=Q3wXlh(SL#11_^MwF+M>kd_2_Xey> zjmNpWYj$JR?nYI1#Umx*ZlWkYb&TyrlPZtnE)P`UMn%dPm(te!o|58o4qFPPq#Bd9 z0{jEPy!Afm*=L`<9Iwxf)-%J`nEJ4GU8SoBv8?ttzVVHh-*~FF zMeF9*b+6t;|AhFwmZbNTlKs^2WBDua;Bjd__w6R>iJ5Vb6sh)Pbd~wap>7=aF-o|E ztiIaTO{ypp%2G&(V^B|r$Zo5-5&?hhV%;ge?ig3Ptrdl6>F+r z9Xq{P2YyW-wkuhSPF;eypg)=|KngfFhIw|%yTn-Y*$6ISldeubRh_sW6GtH_uXSBK zSa*M8g-U_B*0*jS`qf|k)w>@|OF`pon%2tm9U!@ika#&tK$`r)^BoW8JGINg*B!d| z6^w&egj<|4rb9;_lge_nb5-cZPLmOX1No?NIKJ9Z{*J-j+K|?Jp;x~0m6u;(U$nM$ z>*qPwrSEm`-AcTbK-6MGVkZDA&iOohuanBPLj5$4Nf~J3aC~Kclj@Tac;cXho{mRnQ`T9n z)1fm4isI3&rAu%}2vCl99d~*teJ14QOo8ww1&d?q5K_@J65^V$Kz|)&i(Aa zZf%QhlE7}VQ)EhMLDNdp8J@Rg$V<&*`CU*YBvx{8+&kX!j>DVZ{N}@B={JA#H}Bm= z&hoASyAEKJA10OA*+nH)N~De$z_?u{wTqJk_N!0co2L{}$XtJX-XWS&oEO1Y<l&Msipy1!Y(0dO=GAssuuE(Wd%`swz{B}U?_#-zAc6;~x9{;jsl z=S=)FnqA3tGz~&NcZR|RppF$3D#4MrLQh6bu0|!R3#P8hDft{Q*92WhNpDpR+z!Bz zvX`2(3mC4Fsidv(IHsd1!1H*glcLKuuWkvd07z9zImBJj!dK?O#y}l#o(sxOPbYWL z(ia~=m68r$9k?i69hxDnZ`ZEN-4j~+uHS8{$AGO5eAf@9Em}WzhHFV6=+?7~^e&(} z`#Zm;;7xk)js<0+92ZA8Cdrx5@eOZy!`V5$TiEe<9A)q|2+TT6^uf6}N9i#cQ|IcD z$w#rR+|^C3Q;n~&E0Nyyu6G@N;0J!-l3eR&YSyo7teah@KI5cII(B^0Sf$UdKa-9) zM)Aisrw`7bCQTTBe(DzOeuV`ow04Xc4cW+D!cAmc;{<%=b~WB19M9Y3?dGGL9W-7K zU0F%Bp8K$_2ChV3Z7Fb_E=gaf8zy72i;4?Xt*(g)r7ymA{hi!hU@SUu=9VxFsAO0i zt9Uu2m@0csVDma$CBd6vc`ZDL^-wlawou2)Hfo-trSrN=yuJ&yo=&uW42|bL#_l!W zC6Xf6pKb9N5K*Lj&wg~3O8Hg!tFsyRpHkp=lNBlS!S)>kx=RbcLv+g3VJI!m3TLbe|#DTV(gviIqd=hb{k{0E%-`SQpKCNLI zZ;MWjiDQ}!f=UO4HmwxOaVqKD91aK`pNyb%p9Y5u>`rD^ z39ktn-Y&iBRj;~S*(+LKV^6-qxyWxSdjb?SmSwprfv+`=+opP|_({NruUz%3PwAoh zaoo+p5TghpB#*-}I1Ujq8G(5_H0>%($3xAXCap5Q0~p1>$RVVXfjrjhy6~{>B46LO zMLz4&xYI%TtwUHfPbCVkwdS$@Nf(@-;-uS-9S(hE@8Y&y zI-fdH!Os0%-0hN+*_}^hb3EQC6j6+(2&?9~vFbRCXD^1|HM;cJ8{#l^;mS#J7u0z|xTd zk~;2u&2w-Z&UZlTXtG69=M!Cd*ObQJV5i$hrfgITcMh8Cxw$*I0ck}_!5ZD?xT z#kcErB5ri#l52<90J}V$vDJK)KyDlc-6Y6&5Yw%3J&kC6PQ(eRbf^dYulHE%ft8Cs z@rh3ye&=_7M?R3n>9eQ<$SEmc*bP|4v#XwN7m)P%CeZo#`q#hyJVB*9zi>e~i;8FnK<~dzi?`xgBel&VrxLD87TeN<+sUG*W3rfxg*MJ+&dko+v zuw?(KW7GrbN$A$l|NAqX=fNI#GDb?Fa>;X3Nl$%~v2G6Q;M$aa-MMO?tYbp!{m}f=W%&K*`bvi&;WN&m` z1J$SHE@0faT~ttyrA=t&D$lvu$}qp0x8v9y@JT4G&K<1_5bGD(@xxyAITPExtGG%& z``YQa13bkG{}Q+*a*60-RK+6#kE(7w7J0Jl8&?kN7=J*T6-Js=A?D4z)B;d;m!N8H z(o>}4Y06jWoqXZu zb@H6wCcL_JF~D_xEIk(8aB%Xm+QucNl>rsc6yzu?p@BmX)+6c2^PEmh2~8)qT7B!i z&SM|-wVs#v+;h*}YwHpkb#&)7Uj+>ZwBnm=tdhWi?6Cpq#GCsPIdF9T#W{6;>Pi_i zK_jZgPbxuIQIZ~szvFivZl|tsLdj#pGT&VUp3vIftzT)c2T&&YX+2!|yR~;d59|34ti!q4PIUlPW4LPY>y%tVxb2$in^dS>dJGR`vztNz_qZ>vMqJ{Q01mR9 z?S|^-ac)b1q>^iQ%gTJ!7IRVEiOu}F?d*CAYdz{Kt*vM!QGI27y-#w#A*9y1vmP9G z4qGT3JMnRfnH`$??HjQhNOZZK=0FcW7SWw=6ZhiVDE zIgyG#&+Q!Ug0-t26MdcGmF`L} zbvyxrlCwj_m*+kBwRJpK26l<&gfI^m&=cj4Vf&b5U>o*TU@%|YvN^!$^i{Gu>c%M_ zb#r+w2?!>7@|Oz}@H$HVN#i+I`0Ef#KCasVnwpZEOi zOQLQYFx8bS1M8jU;&BS9F5uX1ydD<}$76qShAX<_ze8yIv_nuf=n`tm$F(cqO@ONa z$NART>0V_@!9B@(HIL^^csoNoo|D991G%ck`nk%?PI8Io_&cYlq*p=f_$uN`qCkVk zcR@JyxzjtXUGZZ1&M&H+ml!E=d9JHS zF7O>9x?jUk0mjZ@CopeC6I9%t%l$5(oG+8Tm5h4jpiKUE_Mgb&cu|4Q{#N6$o!4=0 zDrSwjccZIfnA#G#z`}j5ldI50>7yKkeXIx1Q;Eg;D*cqfSzqSGYw3_Vk+bc!eAGHS z9$i1{=we6NC-rypciJY@0Tj|nEk+@@LgU1^6TCB)9pko#c--+w`s@VKq^hzW4g_{6 zCDbn8Yl5#!4vZfaJE;x^tGJ{KNsSb~i{0*siHKPEVf4em<#6q?q6&Ofc^2 zlu=43qw!^FSM`dA(#5qY#B>RQV_b#eHbz%9m&c>i*(X!qoQK=0L>&-t0%4o0M2Sz= zj)|&bCFzXgCw@--Y7HE-@);0{Ii9<~#)Z4ez{((cC_~`6A{QhMARO9NU($IJyttkO z0B2LhR04Gg$#d1%X+mz+0(6yWljTv=dwNBT9WRpv$MYghAF9}P-B!X z+{W!Go)mvIMkPA8Un-LdoX%*8Qeg8qH;6Y9p*K?NS`=91j4W7@8$v#T1m~H@)9rX zuj8FW=&Faay^~#Yl5O1BjuN1$J_VZ0Z|dX6(gZl%96Udz?bJkA=WKL{N}dV2sDN}O znzsuMeL4dokFG88;c;nQt{>Ors%`=yHJ*=geA;D9sRNlBqY{htNq)GtBH`NUw!yGL7onUI0#bWySY5RLpLR7f{4jcPDe!wa9k4XkOHV(u-uii)6W2c z>rOj_iN0#5v9op;?+?Dud2&SE>D_;8!6P%7CJsl;a9s&6NFr*fU6JN^t*s;_HP z{HJqMFsg*=0`3}`BpB+cfS5_e(^*tuP5_+m;u_nO|LXYUl;f&|%qM7`f}O`EKFqU2 z3&+z1{gl`-$D{tv*OX8n6g)1t6k0L*xV1Q09jT<|_MJq+Ib9-RP`H4=`JJvVpcE-&Pjhw? zZO13O_H~uX@pW;R=HT&NWlEGBn9$LcXr9jjwTt#UW082fhWzk#GKclqr7nOlFdPb} zYK**;ye`2KXgtaFK4#8jP2D_Byj0nhKkV-_rNB|=llL-3{O~{=$BR?Jo`M;9bg1Iw zrgq1t#ySV6vHXgGpLlfJp=6_Jo1-T&Kl%DeQHt4VwV8r!wb=)%Oq9K5A3Blewu#uk zQ@PZu;)`wgfXb9`jQ$J~HE$Axybo97CSbz)IXZ9bBGf$Rw`sz00PpN9rnDO;e2jc} z2gP(!LNP#`!+A@_3DoWF*^nyvI^`XqUP_+Rl@r$SRy;cJcub&l`lW3+>x$z$ojSc$ zzp@X@bV-}g|5|#|yb@rX)ZTAVGI+J|y;%xP)iw>=UB3%{2z~oxoG*<{n7@{WpGjLYK|JyQ8zaACt1n=lSDtcEtLzx6r4CWA!kBK zMz*vSQnH+GikEWqG{C2AsecLrog-BcFT!stbM8HjkAGu1b zf?es>$?bIN=5T*X=#I~nChJN&gEjW#vM~i6kD2V>CtDQf_-W!WA3pDbiyi2yStmPf zjoq9sKvK0x#B1tMnddwa2xKwr6TC3Q>F& zKXpD?6!UcCl4Ii3sb0>piI1`iwx7@btLTZZBBxi$=dIvQ#p&9MC(7b>#lyvs(jCWh zoGB2+c^#m2c3XcBE{(HNYVzSQt6ZuQxL2nnFNh7mJlH50;N9FqSXT6`FpJa`1C)jmaTb|R1Cv787=W&#H_x7#vJ_+bAeai)pz&kiaC&gF=v@4^mWVfZ^d9Gd28td-s;{{Il?u*n1lh+1+ zHxGkpC&}1P3Rrjx=HyRMInC#Djtj?w^_~=Kil6DU} zv9hbng9{K`hB=@W)1-V?))jv>UR6f!n+A@P;o5K>C$SonsH>PRAUeHW5cBx7=1zu^ zRl@VfB$Nusj&_oDTIpR8xK_rg z3kG&}XV4_LGlS3NodO#;^$f%Q;&CggE})+K*;K0z|nAOE^JE>M*%uzFPc zH8j;rfe9XWdQ95S&92}{Cgv36yIeS_Nu3bYer(<#s)~{bgQVuDgkb)Ow|ZaTWT$h* ztH|9=fdM~RKh8xS+^!O}^P@{t)yHjJrFL_JwY8{s&)MpH>^! zpMrG9!Kr%fRD{IuvE#2L^*#{gUQ=;#enRq{UBj-ucXj0Bq$kf;uZbTg`h>sYr|etD z4=zCPU>4FLj#J8wQR7kx#%)1oMAe3fc#R_`{oULyfV)83B^7*VOQoMmD&(Q~yMCsL z{kk|)enrq$K%in~A}{HPYdNJ(PAo3VlTunwqEQY^4poXP8OTE=vrF)lc%5NguumK^ z<-O?I_k>I-8C0;PU{rK1xLj+m-Gol#ISHL_kKui9(cJ@8Qb|u+KjkP^kmu|qr{cpw z?-n{SK#fTOk&>GeI+5yE_E9_&a3LS3Lz>HgndT&$p2LnJoqr?fnhL3Gfd1gQJpb+j zL_!^v^$O9PQryP@;+(0@6QyKS>zcG7kFH|!T9hshJY3RrNl?U(sz#nn7zq68VnOlk zb}c;5C8yg}Oew81(F6pOJ-EGnpbAcQZPOr3iNe+KJV0WVtn5%H6P@jXV3L>#jTI23 z@ru|+%%A&stHS)T>M1?hJ_$^@it?D6pQ@(%&Eu{*GCA2UdDDD1FHx=0-ZjpHC*!Km z8CM1Bq>N6HfgRgz&v*_7sME`lOWF<{9Uq1M9B|2BX*a;+5=U*xU1fFpxj)uR=~H~T zEm2pc_zsoFCQ{8=f9*GIlcC*0+34wgfqFm+Q`(Ysm8c_CnRX0{Hc)&xQM-z#5{Jij zJW#L1E3HTA=Cw&xPpbW)hn#7J$Tu-T!6Mu#?C5vjGAPu*5TQx@=C-F)bt~M%O)H+p_QvC3>_#vNe4$D9uxb|xZ=baLQ zC`uLQ8U?0{>K4nWhI3Ssrv#t2qpS(3z)1+4sPh8`+LH(z_ugj*aMW052Q@AwyV`uiZEh18HvXZ?NkqSypz;8w?kHz)5&0YiiZkNt^iX~ z$gik-&*J!Eg;C=f5T_vFDyFNRYA(j9s;V{CKDZwjpvtLIk`2;P_p5YlK<>v^)yLaD z<$xr=NL4yV4k6tZayO`vhmzy?vOS$`uhB)#->HVvYM^3Ht<%j*I;+?{9`%(^=iieO z#>yoJI3zqbrM+G!4&{ZWB;%6K=QV#PXz&;KoSw{kC*jn*iH^$gPPa}b%TBU7QtL?T z*;!|jQ{>{y9rJmLGq(;|nd-b-I477&q=Q+2LQF@T5USsuD+5)#TVn!)N&r=RlQWS$y3u?fyg0Fz?J>62pc)J=BnPB9ly=X1a3W`&h3 zUIg#6aSqCKr5VRM{%Rg0ZgO%57DZf56V*KRxk(3vB@4&9BuaWs+fW|sq0W`A?9W7# zF>a2FJ&ZM%peKdIQ|F`5Yn4}RDlsQAd6B7fPZB%HNqSA4j{HV1Vfj2-Fpc(Ec5B+O`WWg zYxh0fR7TR-*)|2Q(#uuGZv;(kJQ<_x?{*I^E|OgKA zJV&YzWw@%g!@J|*_*@eVZj4J51`NmdF$vKH*%Ta-F1tL`Tn7eftUKmi03UPgrlfGy zl4U#H`R_(;$55mssigsz?S za24;d)o%xIr+%M+Y4UB&P5$Ni$tEXe?6yOX-A^VY?u>a3rP=YD=#x+jk0*xaIoDJR zSE&;z(3{9~pp$kCj*@Su;FieA=J#DZ$HfiL#ql_AvZKJcr=$Q2K_%S660zbb0jfWh zZH2ZjNn91000ruI4QO=1=JeySNnT3q#EUDiqa8p|TcT^Js~TJYluzAUN0WF^a~?d= zH4UB^F3ne2u1anl<2bj#F8Gk2EB(r0xaDCTQK#MHV&vn#j^|Errs{|5R>xDiCh(Mw zDbds1Q~(t1fc|70j=lHGfk{vbXw|RgCvsahJXgrmm0_jx#Bn=_G?}ZaJ0~U|D;^Wn zO?onTD0{KJJHT|lP~#IgIlgI4j$A@bE7C4qws|hQQ9ugFE|_qFb;u0cFpe4VGUQ}jvO0+u=DF&Vp?AlR=^_M1tQVD9FpYL(_q0$puu&tl7o zx+HLR@$e)8O;OsJ-2Q#!benA-=on0@R-LQ4iAN%IhT9rU$ZbW-8Pg=~cu(GkbUYk! z5V@**t&JRr?V$T4kdwW*I`OVxqDx9x6|XxLS=kuqqpLFgH=M+SJEN*#Oi8vIv}n+E zhb|^qm(1LbXoor{u;@qyVZEY$-M5NUpF(cb+r6)4eI~LdZBu@%qw`l6;GAGxHR5=4 zl~L)ze~J%}U8lEw|NYYY+Sk7JW4ABQpu(*D)Mz{(`yNB6gW{JG-tkat#bf4~>@c+v z@~*ZCt#Ngi$3$+omZFK>o-`>tJciG;`WD zwkw(x5RiSo-7)>jWnOjOR;Lf$zWgu_e3C|Y!Fmlcs)Z)>O?pf!(qr_(zM@_JyW_jW z>Drjb|DVXk?PC)+$oi{Ked<$hUFUZn+CKHvQ~$$l$MZGlF#@g`zq8m$z)r@hYU$c0 z6?L)(OxUiH;`kj(yqO;d9_}X8nE$Vnvfno_k)FTZ)%|R>-6hj!KJ%I9ZU_7?OFk?0 zBv9&`=@3o~)wj!Qkqg2^YOBoKO_j*k62IY}jCH!GEwCe}y9(wf0h~{ou8508|I^Bw zYI_O?eCbPH`pC;(_Og%O9{&8T0bi!xd^^-qh=y}j+hnVT{C5aZ_8N3|Jlz&T9q;Jr z-V=Qzw_7_GKivA#`xdnxcg}P^Uw`XVN6Lqra-P2(|3BYu@BUA0lpLOZ`srt0@{*VQ z--my@!gqAeJ}Pdcckm5cRKbGrATX+Fn9 z0_|(IyGo~SjvKd1(QV&{Z?|^;BUYrlwFJKFqQrl?efeK+c|3EhOBcgrK;=}Q3sA+w zk*ZoQ0NCN{PhDzOdMKxK6*D;{1=U2CF7OhME`hqaDp_1I?IbH#J8W~dR(dE(dMEz( zS)8A{{ri>MB>F$xpph3|^P1QE`&&l;;a1qYZ%^NI`}cHm=wqCQJRUQbor`@%*CPJ4 z!93x`?Gw434$O + + + + From 2fc98b948f66978f4d287be8365e8b064e2fa966 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Jul 2024 23:23:27 +0400 Subject: [PATCH 142/223] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 - .../domain/tokens/GetTokenListUseCase.kt | 42 +-- .../CurrenciesStatusesOperations.kt | 53 --- .../domain/tokens/GetTokenListUseCaseTest.kt | 322 ------------------ .../feature/wallet/di/FeatureTogglesModule.kt | 20 -- .../featuretoggle/WalletFeatureToggles.kt | 11 - .../organizetokens/OrganizeTokensViewModel.kt | 40 +-- .../domain/GetMultiWalletWarningsFactory.kt | 2 +- .../implementors/MultiWalletContentLoader.kt | 3 - .../MultiWalletContentLoaderFactory.kt | 3 - .../MultiWalletTokenListSubscriber.kt | 10 +- 11 files changed, 15 insertions(+), 495 deletions(-) delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt 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 7170767402..8c540a7cbd 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -19,10 +19,6 @@ "name": "WC_SOLANA_TX_SIGN_ENABLED", "version": "undefined" }, - { - "name": "TOKEN_LIST_LCE_ENABLED", - "version": "5.12.0" - }, { "name": "CARDANO_TOKENS_SUPPORT_ENABLED", "version": "5.12.0" 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 ef0b96e10b..9cd14a845c 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,8 +1,6 @@ package com.tangem.domain.tokens -import arrow.core.left import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce @@ -12,7 +10,6 @@ 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.operations.CurrenciesStatusesLceOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -31,29 +28,7 @@ class GetTokenListUseCase( ) { @OptIn(ExperimentalCoroutinesApi::class) - fun launch(userWalletId: UserWalletId): EitherFlow { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens -> - maybeTokens.fold( - ifLeft = { error -> - emit(error.mapToTokenListError().left()) - }, - ifRight = { tokens -> - emitAll(createTokenList(userWalletId, tokens)) - }, - ) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun launchLce(userWalletId: UserWalletId): LceFlow { + fun launch(userWalletId: UserWalletId): LceFlow { val operations = CurrenciesStatusesLceOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, @@ -78,21 +53,6 @@ class GetTokenListUseCase( } } - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): EitherFlow { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } - } - private fun createTokenListLce( userWalletId: UserWalletId, currencies: List, 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 24bb880d94..a1c6c428a3 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 @@ -24,52 +24,6 @@ internal class CurrenciesStatusesOperations( private val userWalletId: UserWalletId, ) { - @OptIn(ExperimentalCoroutinesApi::class) - fun getCurrenciesStatusesFlow(): EitherFlow> { - return getMultiCurrencyWalletCurrencies().transformLatest { maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.fold( - ifLeft = { error -> - emit(error.left()) - return@transformLatest - }, - ifRight = List::toNonEmptyListOrNull, - ) - - if (nonEmptyCurrencies == null) { - val emptyCurrenciesStatuses = emptyList() - - emit(emptyCurrenciesStatuses.right()) - return@transformLatest - } - - val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, - ) - - emit(maybeLoadingCurrenciesStatuses) - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - val currenciesFlow = combine( - getQuotes(currenciesIds), - getNetworksStatuses(networks), - getYieldBalances(), - ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> - createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworksStatuses, - maybeYieldBalances = maybeYieldBalances, - ) - } - - emitAll(currenciesFlow) - } - } - suspend fun getCurrenciesStatusesSync(): Either> { return either { catch( @@ -360,13 +314,6 @@ internal class CurrenciesStatusesOperations( return currencyStatusOperations.createTokenStatus() } - private fun getMultiCurrencyWalletCurrencies(): Flow>> { - return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWalletId) - .map, Either>> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyCurrencies.left()) } - } - private suspend fun Raise.getMultiCurrencyWalletCurrency(currencyId: CryptoCurrency.ID): CryptoCurrency { return Either.catch { currenciesRepository.getMultiCurrencyWalletCurrency( 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 deleted file mode 100644 index b5cd747c50..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ /dev/null @@ -1,322 +0,0 @@ -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.TokenListError -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.NetworkStatus -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.repository.MockCurrenciesRepository -import com.tangem.domain.tokens.repository.MockNetworksRepository -import com.tangem.domain.tokens.repository.MockQuotesRepository -import com.tangem.domain.tokens.repository.MockStakingRepository -import com.tangem.domain.wallets.models.UserWalletId -import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.test.runTest -import org.junit.Ignore -import org.junit.Test - -internal class GetTokenListUseCaseTest { - - private val userWalletId = UserWalletId(value = null) - - @Ignore - @Test - fun `when list ungrouped and unsorted then correct token list should be returned`() = runTest { - // Given - val expectedResult = listOf( - MockTokenLists.loadingUngroupedTokenList.right(), - MockTokenLists.failedUngroupedTokenList.right(), - ) - - val useCase = getUseCase( - isGrouped = flowOf(false.right()), - isSortedByBalance = flowOf(false.right()), - ) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when tokens getting failed then error should be received`() = runTest { - // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left())) - - // When - val result = useCase.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when quotes getting failed then token list without quotes should be received`() = runTest { - // Given - val expectedResult = listOf( - MockTokenLists.loadingUngroupedTokenList.right(), - MockTokenLists.noQuotesUngroupedTokenList.right(), - ) - - val useCase = getUseCase( - quotes = flowOf(DataError.NetworkError.NoInternetConnection.left()), - statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - ) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when grouping type getting failed then error should be received`() = runTest { - // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left())) - - // When - val result = useCase.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when sorting type getting failed then error should be received`() = runTest { - // Given - val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left())) - - // When - val result = useCase.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Ignore - @Test - fun `when tokens getting failed on second emit then error should be received`() = runTest { - // Given - val error = DataError.NetworkError.NoInternetConnection.left() - val expectedResult = listOf( - MockTokenLists.loadingUngroupedTokenList.right(), - MockTokenLists.failedUngroupedTokenList.right(), - TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left(), - ) - - val useCase = getUseCase( - tokens = flowOf( - MockTokens.tokens.right(), - error, - ).map { delay(timeMillis = 1_000); it }, - ) - - // When - val result = useCase.launch(userWalletId) - .take(count = 3) - .toList() - - // Then - assertEquals(expectedResult, result) - } - - @Ignore - @Test - fun `when list grouped then correct token list should be received`() = runTest { - val expectedResult = listOf( - MockTokenLists.loadingGroupedTokenList.right(), - MockTokenLists.failedGroupedTokenList.right(), - ) - - val useCase = getUseCase(isGrouped = flowOf(true.right())) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when list is sorted and ungrouped then correct token list should be received`() = runTest { - val expectedResult = listOf( - MockTokenLists.loadingUngroupedTokenList.copy(sortedBy = TokenList.SortType.BALANCE).right(), - MockTokenLists.sortedUngroupedTokenList.right(), - ) - - val useCase = getUseCase( - statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - isGrouped = flowOf(false.right()), - isSortedByBalance = flowOf(true.right()), - ) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when list is sorted and grouped then correct token list should be received`() = runTest { - val expectedResult = listOf( - MockTokenLists.loadingGroupedTokenList.copy(sortedBy = TokenList.SortType.BALANCE).right(), - MockTokenLists.sortedGroupedTokenList.right(), - ) - - val useCase = getUseCase( - statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - isGrouped = flowOf(true.right()), - isSortedByBalance = flowOf(true.right()), - ) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when tokens is empty then not initialized token list should be received`() = runTest { - val expectedResult = MockTokenLists.emptyTokenList.right() - - val useCase = getUseCase(tokens = flowOf(emptyList().right())) - - // When - val result = useCase.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when tokens flow is empty then error should be received`() = runTest { - val expectedResult = TokenListError.EmptyTokens.left() - - val useCase = getUseCase(tokens = flowOf()) - - // When - val result = useCase.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when networks statuses flow is empty then error should be received`() = runTest { - val expectedResult = listOf( - MockTokenLists.loadingUngroupedTokenList.right(), - TokenListError.EmptyTokens.left(), - ) - - val useCase = getUseCase(statuses = flowOf()) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // 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.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when quotes flow is empty then list without quotes should be received`() = runTest { - val expectedResult = listOf( - MockTokenLists.loadingUngroupedTokenList.right(), - MockTokenLists.noQuotesUngroupedTokenList.right(), - ) - - val useCase = getUseCase( - statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), - quotes = flowOf(emptySet().right()), - ) - - // When - val result = useCase.launch(userWalletId) - .take(count = 2) - .toList() - - // 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.launch(userWalletId).first() - - // Then - assertEquals(expectedResult, result) - } - - private fun getUseCase( - tokens: Flow>> = flowOf(MockTokens.tokens.right()), - quotes: Flow>> = flowOf(MockQuotes.quotes.right()), - statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), - isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), - isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), - ) = GetTokenListUseCase( - currenciesRepository = MockCurrenciesRepository( - sortTokensResult = Unit.right(), - removeCurrencyResult = Unit.right(), - token = MockTokens.token1.right(), - tokens = tokens, - isGrouped = isGrouped, - isSortedByBalance = isSortedByBalance, - ), - quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(statuses), - stakingRepository = MockStakingRepository(), - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt deleted file mode 100644 index a358685961..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/FeatureTogglesModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.feature.wallet.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles -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 FeatureTogglesModule { - - @Provides - @Singleton - fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletFeatureToggles { - return WalletFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt deleted file mode 100644 index e0881eead6..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggle/WalletFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.feature.wallet.featuretoggle - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -internal class WalletFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) { - - val isTokenListLceFlowEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("TOKEN_LIST_LCE_ENABLED") -} \ No newline at end of file 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 2540a508f8..93c0aab777 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 @@ -13,9 +13,7 @@ import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase import com.tangem.domain.tokens.ToggleTokenListSortingUseCase import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState @@ -42,7 +40,6 @@ internal class OrganizeTokensViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, - private val walletFeatureToggles: WalletFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents { @@ -173,34 +170,21 @@ internal class OrganizeTokensViewModel @Inject constructor( } private suspend fun getTokenList(): TokenList? { - return if (walletFeatureToggles.isTokenListLceFlowEnabled) { - val tokenList = getTokenListUseCase.launchLce(userWalletId) - .transform { maybeTokenList -> - val tokenList = maybeTokenList.getOrElse( - ifLoading = { return@transform }, - ifError = { error -> - stateHolder.updateStateWithError(error) + val tokenList = getTokenListUseCase.launch(userWalletId) + .transform { maybeTokenList -> + val tokenList = maybeTokenList.getOrElse( + ifLoading = { return@transform }, + ifError = { error -> + stateHolder.updateStateWithError(error) - return@transform - }, - ) + return@transform + }, + ) - emit(tokenList) - } - - tokenList.firstOrNull() - } else { - val maybeTokenList = getTokenListUseCase.launch(userWalletId) - .first { maybeTokenList -> - maybeTokenList.getOrNull()?.totalFiatBalance !is TotalFiatBalance.Loading - } - - maybeTokenList.getOrElse { error -> - stateHolder.updateStateWithError(error) - - null + emit(tokenList) } - } + + return tokenList.firstOrNull() } private fun bootstrapDragAndDropUpdates() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index c28d7d5e70..e3bef9a24d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -46,7 +46,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val promoFlow = flow { emit(promoRepository.getOkxPromoBanner()) } return combine( - flow = getTokenListUseCase.launchLce(userWallet.walletId), + flow = getTokenListUseCase.launch(userWallet.walletId), flow2 = isReadyToShowRateAppUseCase(), flow3 = isNeedToBackupUseCase(userWallet.walletId), flow4 = shouldShowSwapPromoWalletUseCase(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 0a3310ce92..7dc59658c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -5,7 +5,6 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory @@ -28,7 +27,6 @@ internal class MultiWalletContentLoader( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val walletFeatureToggles: WalletFeatureToggles, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -42,7 +40,6 @@ internal class MultiWalletContentLoader( walletWithFundsChecker = walletWithFundsChecker, getTokenListUseCase = getTokenListUseCase, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - walletFeatureToggles = walletFeatureToggles, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 13dbadebb3..1b405c8af6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -5,7 +5,6 @@ import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory @@ -26,7 +25,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletFeatureToggles: WalletFeatureToggles, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) { @@ -42,7 +40,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, - walletFeatureToggles = walletFeatureToggles, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 6b19dfd81d..12d5115bcf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase @@ -12,19 +11,16 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.featuretoggle.WalletFeatureToggles import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import kotlinx.coroutines.flow.map @Suppress("LongParameterList") internal class MultiWalletTokenListSubscriber( private val userWallet: UserWallet, private val getTokenListUseCase: GetTokenListUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val walletFeatureToggles: WalletFeatureToggles, stateHolder: WalletStateController, clickIntents: WalletClickIntents, tokenListAnalyticsSender: TokenListAnalyticsSender, @@ -42,11 +38,7 @@ internal class MultiWalletTokenListSubscriber( ) { override fun tokenListFlow(): LceFlow { - return if (walletFeatureToggles.isTokenListLceFlowEnabled) { - getTokenListUseCase.launchLce(userWallet.walletId) - } else { - getTokenListUseCase.launch(userWallet.walletId).map { it.toLce() } - } + return getTokenListUseCase.launch(userWallet.walletId) } override suspend fun onTokenListReceived(maybeTokenList: Lce) { From 0cd5575681112e4e7d9b4a9afc49eb36ea56671c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 11:07:58 +0300 Subject: [PATCH 143/223] Updated on 2026-08-14 --- .../api/markets/TangemTechMarketsApi.kt | 2 +- .../response/TokenMarketChartListResponse.kt | 7 +- .../com/tangem/datasource/di/NetworkModule.kt | 2 +- .../java/com/tangem/pagination/BatchAction.kt | 12 +- .../com/tangem/pagination/BatchListSource.kt | 165 ++++++++++++++---- .../tangem/pagination/BatchUpdateFetcher.kt | 44 ++++- .../fetcher/LimitOffsetBatchFetcher.kt | 35 ++-- data/markets/build.gradle.kts | 2 + .../markets/DefaultMarketsTokenRepository.kt | 112 ++++-------- .../data/markets/MarketsBatchUpdateFetcher.kt | 117 +++++++++++++ .../converters/TokenListConfigConverters.kt | 2 +- .../converters/TokenMarketChartsConverter.kt | 2 +- .../converters/TokenMarketListConverter.kt | 6 +- .../converters/TokenQuotesConverter.kt | 13 +- .../data/markets/di/MarketsDataModule.kt | 5 +- .../tangem/data/markets/utils/RequestUtils.kt | 27 +++ .../com/tangem/domain/markets/TokenMarket.kt | 6 +- .../com/tangem/domain/markets/TokenQuotes.kt | 14 +- .../markets/GetMarketsTokenListFlowUseCase.kt | 16 ++ .../repositories/MarketsTokenRepository.kt | 10 +- 20 files changed, 437 insertions(+), 162 deletions(-) create mode 100644 data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt create mode 100644 data/markets/src/main/java/com/tangem/data/markets/utils/RequestUtils.kt create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 0ded78dbab..014f9adf75 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -34,7 +34,7 @@ interface TangemTechMarketsApi { @GET("coins/history_preview") suspend fun getCoinsListCharts( - @Query("coin_ids") coinIds: List, + @Query("coin_ids") coinIds: String, @Query("currency") currency: String, @Query("interval") interval: String, ): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt index 0d1667ee24..b2f534b038 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt @@ -1,8 +1,3 @@ package com.tangem.datasource.api.markets.models.response -import com.squareup.moshi.Json - -class TokenMarketChartListResponse( - @Json(name = "tokens") - val tokens: Map, -) \ No newline at end of file +typealias TokenMarketChartListResponse = Map \ 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 d2b395d5f9..9b2c5b5d29 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 @@ -132,7 +132,7 @@ class NetworkModule { @Provides @DevTangemApi @Singleton - fun provideCoinMarketsApi( + fun provideTangemTechMarketsApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, appVersionProvider: AppVersionProvider, diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 691941752a..8078c699b1 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -1,5 +1,7 @@ package com.tangem.pagination +import java.util.UUID + /** * Action that can be dispatched to [BatchListSource]. * @@ -7,7 +9,7 @@ package com.tangem.pagination * @param TKey type of the key of the batch. * @param TUpdate type of the update request. */ -sealed class BatchAction { +sealed class BatchAction { /** * Action to load the first batch. @@ -34,10 +36,18 @@ sealed class BatchAction { * * @param keys keys of the batches to update. * @param updateRequest request to update the batches. + * @param async true if the request doesn't require to synchronize on specific batches in order to fetch update + * data, this request will be delegated to fetchAsync method in [BatchUpdateFetcher], + * false if request requires to hold the current batches data until fetch + update is completed + * @param operationId the unique identifier of the request. + * Only one request with the same hash can be executed at a time, + * the rest of the requests will be canceled as long as there is a request with this hash in progress. */ class UpdateBatches( val keys: Set, val updateRequest: TUpdate, + val async: Boolean = false, + val operationId: String = UUID.randomUUID().toString(), ) : BatchAction() /** diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index 77e58eafcf..1cc865396d 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -75,6 +75,8 @@ private class DefaultBatchListSource private val scope = context.coroutineScope private val updateJobs = MutableStateFlow, Job>>>(emptyList()) + private val updateAsyncJobs = + MutableStateFlow, Job>>>(emptyList()) private val waitingUpdateJobs = MutableStateFlow, Job>>>(emptyList()) @@ -127,38 +129,13 @@ private class DefaultBatchListSource } is BatchAction.UpdateBatches -> { if (updateFetcher == null) return + // If the request with the same operationId is in progress, skip the request + if (updateInProgressExists(action.operationId)) return - scope.launch(fetchDispatcher) { - // Lazily start a job so we can avoid batch update collisions - // by waiting for other tasks with the same keys to complete - val job = launch(start = CoroutineStart.LAZY) { - updateBatchesTask(action) - } - - val actionJob = action to job - - waitingUpdateJobs.update { it + actionJob } - - // Wait for other update tasks that mutate batches with the same keys - updateJobs.first { workingJobs -> - action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() - } - - waitingUpdateJobs.update { it - actionJob } - - // No other task are mutating batches with the same keys, so we can start a job - val started = job.start() - - if (started) { - updateJobs.update { it + actionJob } - - job.invokeOnCompletion { cause -> - // If the job was cancelled it is up to a canceller to remove job from the updateJobs list - if (cause !is CancellationException) { - updateJobs.update { it - actionJob } - } - } - } + if (action.async) { + collectAsyncUpdateAction(action) + } else { + collectSyncUpdateAction(action) } } BatchAction.CancelAllUpdates -> { @@ -176,6 +153,57 @@ private class DefaultBatchListSource } } + private fun collectAsyncUpdateAction(action: BatchAction.UpdateBatches) { + val job = scope.launch(fetchDispatcher) { + updateBatchesAsyncTask(action) + } + val actionJob = action to job + + updateAsyncJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + // If the job was cancelled it is up to a canceller to remove job from the updateJobs list + if (cause !is CancellationException) { + updateAsyncJobs.update { it - actionJob } + } + } + } + + private fun collectSyncUpdateAction(action: BatchAction.UpdateBatches) { + // Lazily start a job so we can avoid batch update collisions + // by waiting for other tasks with the same keys to complete + val job = scope.launch(fetchDispatcher, start = CoroutineStart.LAZY) { + updateBatchesTask(action) + } + + val actionJob = action to job + + waitingUpdateJobs.update { it + actionJob } + + scope.launch(fetchDispatcher) { + // Wait for other update tasks that mutate batches with the same keys + updateJobs.first { workingJobs -> + action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() + } + + waitingUpdateJobs.update { it - actionJob } + + // No other task are mutating batches with the same keys, so we can start a job + val started = job.start() + + if (started) { + updateJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + // If the job was cancelled it is up to a canceller to remove job from the updateJobs list + if (cause !is CancellationException) { + updateJobs.update { it - actionJob } + } + } + } + } + } + private suspend fun reloadTask(action: BatchAction.Reload) { state.value = BatchListState( data = emptyList(), @@ -184,7 +212,10 @@ private class DefaultBatchListSource val res = runCatching { batchFetcher.fetchFirst(action.requestParams) - }.getOrElse { BatchFetchResult.Error(it) } + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchFetchResult.Error(it) + } state.value = when (res) { is BatchFetchResult.Success -> { @@ -275,6 +306,7 @@ private class DefaultBatchListSource updateRequest = action.updateRequest, ) } catch (t: Throwable) { + currentCoroutineContext().ensureActive() BatchUpdateResult.Error(t) } @@ -292,7 +324,64 @@ private class DefaultBatchListSource updateResults.emit(action.updateRequest to result) } + private suspend fun updateBatchesAsyncTask(action: BatchAction.UpdateBatches) { + if (updateFetcher == null) return + + val batches = state.value.data + val batchesToUpdate = batches.filter { action.keys.contains(it.key) } + + val updateContext = UpdateContext(request = action.updateRequest, action.keys) + + with(updateFetcher) { + updateContext.fetchUpdateAsync(batchesToUpdate, action.updateRequest) + } + } + + @Suppress("FunctionNaming") + private fun UpdateContext(request: TUpdate, keysToUpdate: Set) = + object : BatchUpdateFetcher.UpdateContext { + + override suspend fun update(update: List>.() -> BatchUpdateResult) { + val stateToFetchUpdateBasedOn = state.value.data.filter { + keysToUpdate.contains(it.key) + } + + val result = runCatching { + stateToFetchUpdateBasedOn.update() + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchUpdateResult.Error(it) + } + + if (result is BatchUpdateResult.Success) { + state.update { currentState -> + val resMap = result.data.associateBy { it.key } + currentState.copy( + data = currentState.data.map { + resMap[it.key] ?: it + }, + ) + } + } + + updateResults.emit(request to result) + } + } + + private fun updateInProgressExists(operationId: String): Boolean { + return updateAsyncJobs.value.any { it.first.operationId == operationId } || + updateJobs.value.any { it.first.operationId == operationId } || + waitingUpdateJobs.value.any { it.first.operationId == operationId } + } + private fun stopAllUpdates() { + updateAsyncJobs.update { actionAsyncJobs -> + actionAsyncJobs.forEach { + it.second.cancel() + } + emptyList() + } + updateJobs.update { actionJobs -> waitingUpdateJobs.update { waitingActionJobs -> waitingActionJobs.forEach { @@ -308,6 +397,18 @@ private class DefaultBatchListSource } private fun stopUpdates(predicate: (BatchAction.UpdateBatches) -> Boolean) { + updateAsyncJobs.update { actionAsyncJobs -> + actionAsyncJobs.mapNotNull { + if (predicate(it.first)) { + it.second.cancel() + null + } else { + it + } + } + emptyList() + } + updateJobs.update { actionJobs -> waitingUpdateJobs.update { waitingActionJobs -> waitingActionJobs.mapNotNull { diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt index 704c416316..f53deb4449 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -8,7 +8,7 @@ package com.tangem.pagination * @param TData type of the data. * @param TUpdate type of the update request. */ -fun interface BatchUpdateFetcher { +interface BatchUpdateFetcher { /** * Fetches updates for a batch of data. @@ -19,5 +19,45 @@ fun interface BatchUpdateFetcher { * @param updateRequest request to update the data. * @return result of the update operation. */ - suspend fun fetchUpdate(toUpdate: List>, updateRequest: TUpdate): BatchUpdateResult + suspend fun fetchUpdate( + toUpdate: List>, + updateRequest: TUpdate, + ): BatchUpdateResult = BatchUpdateResult.Error(NotImplementedError()) + + /** + * Fetches updates for a batch of data asynchronously. + * To update the data, use the [UpdateContext.update] method. + * [UpdateContext.update] could be called as many times as you want. + * + * Note that the result batch key as a result of executing the method must be presented in the [toUpdate] list, + * otherwise, updates will not be performed. + * + * @param toUpdate list of batches to update. **Attention** Data may be outdated and should be used only to make + * a request for an update, not for the actual update operation. For the actual update operation, use the batches + * provided by [UpdateContext.update]. + * @param updateRequest request to update the data. + */ + suspend fun UpdateContext.fetchUpdateAsync( + toUpdate: List>, + updateRequest: TUpdate, + ) { + } + + /** + * Context for updating the data. + * Used by [BatchListSource] to provide a way to update batches by [fetchUpdateAsync] method. + */ + interface UpdateContext { + + /** + * Updates the data of the batch. + * Could be called as many times as you want. + * + * Input batches keys and the data could not always be the same as the keys of the [toUpdate] list in + * [fetchUpdateAsync] method, but the provided set of keys will always be a subset of the [toUpdate] list. + * + * @param update lambda to update the data. + */ + suspend fun update(update: List>.() -> BatchUpdateResult) + } } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index 3dd04c24c7..de125a62ff 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -2,6 +2,8 @@ package com.tangem.pagination.fetcher import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.exception.EndOfPaginationException +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow /** @@ -12,32 +14,42 @@ import kotlinx.coroutines.flow.MutableStateFlow * * @property prefetchDistance number of items to fetch for the first batch. * @property batchSize size of the batch. - * @property fetch function that fetches the data. + * @property subFetcher function that fetches the data. */ class LimitOffsetBatchFetcher( private val prefetchDistance: Int, private val batchSize: Int, - private val fetch: suspend (request: Request) -> BatchFetchResult, + private val subFetcher: SubFetcher, ) : BatchFetcher { - data class Request( + data class Request( val limit: Int, val offset: Int, - val request: TRequest, + val params: TRequestParams, ) + fun interface SubFetcher { + suspend fun fetch( + request: Request, + lastResult: BatchFetchResult?, + ): BatchFetchResult + } + private val lastRequest = MutableStateFlow?>(null) override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult { val req = Request( offset = 0, limit = prefetchDistance, - request = requestParams, + params = requestParams, ) val res = runCatching { - fetch(req) - }.getOrElse { BatchFetchResult.Error(it) } + subFetcher.fetch(req, null) + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchFetchResult.Error(it) + } lastRequest.value = req return res @@ -58,15 +70,18 @@ class LimitOffsetBatchFetcher( Request( offset = last.offset + last.limit, limit = batchSize, - request = overrideRequestParams ?: last.request, + params = overrideRequestParams ?: last.params, ) } else { last } val res = runCatching { - fetch(req) - }.getOrElse { BatchFetchResult.Error(it) } + subFetcher.fetch(req, lastResult) + }.getOrElse { + currentCoroutineContext().ensureActive() + BatchFetchResult.Error(it) + } lastRequest.value = req return res diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 8e9347ad54..98b32e7c97 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.core.pagination) implementation(projects.domain.tokens.models) implementation(projects.domain.markets) + implementation(projects.data.common) // region DI implementation(deps.hilt.android) @@ -26,6 +27,7 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(deps.timber) implementation(projects.libs.blockchainSdk) // endregion diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 65ebc717bb..a0b50723dc 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.markets import com.tangem.data.markets.converters.* +import com.tangem.data.markets.utils.retryOnError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -9,7 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, @@ -18,28 +19,34 @@ internal class DefaultMarketsTokenRepository( ) : MarketsTokenRepository { private val tokenListConverter = TokenMarketListConverter() - private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) - private val tokenQuotesConverter = TokenQuotesConverter() private val tokenMarketsFetcher - get() = LimitOffsetBatchFetcher>( - prefetchDistance = 50, - batchSize = 30, - fetch = { params -> - withContext(dispatcherProvider.io) { - val res = marketsApi.getCoinsList( - currency = params.request.fiatPriceCurrency, - interval = params.request.priceChangeInterval.toRequestParam(), - order = params.request.priceChangeInterval.toRequestParam(), - search = params.request.searchText, - generalCoins = params.request.showUnder100kMarketCapTokens.not(), - offset = params.offset, - limit = params.limit, - ).getOrThrow() + get() = LimitOffsetBatchFetcher( + prefetchDistance = 150, + batchSize = 100, + subFetcher = object : LimitOffsetBatchFetcher.SubFetcher> { - val last = res.tokens.size < params.limit + var requestTimeStamp: Long? = null // TODO when backend is ready - BatchFetchResult.Success( + override suspend fun fetch( + request: LimitOffsetBatchFetcher.Request, + lastResult: BatchFetchResult>?, + ): BatchFetchResult> { + val res = retryOnError(priority = true) { + marketsApi.getCoinsList( + currency = request.params.fiatPriceCurrency, + interval = request.params.priceChangeInterval.toRequestParam(), + order = request.params.order.toRequestParam(), + search = request.params.searchText, + generalCoins = request.params.showUnder100kMarketCapTokens.not(), + offset = request.offset, + limit = request.limit, + ).getOrThrow() + } + + val last = res.tokens.size < request.limit + + return BatchFetchResult.Success( data = tokenListConverter.convert(res), last = last, ) @@ -47,60 +54,14 @@ internal class DefaultMarketsTokenRepository( }, ) - private val tokenMarketsUpdateFetcher - get() = BatchUpdateFetcher, TokenMarketUpdateRequest> { toUpdate, updateRequest -> - withContext(dispatcherProvider.io) { - val idsToUpdate = toUpdate.map { batch -> - batch.data.map { it.id } - }.flatten() - - val updatedBatches = when (updateRequest) { - is TokenMarketUpdateRequest.UpdateChart -> { - val res = marketsApi.getCoinsListCharts( - coinIds = idsToUpdate, - interval = updateRequest.interval.toRequestParam(), - currency = updateRequest.currency, - ).getOrThrow() - - toUpdate.map { batch -> - batch.copy( - data = batch.data.map { - it.copy( - tokenCharts = tokenListChartsConverter.convert( - chartsToCopy = it.tokenCharts, - tokenId = it.id, - interval = updateRequest.interval, - value = res, - ), - ) - }, - ) - } - } - is TokenMarketUpdateRequest.UpdateQuotes -> { - val quotesRes = tangemTechApi.getQuotes( - currencyId = updateRequest.currencyId, - coinIds = idsToUpdate.joinToString(separator = ","), - fields = quoteFields.joinToString(separator = ","), - ).getOrThrow() - - toUpdate.map { batch -> - batch.copy( - data = batch.data.map { - it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) - }, - ) - } - } - } - - BatchUpdateResult.Success(updatedBatches) - } - } - - override suspend fun getTokenListFlow( + override fun getTokenListFlow( batchingContext: BatchingContext, ): BatchFlow, TokenMarketUpdateRequest> { + val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher( + tangemTechApi = tangemTechApi, + marketsApi = marketsApi, + ) + return BatchListSource( fetchDispatcher = dispatcherProvider.io, context = batchingContext, @@ -109,13 +70,4 @@ internal class DefaultMarketsTokenRepository( updateFetcher = tokenMarketsUpdateFetcher, ).toBatchFlow() } - - companion object { - private val quoteFields = listOf( - "price", - "priceChange24h", - "priceChange1w", - "priceChange30d", - ) - } } \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt new file mode 100644 index 0000000000..fa24fa4144 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -0,0 +1,117 @@ +package com.tangem.data.markets + +import com.tangem.data.markets.converters.TokenListChartConverter +import com.tangem.data.markets.converters.TokenMarketChartsConverter +import com.tangem.data.markets.converters.TokenQuotesConverter +import com.tangem.data.markets.converters.toRequestParam +import com.tangem.data.markets.utils.retryOnError +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketUpdateRequest +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchUpdateFetcher +import com.tangem.pagination.BatchUpdateResult +import kotlinx.coroutines.* + +internal class MarketsBatchUpdateFetcher( + private val marketsApi: TangemTechMarketsApi, + private val tangemTechApi: TangemTechApi, +) : BatchUpdateFetcher, TokenMarketUpdateRequest> { + + private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) + private val tokenQuotesConverter = TokenQuotesConverter() + + override suspend fun BatchUpdateFetcher.UpdateContext>.fetchUpdateAsync( + toUpdate: List>>, + updateRequest: TokenMarketUpdateRequest, + ) { + val idsToUpdate = toUpdate.map { batch -> + batch.key to batch.data.map { it.id } + } + + when (updateRequest) { + is TokenMarketUpdateRequest.UpdateChart -> coroutineScope { + val updateTasks = idsToUpdate.map { batchIds -> + async { + retryOnError { + marketsApi.getCoinsListCharts( + coinIds = batchIds.second.joinToString(separator = ","), + interval = updateRequest.interval.toRequestParam(), + currency = updateRequest.currency, + ).getOrThrow() + } + } + } + + updateTasks.forEachIndexed { index, deferred -> + launch { + val res = deferred.await() + val batchToUpdate = toUpdate[index] + + update { + val resBatch = changeChartsInBatches( + updateRequest = updateRequest, + batchToUpdate = batchToUpdate, + update = res, + ) + BatchUpdateResult.Success(resBatch) + } + } + } + } + is TokenMarketUpdateRequest.UpdateQuotes -> { + val quotesRes = tangemTechApi.getQuotes( + currencyId = updateRequest.currencyId, + coinIds = idsToUpdate.joinToString(separator = ","), + fields = quoteFields.joinToString(separator = ","), + ).getOrThrow() + + update { + val res = toUpdate.map { batch -> + batch.copy( + data = batch.data.map { + it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) + }, + ) + } + + BatchUpdateResult.Success(res) + } + } + } + } + + private fun List>>.changeChartsInBatches( + updateRequest: TokenMarketUpdateRequest.UpdateChart, + batchToUpdate: Batch>, + update: TokenMarketChartListResponse, + ): List>> = mapNotNull { resultBatch -> + if (batchToUpdate.key != resultBatch.key) return@mapNotNull null + + Batch( + key = batchToUpdate.key, + data = batchToUpdate.data.map { + it.copy( + tokenCharts = tokenListChartsConverter.convert( + chartsToCopy = it.tokenCharts, + tokenId = it.id, + interval = updateRequest.interval, + value = update, + ), + ) + }, + ) + } + + companion object { + private val quoteFields = listOf( + "price", + "priceChange24h", + "priceChange1w", + "priceChange30d", + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt index eee63ef4e9..f47c0ff170 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -20,7 +20,7 @@ fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { fun PriceChangeInterval.toRequestParam(): String = when (this) { PriceChangeInterval.H24 -> "24h" PriceChangeInterval.WEEK -> "1w" - PriceChangeInterval.MONTH -> "1m" + PriceChangeInterval.MONTH -> "30d" PriceChangeInterval.MONTH3 -> "3m" PriceChangeInterval.MONTH6 -> "6m" PriceChangeInterval.YEAR -> "1y" diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt index aade2d2073..dcfdee4638 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -14,7 +14,7 @@ class TokenMarketChartsConverter( interval: PriceChangeInterval, value: TokenMarketChartListResponse, ): TokenMarket.Charts { - val prices = requireNotNull(value.tokens[tokenId]) { + val prices = requireNotNull(value[tokenId]) { "$tokenId is not found in the response. This shouldn't have happened." } return when (interval) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index ea8587e123..8f9c571a86 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -20,9 +20,9 @@ class TokenMarketListConverter : Converter retryOnError(priority: Boolean = false, call: suspend () -> T): T { + while (true) { + return try { + call() + } catch (e: Exception) { + if (e is CancellationException) { + currentCoroutineContext().ensureActive() + } + Timber.e(e) + if (priority.not()) { + yield() + delay(timeMillis = 500) + } + continue + } + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index c9587db1f9..b7316cf79a 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -21,13 +21,13 @@ data class TokenMarket( // 25x25 val imageUrlThumb = - "$imageHost/:thumb/:$id.png" + "${imageHost}thumb/$id.png" // 50x50 val imageUrlSmall = - "$imageHost/:small/:$id.png" + "${imageHost}small/$id.png" // 250x250 val imageUrlLarge = - "$imageHost/:large/:$id.png" + "${imageHost}large/$id.png" } \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt index e26b6abb9f..7a162c9aac 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt @@ -4,5 +4,15 @@ import java.math.BigDecimal data class TokenQuotes( val currentPrice: BigDecimal, - val priceChanges: Map, -) \ No newline at end of file + private val priceChanges: Map, +) { + init { + require(priceChanges.containsKey(PriceChangeInterval.H24)) + require(priceChanges.containsKey(PriceChangeInterval.WEEK)) + require(priceChanges.containsKey(PriceChangeInterval.MONTH)) + } + + fun h24Percent() = priceChanges[PriceChangeInterval.H24]!! + fun weekPercent() = priceChanges[PriceChangeInterval.WEEK]!! + fun monthPercent() = priceChanges[PriceChangeInterval.MONTH]!! +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt new file mode 100644 index 0000000000..ee13784443 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetMarketsTokenListFlowUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.markets + +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.pagination.BatchFlow +import com.tangem.pagination.BatchingContext + +typealias TokenListBatchingContext = BatchingContext +typealias TokenListBatchFlow = BatchFlow, TokenMarketUpdateRequest> + +class GetMarketsTokenListFlowUseCase( + private val marketsTokenRepository: MarketsTokenRepository, +) { + operator fun invoke(batchingContext: TokenListBatchingContext): TokenListBatchFlow { + return marketsTokenRepository.getTokenListFlow(batchingContext) + } +} \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index 97aeb80c43..b72061f9f2 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -1,14 +1,8 @@ package com.tangem.domain.markets.repositories -import com.tangem.domain.markets.TokenMarket -import com.tangem.domain.markets.TokenMarketListConfig -import com.tangem.domain.markets.TokenMarketUpdateRequest -import com.tangem.pagination.BatchFlow -import com.tangem.pagination.BatchingContext +import com.tangem.domain.markets.* interface MarketsTokenRepository { - suspend fun getTokenListFlow( - batchingContext: BatchingContext, - ): BatchFlow, TokenMarketUpdateRequest> + fun getTokenListFlow(batchingContext: TokenListBatchingContext): TokenListBatchFlow } \ No newline at end of file From 0391e50dc8c6d1d49854471ccb70d019744ffa08 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 13:26:37 +0300 Subject: [PATCH 144/223] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 18 ++++- .../datasource/api/stakekit/StakeKitApi.kt | 9 ++- ...ionRequestBody.kt => ActionRequestBody.kt} | 5 +- .../data/staking/DefaultStakingRepository.kt | 47 +++++++---- .../model/action/StakingActionCommonType.kt | 6 ++ .../staking/model/transaction/ActionParams.kt | 14 ++++ .../domain/staking/EstimateGasUseCase.kt | 25 ++++++ ...ase.kt => GetStakingTransactionUseCase.kt} | 24 ++---- .../staking/repositories/StakingRepository.kt | 14 ++-- .../repository/MockStakingRepository.kt | 59 +++++++++----- .../ui/StakingNavigationButtons.kt | 2 +- .../viewmodel/StakingViewModel.kt | 81 +++++++++++-------- 12 files changed, 198 insertions(+), 106 deletions(-) rename core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/{EnterActionRequestBody.kt => ActionRequestBody.kt} (87%) create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt rename domain/staking/src/main/java/com/tangem/domain/staking/{InitializeStakingProcessUseCase.kt => GetStakingTransactionUseCase.kt} (62%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index f080165fe4..10f1264ba6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -87,11 +87,23 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideCreateEnterActionUseCase( + fun provideInitializeStakingProcessUseCase( stakingRepository: StakingRepository, stakingErrorResolver: StakingErrorResolver, - ): InitializeStakingProcessUseCase { - return InitializeStakingProcessUseCase( + ): GetStakingTransactionUseCase { + return GetStakingTransactionUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + + @Provides + @Singleton + fun provideGasEstimateUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): EstimateGasUseCase { + return EstimateGasUseCase( stakingRepository = stakingRepository, stakingErrorResolver = stakingErrorResolver, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 17e656903f..fc7dab4bd6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.transaction.Stak import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO import retrofit2.http.* @Suppress("LongParameterList") @@ -45,7 +46,13 @@ interface StakeKitApi { suspend fun getTokens(): ApiResponse> @POST("actions/enter") - suspend fun createEnterAction(@Body body: EnterActionRequestBody): ApiResponse + suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse + + @POST("actions/enter/estimate-gas") + suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse + + @POST("actions/exit/estimate-gas") + suspend fun estimateGasOnExit(@Body body: ActionRequestBody): ApiResponse @PATCH("transactions/{transactionId}") suspend fun constructTransaction( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt similarity index 87% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index 6d06f46f5c..e044b1f9a9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -1,10 +1,11 @@ package com.tangem.datasource.api.stakekit.models.request import com.squareup.moshi.Json +import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -data class EnterActionRequestBody( +data class ActionRequestBody( @Json(name = "integrationId") val integrationId: String, @Json(name = "addresses") @@ -13,6 +14,8 @@ data class EnterActionRequestBody( val args: EnterActionRequestBodyArgs, @Json(name = "referralCode") val referralCode: String? = null, + @Json(name = "gasArgs") + val gasArgs: GasArgs? = null, // used only in estimate_gas request ) { data class EnterActionRequestBodyArgs( diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index c9c22d3596..5249e59c74 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -25,6 +25,9 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.action.StakingActionCommonType +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.transaction.ActionParams import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency @@ -37,7 +40,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.math.BigDecimal @Suppress("LargeClass", "LongParameterList") internal class DefaultStakingRepository( @@ -153,29 +155,28 @@ internal class DefaultStakingRepository( } } - override suspend fun createEnterAction( - integrationId: String, - amount: BigDecimal, - address: String, - validatorAddress: String, - token: Token, - ): EnterAction { + override suspend fun createEnterAction(params: ActionParams): EnterAction { return withContext(dispatchers.io) { - val body = EnterActionRequestBody( - integrationId = integrationId, - addresses = Address(address), - args = EnterActionRequestBody.EnterActionRequestBodyArgs( - amount = amount.toFormattedString(token.decimals), - inputToken = tokenConverter.convertBack(token), - validatorAddress = validatorAddress, - ), - ) + val body = createActionRequestBody(params) val response = stakeKitApi.createEnterAction(body) enterActionResponseConverter.convert(response.getOrThrow()) } } + override suspend fun estimateGas(params: ActionParams): StakingGasEstimate { + return withContext(dispatchers.io) { + val body = createActionRequestBody(params) + + val gasEstimateDTO = when (params.actionCommonType) { + StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(body) + StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(body) + } + + gasEstimateConverter.convert(gasEstimateDTO.getOrThrow()) + } + } + override suspend fun constructTransaction(transactionId: String): StakingTransaction { return withContext(dispatchers.io) { val transactionResponse = stakeKitApi.constructTransaction( @@ -423,6 +424,18 @@ internal class DefaultStakingRepository( } } + private fun createActionRequestBody(params: ActionParams): ActionRequestBody { + return ActionRequestBody( + integrationId = params.integrationId, + addresses = Address(params.address), + args = ActionRequestBody.EnterActionRequestBodyArgs( + amount = params.amount.toFormattedString(params.token.decimals), + inputToken = tokenConverter.convertBack(params.token), + validatorAddress = params.validatorAddress, + ), + ) + } + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt new file mode 100644 index 0000000000..59f5e782d9 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.model.action + +enum class StakingActionCommonType { + ENTER, + EXIT, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt new file mode 100644 index 0000000000..ab5aa8f530 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.staking.model.transaction + +import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.action.StakingActionCommonType +import java.math.BigDecimal + +data class ActionParams( + val actionCommonType: StakingActionCommonType, + val integrationId: String, + val amount: BigDecimal, + val address: String, + val validatorAddress: String, + val token: Token, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt new file mode 100644 index 0000000000..bbbac8f73f --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.transaction.ActionParams +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for staking gas estimation. + */ +class EstimateGasUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + suspend operator fun invoke(params: ActionParams): Either { + return Either.catch { + stakingRepository.estimateGas(params) + }.mapLeft { + stakingErrorResolver.resolve(it) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt similarity index 62% rename from domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt rename to domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index 4a9fc87c47..12107bff68 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -2,37 +2,23 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.model.StakingError -import com.tangem.domain.staking.model.Token -import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.ActionParams import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import kotlinx.coroutines.delay -import java.math.BigDecimal /** * Use case for creating enter action */ -class InitializeStakingProcessUseCase( +class GetStakingTransactionUseCase( private val stakingRepository: StakingRepository, private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke( - integrationId: String, - amount: BigDecimal, - address: String, - validatorAddress: String, - token: Token, - ): Either> { + suspend operator fun invoke(params: ActionParams): Either { return Either.catch { - val createAction = stakingRepository.createEnterAction( - integrationId = integrationId, - amount = amount, - address = address, - validatorAddress = validatorAddress, - token = token, - ) + val createAction = stakingRepository.createEnterAction(params) // workaround, sometimes transaction is not created immediately after actions/enter delay(PATCH_TRANSACTION_REQUEST_DELAY) @@ -40,7 +26,7 @@ class InitializeStakingProcessUseCase( val createdTransaction = createAction.transactions?.get(0) ?: error("No available transaction to patch") val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) - createAction to patchedTransaction + patchedTransaction }.mapLeft { stakingErrorResolver.resolve(it) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 8b74784341..3776a2e27b 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,13 +2,13 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Token import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.action.EnterAction import com.tangem.domain.staking.model.transaction.StakingTransaction -import java.math.BigDecimal import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.transaction.ActionParams import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.wallets.models.UserWalletId @@ -60,13 +60,9 @@ interface StakingRepository { addresses: List, ): YieldBalanceList - suspend fun createEnterAction( - integrationId: String, - amount: BigDecimal, - address: String, - validatorAddress: String, - token: Token, - ): EnterAction + suspend fun createEnterAction(params: ActionParams): EnterAction + + suspend fun estimateGas(params: ActionParams): StakingGasEstimate suspend fun constructTransaction(transactionId: String): StakingTransaction diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 074c07f027..1c27d431af 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -6,9 +6,7 @@ import com.tangem.domain.staking.model.* import com.tangem.domain.staking.model.action.EnterAction import com.tangem.domain.staking.model.action.StakingActionStatus import com.tangem.domain.staking.model.action.StakingActionType -import com.tangem.domain.staking.model.transaction.StakingTransaction -import com.tangem.domain.staking.model.transaction.StakingTransactionStatus -import com.tangem.domain.staking.model.transaction.StakingTransactionType +import com.tangem.domain.staking.model.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -21,7 +19,8 @@ import java.math.BigDecimal class MockStakingRepository : StakingRepository { override fun isStakingSupported(currencyId: String): Boolean = true - override suspend fun fetchEnabledYields(refresh: Boolean) { /* no-op */ + override suspend fun fetchEnabledYields(refresh: Boolean) { + /* no-op */ } override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo = StakingEntryInfo( @@ -165,24 +164,37 @@ class MockStakingRepository : StakingRepository { balances = listOf(YieldBalance.Error), ) - override suspend fun createEnterAction( - integrationId: String, - amount: BigDecimal, - address: String, - validatorAddress: String, - token: Token, - ): EnterAction = EnterAction( - id = "quis", - integrationId = "persequeris", - status = StakingActionStatus.PROCESSING, - type = StakingActionType.CLAIM_REWARDS, - currentStepIndex = 8701, - amount = BigDecimal.ZERO, - validatorAddress = null, - validatorAddresses = listOf(), - transactions = listOf(), - createdAt = DateTime.now(), - ) + override suspend fun createEnterAction(params: ActionParams): EnterAction { + return EnterAction( + id = "quis", + integrationId = "persequeris", + status = StakingActionStatus.PROCESSING, + type = StakingActionType.CLAIM_REWARDS, + currentStepIndex = 8701, + amount = BigDecimal.ZERO, + validatorAddress = null, + validatorAddresses = listOf(), + transactions = listOf(), + createdAt = DateTime.now(), + ) + } + + override suspend fun estimateGas(params: ActionParams): StakingGasEstimate { + return StakingGasEstimate( + amount = BigDecimal(0.0001), + token = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = "solana", + logoURI = null, + isPoints = null, + ), + gasLimit = null, + ) + } override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction( id = "id", @@ -202,11 +214,14 @@ class MockStakingRepository : StakingRepository { ) override suspend fun submitHash(transactionId: String, transactionHash: String) { + /* no-op */ } override suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) { + /* no-op */ } override suspend fun sendUnsubmittedHashes() { + /* no-op */ } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 695d1a3bb7..73e2c6e526 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -113,7 +113,7 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) if (buttonClick != null) buttonClick() }, - showProgress = false, + showProgress = isInProgressInnerState, modifier = Modifier.fillMaxWidth(), colors = TangemButtonsDefaults.primaryButtonColors, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 77a9005b01..5549b2dbf0 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -14,11 +14,14 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.staking.InitializeStakingProcessUseCase +import com.tangem.domain.staking.GetStakingTransactionUseCase +import com.tangem.domain.staking.EstimateGasUseCase import com.tangem.domain.staking.SaveUnsubmittedHashUseCase import com.tangem.domain.staking.SubmitHashUseCase import com.tangem.domain.staking.model.Yield -import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.action.StakingActionCommonType +import com.tangem.domain.staking.model.transaction.ActionParams +import com.tangem.domain.staking.model.transaction.StakingGasEstimate import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -57,7 +60,8 @@ internal class StakingViewModel @Inject constructor( private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase, + private val getStakingTransactionUseCase: GetStakingTransactionUseCase, + private val estimateGasUseCase: EstimateGasUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, @@ -71,8 +75,6 @@ internal class StakingViewModel @Inject constructor( var stakingStateRouter: StakingStateRouter by Delegates.notNull() private set - private var stakingTransaction: StakingTransaction? = null - private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle.get(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY) ?.unbundle(CryptoCurrency.ID.serializer()) @@ -113,8 +115,29 @@ internal class StakingViewModel @Inject constructor( private fun handleOnNextConfirmationClick() { if (isAssentState()) { viewModelScope.launch { - stakingTransaction?.unsignedTransaction?.let { - sendStakingTransaction(TransactionData.Compiled(value = it.hexToBytes())) + stateController.update(SetConfirmationStateInProgressTransformer()) + + val stakingTransaction = getStakingTransactionUseCase( + params = ActionParams( + actionCommonType = StakingActionCommonType.ENTER, + integrationId = yield.id, + amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided"), + address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address"), + validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), + token = yield.token, + ), + ).getOrElse { + error(it) + } + + stakingTransaction.unsignedTransaction?.let { + sendStakingTransaction( + transactionId = stakingTransaction.id, + gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"), + TransactionData.Compiled(value = it.hexToBytes()), + ) } ?: error("No unsigned transaction available") } } @@ -128,23 +151,18 @@ internal class StakingViewModel @Inject constructor( ), ) - val actionWithTransaction = initializeStakingProcessUseCase( - integrationId = yield.id, - amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided"), - address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: error("No available address"), - validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), - token = yield.token, - ) - - val (enterAction, stakingTransaction) = actionWithTransaction.getOrElse { - error(it) - } - - this@StakingViewModel.stakingTransaction = stakingTransaction - - val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info") + val stakingGasEstimate = estimateGasUseCase( + params = ActionParams( + actionCommonType = StakingActionCommonType.ENTER, + integrationId = yield.id, + amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided"), + address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address"), + validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), + token = yield.token, + ), + ).getOrElse { error("Can't get fee info") } stateController.update( SetConfirmationStateAssentTransformer( @@ -270,9 +288,11 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } - private suspend fun sendStakingTransaction(txData: TransactionData) { - stateController.update(SetConfirmationStateInProgressTransformer()) - + private suspend fun sendStakingTransaction( + transactionId: String, + gasEstimate: StakingGasEstimate, + txData: TransactionData, + ) { sendTransactionUseCase( txData = txData, userWallet = userWallet, @@ -280,7 +300,6 @@ internal class StakingViewModel @Inject constructor( ).fold( ifLeft = { error -> Timber.e(error.toString()) - val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -290,17 +309,13 @@ internal class StakingViewModel @Inject constructor( ) }, ifRight = { txHash -> - val transaction = stakingTransaction ?: return@fold - - submitHash(transaction.id, txHash) + submitHash(transactionId, txHash) val txUrl = getExplorerTransactionUrlUseCase( txHash = txHash, networkId = cryptoCurrencyStatus.currency.network.id, ).getOrElse { "" } - val gasEstimate = transaction.gasEstimate ?: error("No gas for transaction") - stateController.update( SetConfirmationStateCompletedTransformer( appCurrencyProvider = Provider { appCurrency }, From cb36d5cf2592ca04006b995f909ec98decdf9f94 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 12 Jul 2024 16:30:49 +0200 Subject: [PATCH 145/223] Updated on 2026-08-14 --- .../send/redux/reducers/AmountReducer.kt | 2 +- .../send/redux/reducers/ReceiptReducer.kt | 7 ++----- .../amountScreen/ui/AmountFieldContainer.kt | 2 +- .../version/DefaultVersionProvider.kt | 3 ++- .../ui/components/rows/SelectorRowItem.kt | 4 ++-- .../ui/components/transactions/Transaction.kt | 4 ++-- .../core/ui/utils/BigDecimalFormatter.kt | 6 ++++-- .../src/main/java/com/tangem/utils/Strings.kt | 8 -------- .../java/com/tangem/utils/StringsSigns.kt | 12 ++++++++++++ .../markets/ui/components/MarketsListItem.kt | 3 ++- .../ui/recipient/SendRecipientContent.kt | 2 +- .../RewardsValidatorStateConverter.kt | 2 +- .../ui/StakingInitialInfoContent.kt | 5 +++-- .../tangem/feature/swap/ui/StateBuilder.kt | 19 ++++++++++++++----- .../feature/swap/ui/SwapScreenContent.kt | 2 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 6 +++--- ...tailsTxHistoryTransactionStateConverter.kt | 4 +++- .../ui/components/TokenDetailsBalanceBlock.kt | 2 +- .../components/staking/StakingBalanceBlock.kt | 4 ++-- .../component/token/TokenCryptoAmount.kt | 4 ++-- .../common/component/token/TokenFiatAmount.kt | 4 ++-- .../common/component/token/TokenPrice.kt | 6 +++--- .../common/state/TokenItemState.kt | 4 ---- .../wallet/state/model/WalletCardState.kt | 7 ++++--- .../converter/TokenItemStateConverter.kt | 5 +++-- .../converter/TxHistoryItemStateConverter.kt | 4 +++- .../VisaTxHistoryItemStateConverter.kt | 4 ++-- .../wallet/ui/components/common/WalletCard.kt | 4 ++-- 28 files changed, 78 insertions(+), 61 deletions(-) delete mode 100644 core/utils/src/main/java/com/tangem/utils/Strings.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/StringsSigns.kt diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt index 76daaa5802..645fc6ec32 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -10,7 +10,7 @@ import com.tangem.tap.features.send.redux.states.AmountState import com.tangem.tap.features.send.redux.states.InputViewValue import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.utils.Strings.STARS +import com.tangem.utils.StringsSigns.STARS import java.math.BigDecimal /** diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt index dbf1db4f40..8d1d46d4cd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt @@ -10,6 +10,7 @@ import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt import com.tangem.tap.features.send.redux.SendScreenAction import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.store +import com.tangem.utils.StringsSigns.LOWER_SIGN import java.math.BigDecimal /** @@ -401,11 +402,7 @@ class ReceiptReducer : SendInternalReducer { } private fun String.addPrecisionSign(): String { - val result = if (feeState.feeIsApproximate) "$CAN_BE_LOWER_SIGN $this" else this + val result = if (feeState.feeIsApproximate) "$LOWER_SIGN $this" else this return result.trim() } - - private companion object { - const val CAN_BE_LOWER_SIGN = "<" - } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 46fef9fbb0..146c491074 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -16,7 +16,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.utils.Strings.STARS +import com.tangem.utils.StringsSigns.STARS private const val AMOUNT_FIELD_KEY = "amountFieldKey" diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt index 7cebaee8a8..c602be463f 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt +++ b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt @@ -3,6 +3,7 @@ package com.tangem.core.featuretoggle.version import android.content.Context import android.content.pm.PackageManager import android.os.Build +import com.tangem.utils.StringsSigns.MINUS import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -34,6 +35,6 @@ internal class DefaultVersionProvider @Inject constructor( } private companion object { - const val VERSION_NAME_DELIMITER = "-" + const val VERSION_NAME_DELIMITER = MINUS } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index 5eb6b5f287..c6dd8c39f9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -25,7 +25,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns @Composable fun SelectorRowItem( @@ -128,7 +128,7 @@ private fun RowScope.SelectorValueContent( ) if (postDot != null) { Text( - text = Strings.DOT, + text = StringsSigns.DOT, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index 3b99a455ce..6a906295de 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -34,7 +34,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns import java.util.UUID /** @@ -258,7 +258,7 @@ private fun Amount(state: TransactionState, isBalanceHidden: Boolean, modifier: when (state) { is TransactionState.Content -> { Text( - text = if (isBalanceHidden) Strings.STARS else state.amount, + text = if (isBalanceHidden) StringsSigns.STARS else state.amount, modifier = modifier, textAlign = TextAlign.End, color = when (state.status) { 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 0dbdded548..4e5256d1d8 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 @@ -1,6 +1,8 @@ package com.tangem.core.ui.utils import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.StringsSigns.LOWER_SIGN import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -11,8 +13,8 @@ import java.util.Locale object BigDecimalFormatter { - const val EMPTY_BALANCE_SIGN = "—" - const val CAN_BE_LOWER_SIGN = "<" + const val EMPTY_BALANCE_SIGN = DASH_SIGN + const val CAN_BE_LOWER_SIGN = LOWER_SIGN private val FORMAT_THRESHOLD = BigDecimal("0.01") private const val TEMP_CURRENCY_CODE = "USD" diff --git a/core/utils/src/main/java/com/tangem/utils/Strings.kt b/core/utils/src/main/java/com/tangem/utils/Strings.kt deleted file mode 100644 index b40470e852..0000000000 --- a/core/utils/src/main/java/com/tangem/utils/Strings.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.utils - -object Strings { - - const val STARS = "\u2217\u2217\u2217" - const val DOT = "•" - const val PLUS = "+" -} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt new file mode 100644 index 0000000000..15d69db683 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -0,0 +1,12 @@ +package com.tangem.utils + +object StringsSigns { + + const val STARS = "\u2217\u2217\u2217" + const val DOT = "•" + const val PLUS = "+" + const val MINUS = "-" + const val DASH_SIGN = "—" + const val LOWER_SIGN = "<" + const val TILDE_SIGN = "~" +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt index ff348ef270..5bc81ee9f6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt @@ -47,6 +47,7 @@ import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.features.markets.impl.R import com.tangem.features.markets.ui.entity.MarketsListItemUM import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.utils.StringsSigns.MINUS import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop @@ -306,7 +307,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) { .padding(horizontal = TangemTheme.dimens.spacing5), ) { Text( - text = ratingPosition ?: "-", + text = ratingPosition ?: MINUS, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption1, maxLines = 1, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index d1421da969..cf63542cee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -34,7 +34,7 @@ import com.tangem.features.send.impl.presentation.state.previewdata.RecipientSta import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents -import com.tangem.utils.Strings.STARS +import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.ImmutableList private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 7ebb9058e7..169be97410 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -13,7 +13,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider -import com.tangem.utils.Strings.PLUS +import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.addOrReplace import kotlinx.collections.immutable.toPersistentList diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 5ca7d71e8e..44b3566f06 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -42,7 +42,8 @@ import com.tangem.features.staking.impl.presentation.state.previewdata.InitialSt import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import com.tangem.utils.Strings.DOT +import com.tangem.utils.StringsSigns.DOT +import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.extensions.orZero @Composable @@ -205,7 +206,7 @@ private fun StakingRewardBlock( val (text, textColor) = if (isRewardsToClaim) { annotatedReference( buildAnnotatedString { - append("+") + append(PLUS) appendSpace() append(rewardFiat) appendSpace() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 427d112540..c9ad194413 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -22,6 +22,8 @@ import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.viewmodels.SwapProcessDataState import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -243,7 +245,11 @@ internal class StateBuilder( showWarning = true, actions.onReceiveCardWarningClick, ), - amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), + amountTextFieldValue = TextFieldValue( + quoteModel.toTokenInfo.tokenAmount + .formatToUIRepresentation() + .appendApproximateSign(), + ), amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), token = toCurrencyStatus, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, @@ -793,7 +799,7 @@ internal class StateBuilder( tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, networkIconRes = uiStateHolder.receiveCardData.networkIconRes, - balance = toTokenStatus?.getFormattedAmount(isNeedSymbol = false) ?: UNKNOWN_AMOUNT_SIGN, + balance = toTokenStatus?.getFormattedAmount(isNeedSymbol = false) ?: DASH_SIGN, isBalanceHidden = isBalanceHiddenProvider(), ), warnings = emptyList(), @@ -1648,14 +1654,14 @@ internal class StateBuilder( } private fun CryptoCurrencyStatus.getFormattedAmount(isNeedSymbol: Boolean): String { - val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN + val amount = value.amount ?: return DASH_SIGN val symbol = if (isNeedSymbol) currency.symbol else "" return BigDecimalFormatter.formatCryptoAmount(amount, symbol, currency.decimals) } @Suppress("UnusedPrivateMember") private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + val fiatAmount = value.fiatAmount ?: return DASH_SIGN val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) @@ -1687,6 +1693,10 @@ internal class StateBuilder( } } + private fun String.appendApproximateSign(): String { + return "$TILDE_SIGN $this" + } + private companion object { private const val RU_LOCALE = "ru" private const val EN_LOCALE = "en" @@ -1694,7 +1704,6 @@ internal class StateBuilder( const val ADDRESS_FIRST_PART_LENGTH = 7 const val ADDRESS_SECOND_PART_LENGTH = 4 private const val PRICE_IMPACT_THRESHOLD = 0.1 - private const val UNKNOWN_AMOUNT_SIGN = "—" private const val MAX_DECIMALS_TO_SHOW = 8 private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" private const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index d58600cab3..4dd7a9395f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -38,7 +38,7 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R -import com.tangem.utils.Strings.STARS +import com.tangem.utils.StringsSigns.STARS @Suppress("LongMethod") @Composable diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 6b753b705a..5066dc9be6 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -32,7 +32,7 @@ import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -254,7 +254,7 @@ private fun TokenItem( text = if (token.addedTokenBalanceData.isBalanceHidden && !token.addedTokenBalanceData.amountEquivalent.isNullOrEmpty() ) { - Strings.STARS + StringsSigns.STARS } else { token.addedTokenBalanceData.amountEquivalent.orEmpty() }, @@ -270,7 +270,7 @@ private fun TokenItem( text = if (token.addedTokenBalanceData.isBalanceHidden && !token.addedTokenBalanceData.amount.isNullOrEmpty() ) { - Strings.STARS + StringsSigns.STARS } else { token.addedTokenBalanceData.amount.orEmpty() }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index 1e2ab2341d..3aacd1221a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -10,6 +10,8 @@ import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.StringsSigns.MINUS +import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString @@ -96,7 +98,7 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( private fun TxHistoryItem.getAmount(): String { val prefix = when (status) { TxHistoryItem.TransactionStatus.Failed -> "" - else -> if (isOutgoing) "-" else "+" + else -> if (isOutgoing) MINUS else PLUS } return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 394e52f37b..d3e4bdff81 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -22,7 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPre import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R -import com.tangem.utils.Strings.STARS +import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.toImmutableList @Composable diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index 3cbd9419a3..0d5d72efd5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -24,7 +24,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakedBlock import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.features.tokendetails.impl.R -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns @Composable internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifier = Modifier) { @@ -58,7 +58,7 @@ internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifie color = TangemTheme.colors.text.primary1, ) Text( - text = Strings.DOT, + text = StringsSigns.DOT, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt index ff7c37e28b..b68b55f129 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt @@ -11,7 +11,7 @@ import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState @Composable @@ -23,7 +23,7 @@ internal fun TokenCryptoAmount( when (state) { is TokenCryptoAmountState.Content -> { CryptoAmountText( - amount = if (isBalanceHidden) Strings.STARS else state.text, + amount = if (isBalanceHidden) StringsSigns.STARS else state.text, modifier = modifier, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt index 6bc1be0dbe..437bd97b09 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.composed import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState @Composable @@ -17,7 +17,7 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool when (state) { is TokenFiatAmountState.Content -> { FiatAmountText( - text = if (isBalanceHidden) Strings.STARS else state.text, + text = if (isBalanceHidden) StringsSigns.STARS else state.text, modifier, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt index 4822e0d489..4df035cf66 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenPrice.kt @@ -18,7 +18,7 @@ import com.tangem.core.ui.components.SpacerW6 import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoPriceState as TokenPriceChangeState @Composable @@ -33,7 +33,7 @@ internal fun TokenPrice(state: TokenPriceChangeState?, modifier: Modifier = Modi ) } is TokenPriceChangeState.Unknown -> { - PriceText(text = TokenItemState.UNKNOWN_AMOUNT_SIGN, modifier = modifier) + PriceText(text = DASH_SIGN, modifier = modifier) } is TokenPriceChangeState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) @@ -104,7 +104,7 @@ private fun PriceChangeIcon(type: PriceChangeType) { private fun PriceChangeText(type: PriceChangeType?, text: String?, modifier: Modifier = Modifier) { AnimatedContent(targetState = text, modifier = modifier, label = "Update the price change's text") { animatedText -> Text( - text = animatedText ?: TokenItemState.UNKNOWN_AMOUNT_SIGN, + text = animatedText ?: DASH_SIGN, color = when (type) { PriceChangeType.UP -> TangemTheme.colors.text.accent PriceChangeType.DOWN -> TangemTheme.colors.text.warning 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 c5bc2e083e..6338c9f8b0 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 @@ -161,8 +161,4 @@ internal sealed class TokenItemState { object Locked : CryptoPriceState() } - - 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/state/model/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt index d20706f5f8..d2ffe9d0d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt @@ -4,7 +4,8 @@ import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns +import com.tangem.utils.StringsSigns.DASH_SIGN /** Wallet card state */ @Immutable @@ -123,7 +124,7 @@ internal sealed interface WalletCardState { } companion object { - val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = Strings.STARS) } - val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = "—") } + val HIDDEN_BALANCE_TEXT by lazy { TextReference.Str(value = StringsSigns.STARS) } + val EMPTY_BALANCE_TEXT by lazy { TextReference.Str(value = DASH_SIGN) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index c16dc3e022..32651bb309 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -10,6 +10,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero import java.math.BigDecimal @@ -64,7 +65,7 @@ internal class TokenItemStateConverter( private fun CryptoCurrencyStatus.getFormattedAmount(): String { val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() - val amount = value.amount?.plus(yieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val amount = value.amount?.plus(yieldBalance) ?: return DASH_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } @@ -72,7 +73,7 @@ internal class TokenItemStateConverter( private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() val fiatYieldBalance = value.fiatRate?.times(yieldBalance).orZero() - val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index d31a165d9a..45a7931776 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -9,6 +9,8 @@ import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.utils.StringsSigns.MINUS +import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString @@ -96,7 +98,7 @@ internal class TxHistoryItemStateConverter( private fun TxHistoryItem.getAmount(): String { val prefix = when (status) { TxHistoryItem.TransactionStatus.Failed -> "" - else -> if (isOutgoing) "-" else "+" + else -> if (isOutgoing) MINUS else PLUS } return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 819d0d20d1..60ac94068a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -9,7 +9,7 @@ import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.VisaWalletIntents -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -21,7 +21,7 @@ internal class VisaTxHistoryItemStateConverter( override fun convert(value: VisaTxHistoryItem): TransactionState { val localDate = value.date.withZone(DateTimeZone.getDefault()) val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter) - val subtitle = "$time ${Strings.DOT} ${value.status.capitalize()}" + val subtitle = "$time ${StringsSigns.DOT} ${value.status.capitalize()}" return TransactionState.Content( txHash = value.id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 7cc578f380..07aaba8fe5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -53,7 +53,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.utils.Strings +import com.tangem.utils.StringsSigns private const val HALF_OF_ITEM_WIDTH = 0.5 @@ -291,7 +291,7 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: when (walletCardState) { is WalletCardState.Content -> { ResizableText( - text = if (isBalanceHidden) Strings.STARS else walletCardState.balance, + text = if (isBalanceHidden) StringsSigns.STARS else walletCardState.balance, fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32), color = TangemTheme.colors.text.primary1, From e33edbdf2d0c8f2bd3ce1a37669445db0a138ef8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jul 2024 18:49:35 +0500 Subject: [PATCH 146/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 10 +- .../presentation/state/fee/FeeConverter.kt | 18 ++- .../state/fee/SendFeeCustomFieldConverter.kt | 17 ++ .../fee/custom/EthereumCustomFeeConverter.kt | 2 +- .../fee/custom/KaspaCustomFeeConverter.kt | 153 ++++++++++++++++++ .../ui/fee/SendSpeedSelectorItem.kt | 5 +- gradle/dependencies.toml | 2 +- 8 files changed, 203 insertions(+), 8 deletions(-) create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6cb64bafa2..84cc72c8e2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -487,15 +487,17 @@ Транзакция не выполнена Сумма Вы можете установить комиссию за транзакцию, изменив значение в поле Satoshi per vByte. + Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Это стоимость, которую вы готовы заплатить за каждую единицу газа. Чем выше цена газа, тем быстрее ваша транзакция будет обработана. (Приоритетная комиссия включена) Приоритетная комиссия Комиссия, которую пользователь может заплатить майнерам или валидаторам за ускорение включения его транзакции в блок. + Комиссия, которую нужно заплатить за использование каждого неиспользованного выхода транзакции (UTXO) в сети Kaspa. Чем больше UTXO вы используете в транзакции, тем выше будет комиссия. + KAS за UTXO %1$s, %2$s Адрес Код назначения Введите адрес Адрес совпадает с адресом кошелька - Комиссия, которая будет взята за вашу транзакцию. Вы можете выставить своё собственное значение. Недопустимый Tag. Он не будет добавлен в транзакцию. Недопустимый Memo. Он не будет добавлен в транзакцию. Tag diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6b23058989..5ef91d4236 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. Start backup process + From your fiat card or bank account %d card %d cards @@ -257,6 +258,7 @@ No tokens found. Please try another request ID: %s Transaction ID copied + From another currency in your wallet The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have @@ -329,10 +331,12 @@ To start buying, exchanging or receiving this asset, add this token to at least 1 network This asset is not available Add to portfolio + Add token Available networks My portfolio Market To generate addresses for selected networks, you need to attach a Tangem card + Quick actions Select network Select wallet 1m @@ -348,6 +352,7 @@ Top Gainers Top losers Trending + About %s Based on %d rating Based on %d ratings @@ -486,6 +491,7 @@ Camera access denied %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. + Show QR-code or share your address Participate Failed to load the information about the referral program. Please try again later. Failed to load the information about the referral program. Error code: %s. Please try again later. @@ -541,16 +547,18 @@ The transaction is not completed Amount You can set your transaction fee by adjusting the value in the Satoshi per vByte field. + The fee that will be charged for your transaction. You can set your own value. Max fee This is the cost you are willing to pay for each unit of gas. The higher the gas price, the faster your transaction will be processed. (Priority fee included) Priority fee The fee that a user can pay to miners or validators to expedite the inclusion of their transaction in a block. + The fee required for using each unspent transaction output (UTXO) in the Kaspa network. The more UTXOs you use in a transaction, the higher the fee will be. + KAS per UTXO %1$s, %2$s Address Destination Tag Enter address Address is the same as wallet address - The fee that will be charged for your transaction. You can set your own value. Invalid Tag. It won\'t be added to the transaction. Invalid Memo. It won\'t be added to the transaction. Tag diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index 5109aa17b9..670da73277 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter +import com.tangem.features.send.impl.presentation.state.fee.custom.KaspaCustomFeeConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -37,6 +38,15 @@ internal class FeeConverter( ) } + private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + KaspaCustomFeeConverter( + clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, + appCurrencyProvider = appCurrencyProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, + ) + } + override fun convert(value: FeeSelectorState.Content): Fee { return when (val fees = value.fees) { is TransactionFee.Choosable -> { @@ -47,7 +57,12 @@ internal class FeeConverter( FeeType.Custom -> convertCustom(value, fees) } } - is TransactionFee.Single -> fees.normal + is TransactionFee.Single -> + when (value.selectedFee) { + FeeType.Market -> fees.normal + FeeType.Custom -> convertCustom(value, fees) + else -> fees.normal + } } } @@ -60,6 +75,7 @@ internal class FeeConverter( when (normalFee) { is Fee.Ethereum -> ethereumCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) is Fee.Bitcoin -> bitcoinCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) + is Fee.Kaspa -> kaspaCustomFeeConverter.convertBack(normalFee = normalFee, value = customValues) else -> { val customFee = customValues.firstOrNull() Fee.Common( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index 5fd2a4060d..d80cf917b0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.custom.BitcoinCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fee.custom.EthereumCustomFeeConverter +import com.tangem.features.send.impl.presentation.state.fee.custom.KaspaCustomFeeConverter import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -38,10 +39,20 @@ internal class SendFeeCustomFieldConverter( ) } + private val kaspaCustomFeeConverter by lazy(LazyThreadSafetyMode.NONE) { + KaspaCustomFeeConverter( + clickIntents = clickIntents, + stateRouterProvider = stateRouterProvider, + appCurrencyProvider = appCurrencyProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, + ) + } + override fun convert(value: Fee): ImmutableList { return when (value) { is Fee.Ethereum -> ethereumCustomFeeConverter.convert(value) is Fee.Bitcoin -> bitcoinCustomFeeConverter.convert(value) + is Fee.Kaspa -> kaspaCustomFeeConverter.convert(value) else -> persistentListOf() } } @@ -59,6 +70,12 @@ internal class SendFeeCustomFieldConverter( value = value, txSize = fee.txSize, ) + is Fee.Kaspa -> kaspaCustomFeeConverter.onValueChange( + customValues = feeSelectorState.customValues, + index = index, + value = value, + utxoCount = fee.utxoCount, + ) else -> feeSelectorState.customValues }, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index 0c9185cf91..fdee783db4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -43,7 +43,7 @@ internal class EthereumCustomFeeConverter( keyboardType = KeyboardType.Number, ), title = resourceReference(R.string.send_max_fee), - footer = resourceReference(R.string.send_evm_custom_fee_footer), + footer = resourceReference(R.string.send_custom_amount_fee_footer), label = getFiatReference( rate = feeCurrency?.fiatRate, value = feeValue, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt new file mode 100644 index 0000000000..05b117547d --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt @@ -0,0 +1,153 @@ +package com.tangem.features.send.impl.presentation.state.fee.custom + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.StateRouter +import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance +import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Provider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import java.math.RoundingMode + +internal class KaspaCustomFeeConverter( + private val clickIntents: SendClickIntents, + private val stateRouterProvider: Provider, + private val appCurrencyProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, +) : CustomFeeConverter { + + override fun convert(value: Fee.Kaspa): ImmutableList { + val feeValue = value.amount.value + val feeCurrency = feeCryptoCurrencyStatusProvider()?.value + val network = feeCryptoCurrencyStatusProvider()?.currency?.network?.id?.value + return if (network != null) { + persistentListOf( + SendTextField.CustomFee( + value = feeValue?.parseBigDecimal(value.amount.decimals).orEmpty(), + decimals = value.amount.decimals, + symbol = value.amount.currencySymbol, + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_AMOUNT_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Number, + ), + title = resourceReference(R.string.send_max_fee), + footer = resourceReference(R.string.send_custom_amount_fee_footer), + label = getFiatReference( + rate = feeCurrency?.fiatRate, + value = feeValue, + appCurrency = appCurrencyProvider(), + ), + keyboardActions = KeyboardActions(), + ), + SendTextField.CustomFee( + value = value.valuePerUtxo.parseBigDecimal(value.amount.decimals), + decimals = value.amount.decimals, + symbol = "", + title = resourceReference(R.string.send_custom_kaspa_per_utxo_title), + footer = resourceReference(R.string.send_custom_kaspa_per_utxo_footer), + onValueChange = { clickIntents.onCustomFeeValueChange(FEE_VALUE_PER_UTXO_INDEX, it) }, + keyboardOptions = KeyboardOptions( + imeAction = if (checkExceedBalance( + feeBalance = feeCurrency?.amount, + feeAmount = feeValue, + ) + ) { + ImeAction.None + } else { + ImeAction.Done + }, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) }, + ), + ), + ) + } else { + persistentListOf() + } + } + + override fun convertBack(normalFee: Fee.Kaspa, value: ImmutableList): Fee.Kaspa { + val feeAmount = value[FEE_AMOUNT_INDEX].value.parseToBigDecimal(value[FEE_AMOUNT_INDEX].decimals) + val valuePerUtxo = + value[FEE_VALUE_PER_UTXO_INDEX].value.parseToBigDecimal(value[FEE_VALUE_PER_UTXO_INDEX].decimals) + return normalFee.copy( + amount = normalFee.amount.copy(value = feeAmount), + valuePerUtxo = valuePerUtxo, + ) + } + + fun onValueChange( + customValues: ImmutableList, + index: Int, + value: String, + utxoCount: Int, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + return mutableCustomValues.apply { + when (index) { + FEE_AMOUNT_INDEX -> { + val valueDecimal = value.parseToBigDecimal(this[FEE_AMOUNT_INDEX].decimals) + val newValuePerUtxo = valueDecimal.divide( + /* divisor = */ utxoCount.toBigDecimal(), + /* scale = */ this[FEE_VALUE_PER_UTXO_INDEX].decimals, + /* roundingMode = */ RoundingMode.HALF_UP, + ) + set( + FEE_VALUE_PER_UTXO_INDEX, + this[FEE_VALUE_PER_UTXO_INDEX].copy( + value = newValuePerUtxo.parseBigDecimal(this[FEE_VALUE_PER_UTXO_INDEX].decimals), + ), + ) + set( + index, + this[index].copy( + value = value, + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = valueDecimal, + appCurrency = appCurrencyProvider(), + ), + ), + ) + } + FEE_VALUE_PER_UTXO_INDEX -> { + val valuePerUtxo = value.parseToBigDecimal(this[FEE_VALUE_PER_UTXO_INDEX].decimals) + val newFeeAmount = valuePerUtxo.multiply(utxoCount.toBigDecimal()) + set( + FEE_AMOUNT_INDEX, + this[FEE_AMOUNT_INDEX].copy( + value = newFeeAmount.parseBigDecimal(this[FEE_AMOUNT_INDEX].decimals), + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = newFeeAmount, + appCurrency = appCurrencyProvider(), + ), + ), + ) + set(index, this[index].copy(value = value)) + } + } + }.toImmutableList() + } + + private companion object { + private const val FEE_AMOUNT_INDEX = 0 + private const val FEE_VALUE_PER_UTXO_INDEX = 1 + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index b1cf3f315d..08e3ff9853 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -36,9 +36,8 @@ internal fun SendSpeedSelectorItem( val content = feeSelectorState as? FeeSelectorState.Content val amount = content?.getAmount(feeType) val (showDivider, isVisible) = content.getDividerAndVisibility(feeType) - val hasCustomValues = !content?.customValues.isNullOrEmpty() AnimatedVisibility( - visible = isVisible || hasCustomValues, + visible = isVisible, label = "Fee Selector Visibility Animation", enter = expandVertically().plus(fadeIn()), exit = shrinkVertically().plus(fadeOut()), @@ -135,7 +134,7 @@ private fun FeeSelectorState.Content?.getDividerAndVisibility(feeType: FeeType): val isNotSingle = this?.fees !is TransactionFee.Single return when (feeType) { FeeType.Slow -> true to isNotSingle - FeeType.Market -> isNotSingle to true + FeeType.Market -> (isNotSingle || hasCustomValues) to true FeeType.Fast -> hasCustomValues to isNotSingle FeeType.Custom -> false to hasCustomValues } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4742ffb2c9..e8dbee94ec 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-699" +tangemBlockchainSdk = "develop-700" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-370" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 09932a54049a3b8c7008926ae2ffd77f0e76c8bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 13:13:14 +0500 Subject: [PATCH 147/223] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 5 ++- .../transaction/usecase/EstimateFeeUseCase.kt | 36 +++++++++++++++---- .../feature/swap/domain/SwapInteractorImpl.kt | 32 ++++++++++------- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 8ce98f292d..8af69450c8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -75,7 +75,10 @@ internal object TransactionDomainModule { @Provides @Singleton fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase { - return EstimateFeeUseCase(walletManagersFacade) + return EstimateFeeUseCase( + walletManagersFacade = walletManagersFacade, + demoConfig = DemoConfig(), + ) } @Provides diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt index 541f04b900..2a32a62d43 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -8,10 +8,12 @@ import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.math.BigDecimal @@ -21,18 +23,27 @@ import java.math.BigDecimal */ class EstimateFeeUseCase( private val walletManagersFacade: WalletManagersFacade, + private val demoConfig: DemoConfig, ) { suspend operator fun invoke( amount: BigDecimal, - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): Flow> { return flow { - val result = walletManagersFacade.estimateFee( - amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) + val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount) + val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { + demoTransactionSender(userWallet, cryptoCurrency).estimateFee( + amount = amountData, + destination = "", + ) + } else { + walletManagersFacade.estimateFee( + amount = amountData, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + } val maybeFee = when (result) { is Result.Success -> result.data.right() @@ -43,6 +54,17 @@ class EstimateFeeUseCase( } } + private suspend fun demoTransactionSender( + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + ): DemoTransactionSender { + return DemoTransactionSender( + walletManagersFacade + .getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network) + ?: error("WalletManager is null"), + ) + } + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( currencySymbol = cryptoCurrency.symbol, value = amount, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 1e04778c76..37675be767 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1003,10 +1003,10 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency return coroutineScope { - val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> + val txFeeResult = getSelectedWalletSyncUseCase().getOrNull()?.let { userWallet -> getUnhandledFee( amount = amount.value, - userWalletId = userWalletId, + userWallet = userWallet, cryptoCurrency = fromToken, ) } @@ -1263,15 +1263,21 @@ internal class SwapInteractorImpl @Inject constructor( toAddress = toToken.value.networkAddress?.defaultAddress?.value.orEmpty(), ).fold( ifRight = { swapData -> - val feeData = transactionManager.getFee( - networkId = networkId, - amountToSend = amount.value, - currencyToSend = swapCurrencyConverter.convert(fromToken.currency), - destinationAddress = swapData.transaction.txTo, - increaseBy = INCREASE_GAS_LIMIT_BY, - data = (swapData.transaction as ExpressTransactionModel.DEX).txData, - derivationPath = fromToken.currency.network.derivationPath.value, - ) + val userWallet = getSelectedWallet() + val cardId = userWallet?.scanResponse?.card?.cardId + val feeData = if (cardId != null && isDemoCardUseCase(cardId)) { + getDemoFees(fromToken.currency) + } else { + transactionManager.getFee( + networkId = networkId, + amountToSend = amount.value, + currencyToSend = swapCurrencyConverter.convert(fromToken.currency), + destinationAddress = swapData.transaction.txTo, + increaseBy = INCREASE_GAS_LIMIT_BY, + data = (swapData.transaction as ExpressTransactionModel.DEX).txData, + derivationPath = fromToken.currency.network.derivationPath.value, + ) + } val txFeeState = when (feeData) { is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency) is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency) @@ -1380,12 +1386,12 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getUnhandledFee( amount: BigDecimal, - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): Either? { return estimateFeeUseCase( amount = amount, - userWalletId = userWalletId, + userWallet = userWallet, cryptoCurrency = cryptoCurrency, ).firstOrNull() } From b0cb12b56acb3005abeeed58ec8770186e20ec97 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 17:49:37 +0500 Subject: [PATCH 148/223] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 12 +++++++++++ .../datasource/api/stakekit/StakeKitApi.kt | 7 +++++-- .../data/staking/DefaultStakingRepository.kt | 20 +++++++++++++++---- .../action/EnterActionResponseConverter.kt | 8 ++++---- .../{EnterAction.kt => StakingAction.kt} | 2 +- .../staking/GetStakingTransactionUseCase.kt | 2 +- .../staking/IsStakeMoreAvailableUseCase.kt | 19 ++++++++++++++++++ .../staking/repositories/StakingRepository.kt | 15 +++++++------- .../repository/MockStakingRepository.kt | 9 ++++++--- .../state/stub/StakingClickIntentsStub.kt | 3 +++ .../viewmodel/StakingClickIntents.kt | 3 +++ .../viewmodel/StakingViewModel.kt | 4 ++++ 12 files changed, 82 insertions(+), 22 deletions(-) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/{EnterAction.kt => StakingAction.kt} (95%) create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 10f1264ba6..c280208083 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -144,4 +144,16 @@ internal object StakingDomainModule { stakingErrorResolver = stakingErrorResolver, ) } + + @Provides + @Singleton + fun provideIsStakeMoreAvailableUseCase( + stakingRepository: StakingRepository, + stakingErrorResolver: StakingErrorResolver, + ): IsStakeMoreAvailableUseCase { + return IsStakeMoreAvailableUseCase( + stakingRepository = stakingRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index fc7dab4bd6..4d6ff9ca76 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -5,11 +5,11 @@ import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO import retrofit2.http.* @Suppress("LongParameterList") @@ -48,6 +48,9 @@ interface StakeKitApi { @POST("actions/enter") suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse + @POST("actions/exit") + suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse + @POST("actions/enter/estimate-gas") suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 5249e59c74..cab788363d 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -24,14 +24,15 @@ import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* -import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.action.StakingAction import com.tangem.domain.staking.model.action.StakingActionCommonType -import com.tangem.domain.staking.model.transaction.StakingGasEstimate import com.tangem.domain.staking.model.transaction.ActionParams +import com.tangem.domain.staking.model.transaction.StakingGasEstimate import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -155,10 +156,13 @@ internal class DefaultStakingRepository( } } - override suspend fun createEnterAction(params: ActionParams): EnterAction { + override suspend fun createAction(params: ActionParams): StakingAction { return withContext(dispatchers.io) { val body = createActionRequestBody(params) - val response = stakeKitApi.createEnterAction(body) + val response = when (params.actionCommonType) { + StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(body) + StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(body) + } enterActionResponseConverter.convert(response.getOrThrow()) } @@ -436,6 +440,14 @@ internal class DefaultStakingRepository( ) } + override fun isStakeMoreAvailable(networkId: Network.ID): Boolean { + val blockchain = Blockchain.fromId(networkId.value) + return when (blockchain) { + Blockchain.Solana -> false + else -> true + } + } + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt index 888d805305..c228248f43 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -2,17 +2,17 @@ package com.tangem.data.staking.converters.action import com.tangem.data.staking.converters.transaction.StakingTransactionConverter import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse -import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.action.StakingAction import com.tangem.utils.converter.Converter class EnterActionResponseConverter( private val actionStatusConverter: ActionStatusConverter, private val stakingActionTypeConverter: StakingActionTypeConverter, private val transactionConverter: StakingTransactionConverter, -) : Converter { +) : Converter { - override fun convert(value: EnterActionResponse): EnterAction { - return EnterAction( + override fun convert(value: EnterActionResponse): StakingAction { + return StakingAction( id = value.id, integrationId = value.integrationId, status = actionStatusConverter.convert(value.status), diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingAction.kt similarity index 95% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingAction.kt index c8b8a639f5..5922003ad7 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingAction.kt @@ -4,7 +4,7 @@ import com.tangem.domain.staking.model.transaction.StakingTransaction import org.joda.time.DateTime import java.math.BigDecimal -data class EnterAction( +data class StakingAction( val id: String, val integrationId: String, val status: StakingActionStatus, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index 12107bff68..2c843b9161 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -18,7 +18,7 @@ class GetStakingTransactionUseCase( suspend operator fun invoke(params: ActionParams): Either { return Either.catch { - val createAction = stakingRepository.createEnterAction(params) + val createAction = stakingRepository.createAction(params) // workaround, sometimes transaction is not created immediately after actions/enter delay(PATCH_TRANSACTION_REQUEST_DELAY) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt new file mode 100644 index 0000000000..30e148dccd --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.Network + +class IsStakeMoreAvailableUseCase( + private val stakingRepository: StakingRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + operator fun invoke(networkId: Network.ID): Either { + return Either + .catch { stakingRepository.isStakeMoreAvailable(networkId) } + .mapLeft { stakingErrorResolver.resolve(it) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 3776a2e27b..a374a30d09 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -1,16 +1,14 @@ package com.tangem.domain.staking.repositories -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.Yield -import com.tangem.domain.staking.model.action.EnterAction -import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.* -import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.action.StakingAction import com.tangem.domain.staking.model.transaction.ActionParams +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -60,7 +58,7 @@ interface StakingRepository { addresses: List, ): YieldBalanceList - suspend fun createEnterAction(params: ActionParams): EnterAction + suspend fun createAction(params: ActionParams): StakingAction suspend fun estimateGas(params: ActionParams): StakingGasEstimate @@ -71,4 +69,7 @@ interface StakingRepository { suspend fun storeUnsubmittedHash(unsubmittedTransactionMetadata: UnsubmittedTransactionMetadata) suspend fun sendUnsubmittedHashes() + + /** Returns whether additional staking is possible if there is already active staking */ + fun isStakeMoreAvailable(networkId: Network.ID): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 1c27d431af..68589655af 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -3,13 +3,14 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* -import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.action.StakingAction import com.tangem.domain.staking.model.action.StakingActionStatus import com.tangem.domain.staking.model.action.StakingActionType import com.tangem.domain.staking.model.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow @@ -164,8 +165,8 @@ class MockStakingRepository : StakingRepository { balances = listOf(YieldBalance.Error), ) - override suspend fun createEnterAction(params: ActionParams): EnterAction { - return EnterAction( + override suspend fun createAction(params: ActionParams): StakingAction { + return StakingAction( id = "quis", integrationId = "persequeris", status = StakingActionStatus.PROCESSING, @@ -224,4 +225,6 @@ class MockStakingRepository : StakingRepository { override suspend fun sendUnsubmittedHashes() { /* no-op */ } + + override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 64e66f66d8..e9a6ec4c62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.stub import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @@ -35,4 +36,6 @@ object StakingClickIntentsStub : StakingClickIntents { override fun onExploreClick() {} override fun onShareClick() {} + + override fun onActiveStake(activeStake: BalanceState) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 0173545ee9..05d121258a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType internal interface StakingClickIntents : AmountScreenClickIntents { @@ -24,6 +25,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun selectRewardValidator(rewardValue: String) + fun onActiveStake(activeStake: BalanceState) + fun onExploreClick() fun onShareClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 5549b2dbf0..157dfbebf4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -217,6 +217,10 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onNextClick() } + override fun onActiveStake(activeStake: BalanceState) { + // todo unstake + } + override fun onExploreClick() { val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content From 9c7a593e179e4371a3bfe622269f6401caf822ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 17:59:37 +0500 Subject: [PATCH 149/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 6 ++ core/res/src/main/res/values/strings.xml | 10 +++ .../components/currency/icon/CurrencyIcon.kt | 10 +-- .../components/inputrow/InputRowImageBase.kt | 2 + .../components/inputrow/InputRowImageInfo.kt | 3 + .../inputrow/inner/InputRowAsyncImage.kt | 7 ++- .../core/ui/extensions/MarkdownExtension.kt | 7 ++- .../core/ui/extensions/TextReference.kt | 12 ++++ .../tangem/core/ui/utils/GrayscaleUtils.kt | 11 +++- .../details/utils/UserWalletMappers.kt | 2 +- .../state/InnerYieldBalanceState.kt | 4 ++ .../state/StakingStateController.kt | 1 + .../presentation/state/StakingStateRouter.kt | 8 ++- .../impl/presentation/state/StakingUiState.kt | 8 +++ .../RewardsValidatorStateConverter.kt | 22 +++---- .../converters/YieldBalancesConverter.kt | 20 +++--- .../previewdata/InitialStakingStatePreview.kt | 9 +-- .../SetInitialDataStateTransformer.kt | 2 + .../ui/StakingConfirmationContent.kt | 11 +++- .../ui/StakingInitialInfoContent.kt | 63 +++++++++++++------ .../ui/StakingNavigationButtons.kt | 7 ++- .../impl/presentation/ui/StakingScreen.kt | 1 + .../viewmodel/StakingViewModel.kt | 46 +++++++++----- .../ui/components/TokenInfoBlock.kt | 10 +-- .../components/staking/TokenStakingBlock.kt | 16 +---- 25 files changed, 201 insertions(+), 97 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6cb64bafa2..1eeeaec28d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -86,6 +86,12 @@ Скопировать адрес Создать Свое + + %d день + %d дня + %d дней + %d дней + Удалить Отключено Готово diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6b23058989..8a4d5d07db 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -39,6 +39,7 @@ Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. Start backup process + From your fiat card or bank account %d card %d cards @@ -87,6 +88,10 @@ Copy address Create Custom + + %d day + %d days + Delete Disabled Done @@ -257,6 +262,7 @@ No tokens found. Please try another request ID: %s Transaction ID copied + From another currency in your wallet The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have @@ -329,10 +335,12 @@ To start buying, exchanging or receiving this asset, add this token to at least 1 network This asset is not available Add to portfolio + Add token Available networks My portfolio Market To generate addresses for selected networks, you need to attach a Tangem card + Quick actions Select network Select wallet 1m @@ -348,6 +356,7 @@ Top Gainers Top losers Trending + About %s Based on %d rating Based on %d ratings @@ -486,6 +495,7 @@ Camera access denied %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. + Show QR-code or share your address Participate Failed to load the information about the referral program. Please try again later. Failed to load the information about the referral program. Error code: %s. Please try again later. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index ce67d13f6f..d06e59fc30 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -12,9 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA -import com.tangem.core.ui.utils.GrayscaleColorFilter -import com.tangem.core.ui.utils.NORMAL_ALPHA +import com.tangem.core.ui.utils.getGreyScaleColorFilter /** * Cryptocurrency icon with network badge @@ -76,11 +74,7 @@ private fun BoxScope.ContentIconContainer( ) { val networkBadgeOffset = TangemTheme.dimens.spacing4 val (alpha, colorFilter) = remember(icon.isGrayscale) { - if (icon.isGrayscale) { - GRAY_SCALE_ALPHA to GrayscaleColorFilter - } else { - NORMAL_ALPHA to null - } + getGreyScaleColorFilter(icon.isGrayscale) } ContentIcon( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt index 00213b0077..417a7edbec 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -20,6 +20,7 @@ internal fun InputRowImageBase( modifier: Modifier = Modifier, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, + isGrayscaleImage: Boolean = false, extraContent: @Composable RowScope.() -> Unit = {}, ) { Row( @@ -29,6 +30,7 @@ internal fun InputRowImageBase( ) { InputRowAsyncImage( imageUrl = imageUrl, + isGrayscale = isGrayscaleImage, modifier = Modifier .size(TangemTheme.dimens.spacing36), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt index c4fb398c6e..f9811bbe1a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.res.TangemThemePreview * @param imageUrl icon to load * @param subtitleColor subtitle text color * @param captionColor caption text color + * @param isGrayscaleImage whether to display grayscale image */ @Suppress("LongParameterList") @Composable @@ -45,6 +46,7 @@ fun InputRowImageInfo( title: TextReference? = null, subtitleColor: Color = TangemTheme.colors.text.primary1, captionColor: Color = TangemTheme.colors.text.tertiary, + isGrayscaleImage: Boolean = false, ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), @@ -64,6 +66,7 @@ fun InputRowImageInfo( imageUrl = imageUrl, subtitleColor = subtitleColor, captionColor = captionColor, + isGrayscaleImage = isGrayscaleImage, ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt index 3fd1ae7722..b85e91ba30 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt @@ -10,17 +10,22 @@ import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.currency.icon.LoadingIcon import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.getGreyScaleColorFilter /** * Loads image by url for icon in the input row * * @param imageUrl url of the image * @param modifier modifier + * @param isGrayscale whether to apply grayscale filter */ @Composable -internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier) { +internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier, isGrayscale: Boolean = false) { + val (alpha, colorFilter) = getGreyScaleColorFilter(isGrayscale = isGrayscale) SubcomposeAsyncImage( modifier = modifier, + colorFilter = colorFilter, + alpha = alpha, model = ImageRequest.Builder(context = LocalContext.current) .data(imageUrl) .crossfade(enable = true) diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt index b8b11c058f..c510835242 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.extensions import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight @@ -55,4 +56,8 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): return this } -fun AnnotatedString.Builder.appendSpace() = append(" ") \ No newline at end of file +fun AnnotatedString.Builder.appendSpace() = append(" ") + +fun AnnotatedString.Builder.appendColored(text: String, color: Color) = withStyle(SpanStyle(color = color)) { + append(text) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 386f69b66b..4aab281036 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.AnnotatedString.Builder import androidx.compose.ui.text.buildAnnotatedString import org.intellij.markdown.MarkdownElementTypes import kotlin.contracts.ExperimentalContracts @@ -100,6 +101,17 @@ fun annotatedReference(value: AnnotatedString): TextReference { return TextReference.Annotated(value) } +/** + * Creates a [TextReference] using an annotated string value. + * + * @param onAnnotated The annotated string builder. + * @return A [TextReference] representing the provided annotated string value. + */ +@Composable +inline fun annotatedReference(onAnnotated: Builder.() -> Unit): TextReference { + return TextReference.Annotated(buildAnnotatedString { onAnnotated() }) +} + /** * Creates a [TextReference] using a plural string resource ID with count and optional format arguments. * diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt index f3edb75b60..a71cf3875a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/GrayscaleUtils.kt @@ -8,4 +8,13 @@ const val GRAY_SCALE_ALPHA = 0.4f const val NORMAL_ALPHA = 1f val GrayscaleColorFilter: ColorFilter - get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) \ No newline at end of file + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) + +/** + * Returns alpha and color filter for grayscale if [isGrayscale] is true + */ +fun getGreyScaleColorFilter(isGrayscale: Boolean): Pair = if (isGrayscale) { + GRAY_SCALE_ALPHA to GrayscaleColorFilter +} else { + NORMAL_ALPHA to null +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index 94fc6e00b1..eab5ed50e3 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -9,7 +9,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM import com.tangem.features.details.impl.R -import com.tangem.utils.Strings.STARS +import com.tangem.utils.StringsSigns.STARS import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 61015e2357..4d5cfcff8a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.Yield import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal sealed class InnerYieldBalanceState { data class Data( @@ -19,14 +20,17 @@ data class BalanceGroupedState( val items: ImmutableList, val footer: TextReference?, val title: TextReference, + val type: BalanceGroupType, ) data class BalanceState( val validator: Yield.Validator, val cryptoValue: String, + val cryptoDecimal: BigDecimal, val cryptoAmount: TextReference, val fiatAmount: TextReference, val rawCurrencyId: String?, + val unbondingPeriod: TextReference, ) enum class BalanceGroupType { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index f263b18a14..d3570e6c23 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -44,6 +44,7 @@ internal class StakingStateController @Inject constructor() { isBalanceHidden = false, event = consumedEvent(), bottomSheetConfig = null, + routeType = RouteType.STAKE, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index d3edfd4322..2c0b98f544 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -13,8 +13,12 @@ internal class StakingStateRouter( } fun onNextClick() { - when (stateController.uiState.value.currentStep) { - StakingStep.InitialInfo -> showAmount() + when (stateController.value.currentStep) { + StakingStep.InitialInfo -> when (stateController.value.routeType) { + RouteType.STAKE -> showAmount() + RouteType.UNSTAKE -> showConfirmation() + RouteType.CLAIM -> showRewardsValidators() + } StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Amount, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index cd1e65b946..4a1be94850 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -23,6 +23,7 @@ internal data class StakingUiState( val confirmationState: StakingStates.ConfirmationState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, + val routeType: RouteType, val event: StateEvent, ) { @@ -55,6 +56,7 @@ internal sealed class StakingStates { val rewardSchedule: String, val onInfoClick: (InfoType) -> Unit, val yieldBalance: InnerYieldBalanceState, + val isStakeMoreAvailable: Boolean, ) : InitialInfoState() data class Empty( @@ -98,4 +100,10 @@ enum class StakingStep { Amount, Confirmation, Validators, +} + +enum class RouteType { + STAKE, + UNSTAKE, + CLAIM, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 169be97410..e05f2dfcdf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,7 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.converters -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -10,12 +9,12 @@ import com.tangem.domain.staking.model.BalanceType import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.addOrReplace import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -33,13 +32,6 @@ internal class RewardsValidatorStateConverter( StakingStates.RewardsValidatorsState.Data( isPrimaryButtonEnabled = true, rewards = balances - // todo remove when real data is available - .addOrReplace( - item = balances.first().copy( - type = BalanceType.REWARDS, - ), - predicate = { true }, - ) .filter { it.type == BalanceType.REWARDS } .mapRewardBalances(cryptoCurrencyStatus) .toPersistentList(), @@ -56,11 +48,16 @@ internal class RewardsValidatorStateConverter( } val cryptoValue = balance.amount.times(balance.pricePerShare) val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) - + val unbondingPeriod = yield.metadata.cooldownPeriod.days validator?.toBalanceState( cryptoCurrencyStatus = cryptoCurrencyStatus, cryptoValue = cryptoValue, fiatValue = fiatValue, + unbondingPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(unbondingPeriod), + ), ) } @@ -68,6 +65,7 @@ internal class RewardsValidatorStateConverter( cryptoCurrencyStatus: CryptoCurrencyStatus, cryptoValue: BigDecimal, fiatValue: BigDecimal?, + unbondingPeriod: TextReference, ): BalanceState { val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency @@ -92,9 +90,11 @@ internal class RewardsValidatorStateConverter( return BalanceState( validator = this, cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), + cryptoDecimal = cryptoValue, cryptoAmount = cryptoAmount, fiatAmount = fiatAmount, rawCurrencyId = cryptoCurrency.id.rawCurrencyId, + unbondingPeriod = unbondingPeriod, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 28ede76079..f6d436f3c3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,7 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.converters +import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -14,7 +16,6 @@ import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.addOrReplace import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.toPersistentList @@ -54,13 +55,7 @@ internal class YieldBalancesConverter( } private fun getGroupedBalance(balance: YieldBalanceItem) = balance.items - // todo remove when real data is available - .addOrReplace( - item = balance.items.first().copy( - type = BalanceType.REWARDS, - ), - predicate = { true }, - ) + .sortedBy { it.type } .groupBy { it.type.toGroup() } .mapNotNull { item -> val (title, footer) = getGroupTitle(item.key) @@ -69,6 +64,7 @@ internal class YieldBalancesConverter( items = item.value.mapBalances().toPersistentList(), footer = footer, title = it, + type = item.key, ) } } @@ -83,11 +79,12 @@ internal class YieldBalancesConverter( } val cryptoAmount = balance.amount * balance.pricePerShare val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) - + val unbondingPeriod = yield.metadata.cooldownPeriod.days validator?.let { BalanceState( validator = validator, cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), + cryptoDecimal = cryptoAmount, cryptoAmount = stringReference( BigDecimalFormatter.formatCryptoAmount( cryptoAmount = cryptoAmount, @@ -102,6 +99,11 @@ internal class YieldBalancesConverter( ), ), rawCurrencyId = balance.rawCurrencyId, + unbondingPeriod = pluralReference( + id = R.plurals.common_days, + count = unbondingPeriod, + formatArgs = wrappedList(unbondingPeriod), + ), ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 4eadd4916a..2c9ac37b88 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -2,10 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.Yield -import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState -import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState -import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.* import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { @@ -21,6 +18,7 @@ internal object InitialStakingStatePreview { rewardSchedule = "Block", onInfoClick = {}, yieldBalance = InnerYieldBalanceState.Empty, + isStakeMoreAvailable = true, ) val stateWithYield = defaultState.copy( @@ -32,10 +30,12 @@ internal object InitialStakingStatePreview { BalanceGroupedState( title = stringReference("Staked"), footer = null, + type = BalanceGroupType.ACTIVE, items = persistentListOf( BalanceState( cryptoValue = "100", cryptoAmount = stringReference("100 SOL"), + cryptoDecimal = "100".toBigDecimal(), fiatAmount = stringReference("100 $"), rawCurrencyId = null, validator = Yield.Validator( @@ -50,6 +50,7 @@ internal object InitialStakingStatePreview { votingPower = null, preferred = false, ), + unbondingPeriod = stringReference("3 days"), ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 3cc9baa334..c001927cd1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -31,6 +31,7 @@ import java.math.BigDecimal internal class SetInitialDataStateTransformer( private val clickIntents: StakingClickIntents, private val yield: Yield, + private val isStakeMoreAvailable: Boolean, private val cryptoCurrencyStatusProvider: Provider, private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, @@ -92,6 +93,7 @@ internal class SetInitialDataStateTransformer( rewardSchedule = yield.metadata.rewardSchedule, onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalancesConverter.convert(Unit), + isStakeMoreAvailable = isStakeMoreAvailable, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 98f42fccf2..261d4437cf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -18,21 +18,23 @@ import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.RouteType import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.TransactionDoneState @Composable internal fun StakingConfirmationContent( amountState: AmountState, state: StakingStates.ConfirmationState, clickIntents: StakingClickIntents, + type: RouteType, ) { if (state !is StakingStates.ConfirmationState.Data) return @@ -60,7 +62,9 @@ internal fun StakingConfirmationContent( isEditingDisabled = true, onClick = {}, ) - ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators) + if (type == RouteType.STAKE) { + ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators) + } StakingFeeBlock(feeState = state.feeState) NotificationsBlock(notifications = state.notifications) SpacerHMax() @@ -89,6 +93,7 @@ private fun Preview_StakingConfirmationContent() { amountState = AmountStatePreviewData.amountState, state = ConfirmationStatePreviewData.assentStakingState, clickIntents = StakingClickIntentsStub, + type = RouteType.STAKE, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 44b3566f06..4eb8244ef3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -35,9 +34,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState -import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState -import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.InfoType @@ -77,7 +74,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl } AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { if (it is InnerYieldBalanceState.Data) { - ActiveStakingBlock(it.balance) + ActiveStakingBlock(it.balance, clickIntents::onActiveStake) } } } @@ -204,17 +201,15 @@ private fun StakingRewardBlock( onRewardsClick: () -> Unit, ) { val (text, textColor) = if (isRewardsToClaim) { - annotatedReference( - buildAnnotatedString { - append(PLUS) - appendSpace() - append(rewardFiat) - appendSpace() - append(DOT) - appendSpace() - append(rewardCrypto) - }, - ) to TangemTheme.colors.text.primary1 + annotatedReference { + append(PLUS) + appendSpace() + append(rewardFiat) + appendSpace() + append(DOT) + appendSpace() + append(rewardCrypto) + } to TangemTheme.colors.text.primary1 } else { resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary } @@ -235,7 +230,7 @@ private fun StakingRewardBlock( } @Composable -private fun ActiveStakingBlock(groups: List) { +private fun ActiveStakingBlock(groups: List, onClick: (BalanceState) -> Unit) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { @@ -253,15 +248,43 @@ private fun ActiveStakingBlock(groups: List) { ) { group.items.forEachIndexed { index, balance -> key(balance.validator.address) { + val caption = combinedReference( + if (group.type == BalanceGroupType.UNSTAKED) { + resourceReference(R.string.staking_details_unbonding_period) + annotatedReference { + appendSpace() + appendColored( + text = balance.unbondingPeriod.resolveReference(), + color = TangemTheme.colors.text.accent, + ) + } + } else { + resourceReference(R.string.app_name) + annotatedReference { + appendSpace() + appendColored( + text = BigDecimalFormatter.formatPercent( + percent = balance.validator.apr.orZero(), + useAbsoluteValue = true, + ), + color = TangemTheme.colors.text.accent, + ) + } + }, + ) InputRowImageInfo( title = group.title.takeIf { index == 0 }, subtitle = stringReference(balance.validator.name), - caption = stringReference( - BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), - ), + caption = caption, + isGrayscaleImage = group.type == BalanceGroupType.UNSTAKED, infoTitle = balance.fiatAmount, infoSubtitle = balance.cryptoAmount, imageUrl = balance.validator.image.orEmpty(), + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = { onClick(balance) }, + ), ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 73e2c6e526..41b426c018 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -126,7 +126,12 @@ private fun getButtonData(currentState: StakingUiState): Pair Unit)? StakingStep.InitialInfo -> { val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { - R.string.staking_stake_more to currentState.clickIntents::onNextClick + val click = if (initialState.isStakeMoreAvailable) { + currentState.clickIntents::onNextClick + } else { + null + } + R.string.staking_stake_more to click } else { R.string.common_next to currentState.clickIntents::onNextClick } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index e1b08be7ac..c26935b80c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -155,6 +155,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M amountState = uiState.amountState, state = uiState.confirmationState, clickIntents = uiState.clickIntents, + type = uiState.routeType, ) StakingStep.Validators -> { val confirmState = uiState.confirmationState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 157dfbebf4..c2b477f54b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -14,10 +14,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.staking.GetStakingTransactionUseCase -import com.tangem.domain.staking.EstimateGasUseCase -import com.tangem.domain.staking.SaveUnsubmittedHashUseCase -import com.tangem.domain.staking.SubmitHashUseCase +import com.tangem.domain.staking.* import com.tangem.domain.staking.model.Yield import com.tangem.domain.staking.model.action.StakingActionCommonType import com.tangem.domain.staking.model.transaction.ActionParams @@ -32,10 +29,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingStateController -import com.tangem.features.staking.impl.presentation.state.StakingStateRouter -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer @@ -66,6 +59,7 @@ internal class StakingViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, private val submitHashUseCase: SubmitHashUseCase, + private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -117,15 +111,26 @@ internal class StakingViewModel @Inject constructor( viewModelScope.launch { stateController.update(SetConfirmationStateInProgressTransformer()) + val confirmationState = + value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + + val actionType = when (value.routeType) { + RouteType.STAKE -> StakingActionCommonType.ENTER + RouteType.UNSTAKE -> StakingActionCommonType.EXIT + RouteType.CLAIM -> TODO() + } + val stakingTransaction = getStakingTransactionUseCase( params = ActionParams( - actionCommonType = StakingActionCommonType.ENTER, + actionCommonType = actionType, integrationId = yield.id, amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided"), address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address"), - validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), + validatorAddress = validatorState.chosenValidator.address, token = yield.token, ), ).getOrElse { @@ -151,9 +156,15 @@ internal class StakingViewModel @Inject constructor( ), ) + val actionType = when (value.routeType) { + RouteType.STAKE -> StakingActionCommonType.ENTER + RouteType.UNSTAKE -> StakingActionCommonType.EXIT + RouteType.CLAIM -> TODO() + } + val stakingGasEstimate = estimateGasUseCase( params = ActionParams( - actionCommonType = StakingActionCommonType.ENTER, + actionCommonType = actionType, integrationId = yield.id, amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided"), @@ -209,16 +220,19 @@ internal class StakingViewModel @Inject constructor( } override fun openRewardsValidators() { - stakingStateRouter.showRewardsValidators() + stateController.update { it.copy(routeType = RouteType.CLAIM) } + onNextClick() } override fun selectRewardValidator(rewardValue: String) { stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue)) - stakingStateRouter.onNextClick() + onNextClick() } override fun onActiveStake(activeStake: BalanceState) { - // todo unstake + stateController.update { it.copy(routeType = RouteType.UNSTAKE) } + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue)) + onNextClick() } override fun onExploreClick() { @@ -253,10 +267,14 @@ internal class StakingViewModel @Inject constructor( getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( ifRight = { cryptoCurrencyStatus = it + + val networkId = cryptoCurrencyStatus.currency.network.id + val isStakeMoreAvailable = isStakeMoreAvailableUseCase(networkId) stateController.update( transformer = SetInitialDataStateTransformer( clickIntents = this@StakingViewModel, yield = yield, + isStakeMoreAvailable = isStakeMoreAvailable.getOrElse { false }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider { appCurrency }, 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 index 773dc2868f..a6d81965bb 100644 --- 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 @@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA -import com.tangem.core.ui.utils.GrayscaleColorFilter -import com.tangem.core.ui.utils.NORMAL_ALPHA +import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.features.tokendetails.impl.R @@ -46,11 +44,7 @@ internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Mod } val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { - if (state.iconState.isGrayscale) { - GRAY_SCALE_ALPHA to GrayscaleColorFilter - } else { - NORMAL_ALPHA to null - } + getGreyScaleColorFilter(state.iconState.isGrayscale) } CurrencyIcon( modifier = Modifier diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index afa3fc743d..c2d7940cbc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -19,9 +19,7 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA -import com.tangem.core.ui.utils.GrayscaleColorFilter -import com.tangem.core.ui.utils.NORMAL_ALPHA +import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingErrorBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingLoadingBlock @@ -74,11 +72,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi ) { Row { val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { - if (state.iconState.isGrayscale) { - GRAY_SCALE_ALPHA to GrayscaleColorFilter - } else { - NORMAL_ALPHA to null - } + getGreyScaleColorFilter(state.iconState.isGrayscale) } CurrencyIcon( modifier = Modifier @@ -138,11 +132,7 @@ private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) ) { Row { val (alpha, colorFilter) = remember(iconState.isGrayscale) { - if (iconState.isGrayscale) { - GRAY_SCALE_ALPHA to GrayscaleColorFilter - } else { - NORMAL_ALPHA to null - } + getGreyScaleColorFilter(iconState.isGrayscale) } CurrencyIcon( modifier = Modifier From ebfa4fed41bb1de59358329c2a74bb646daf84c4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 16 Jul 2024 19:59:07 +0500 Subject: [PATCH 150/223] Updated on 2026-08-14 --- .../datasource/api/stakekit/StakeKitApi.kt | 6 ++ .../models/request/ActionRequestBody.kt | 75 +++++++++++-------- .../data/staking/DefaultStakingRepository.kt | 32 ++++++-- .../converters/YieldBalanceConverter.kt | 4 + .../action/PendingActionConverter.kt | 44 +++++++++++ .../domain/staking/model/YieldBalance.kt | 34 +++++++++ .../model/action/StakingActionCommonType.kt | 1 + .../staking/model/transaction/ActionParams.kt | 3 + 8 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 4d6ff9ca76..2d5bd551ac 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -51,12 +51,18 @@ interface StakeKitApi { @POST("actions/exit") suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse + @POST("actions/pending") + suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse + @POST("actions/enter/estimate-gas") suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse @POST("actions/exit/estimate-gas") suspend fun estimateGasOnExit(@Body body: ActionRequestBody): ApiResponse + @POST("actions/pending/estimate-gas") + suspend fun estimateGasOnPending(@Body body: PendingActionRequestBody): ApiResponse + @PATCH("transactions/{transactionId}") suspend fun constructTransaction( @Path("transactionId") transactionId: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index e044b1f9a9..fa3ec28913 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -4,6 +4,20 @@ import com.squareup.moshi.Json import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.domain.staking.model.action.StakingActionType + +data class PendingActionRequestBody( + @Json(name = "type") + val type: StakingActionType, + @Json(name = "integrationId") + val integrationId: String, + @Json(name = "passthrough") + val passthrough: String, + @Json(name = "args") + val args: ActionRequestBodyArgs, + @Json(name = "gasArgs") + val gasArgs: GasArgs? = null, // used only in estimate_gas request +) data class ActionRequestBody( @Json(name = "integrationId") @@ -11,40 +25,39 @@ data class ActionRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") - val args: EnterActionRequestBodyArgs, + val args: ActionRequestBodyArgs, @Json(name = "referralCode") val referralCode: String? = null, @Json(name = "gasArgs") val gasArgs: GasArgs? = null, // used only in estimate_gas request -) { +) - data class EnterActionRequestBodyArgs( - @Json(name = "amount") - val amount: String, - @Json(name = "validatorAddress") - val validatorAddress: String? = null, - @Json(name = "validatorAddresses") - val validatorAddresses: List? = null, - @Json(name = "providerId") - val providerId: String? = null, - @Json(name = "duration") - val duration: String? = null, - @Json(name = "nfts") - val nfts: List? = null, - @Json(name = "ledgerWalletAPICompatible") - val ledgerWalletAPICompatible: Boolean? = null, - @Json(name = "tronResource") - val tronResource: String? = null, - @Json(name = "signatureVerification") - val signatureVerification: SignatureVerification? = null, - @Json(name = "inputToken") - val inputToken: TokenDTO? = null, - ) +data class ActionRequestBodyArgs( + @Json(name = "amount") + val amount: String, + @Json(name = "validatorAddress") + val validatorAddress: String? = null, + @Json(name = "validatorAddresses") + val validatorAddresses: List? = null, + @Json(name = "providerId") + val providerId: String? = null, + @Json(name = "duration") + val duration: String? = null, + @Json(name = "nfts") + val nfts: List? = null, + @Json(name = "ledgerWalletAPICompatible") + val ledgerWalletAPICompatible: Boolean? = null, + @Json(name = "tronResource") + val tronResource: String? = null, + @Json(name = "signatureVerification") + val signatureVerification: SignatureVerification? = null, + @Json(name = "inputToken") + val inputToken: TokenDTO? = null, +) - data class SignatureVerification( - @Json(name = "message") - val message: String, - @Json(name = "signed") - val signed: String, - ) -} \ No newline at end of file +data class SignatureVerification( + @Json(name = "message") + val message: String, + @Json(name = "signed") + val signed: String, +) \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index cab788363d..646ff8d040 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -26,6 +26,7 @@ import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* import com.tangem.domain.staking.model.action.StakingAction import com.tangem.domain.staking.model.action.StakingActionCommonType +import com.tangem.domain.staking.model.action.StakingActionType import com.tangem.domain.staking.model.transaction.ActionParams import com.tangem.domain.staking.model.transaction.StakingGasEstimate import com.tangem.domain.staking.model.transaction.StakingTransaction @@ -158,10 +159,12 @@ internal class DefaultStakingRepository( override suspend fun createAction(params: ActionParams): StakingAction { return withContext(dispatchers.io) { - val body = createActionRequestBody(params) val response = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(body) - StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(body) + StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction(createActionRequestBody(params)) + StakingActionCommonType.EXIT -> stakeKitApi.createExitAction(createActionRequestBody(params)) + StakingActionCommonType.PENDING -> stakeKitApi.createPendingAction( + createPendingActionRequestBody(params), + ) } enterActionResponseConverter.convert(response.getOrThrow()) @@ -170,11 +173,12 @@ internal class DefaultStakingRepository( override suspend fun estimateGas(params: ActionParams): StakingGasEstimate { return withContext(dispatchers.io) { - val body = createActionRequestBody(params) - val gasEstimateDTO = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(body) - StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(body) + StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter(createActionRequestBody(params)) + StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit(createActionRequestBody(params)) + StakingActionCommonType.PENDING -> stakeKitApi.estimateGasOnPending( + createPendingActionRequestBody(params), + ) } gasEstimateConverter.convert(gasEstimateDTO.getOrThrow()) @@ -432,7 +436,7 @@ internal class DefaultStakingRepository( return ActionRequestBody( integrationId = params.integrationId, addresses = Address(params.address), - args = ActionRequestBody.EnterActionRequestBodyArgs( + args = ActionRequestBodyArgs( amount = params.amount.toFormattedString(params.token.decimals), inputToken = tokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, @@ -440,6 +444,18 @@ internal class DefaultStakingRepository( ) } + private fun createPendingActionRequestBody(params: ActionParams): PendingActionRequestBody { + return PendingActionRequestBody( + integrationId = params.integrationId, + type = params.type ?: StakingActionType.UNKNOWN, + passthrough = params.passthrough.orEmpty(), + args = ActionRequestBodyArgs( + amount = params.amount.toFormattedString(params.token.decimals), + validatorAddress = params.validatorAddress, + ), + ) + } + override fun isStakeMoreAvailable(networkId: Network.ID): Boolean { val blockchain = Blockchain.fromId(networkId.value) return when (blockchain) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index d0abdee20e..c3dd742fbe 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -1,5 +1,6 @@ package com.tangem.data.staking.converters +import com.tangem.data.staking.converters.action.PendingActionConverter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.domain.staking.model.BalanceItem import com.tangem.domain.staking.model.BalanceType @@ -9,6 +10,8 @@ import com.tangem.utils.converter.Converter internal class YieldBalanceConverter : Converter { + private val pendingActionConverter by lazy(LazyThreadSafetyMode.NONE) { PendingActionConverter() } + override fun convert(value: Data): YieldBalance { return if (value.balance.isEmpty()) { YieldBalance.Empty @@ -22,6 +25,7 @@ internal class YieldBalanceConverter : Converter { + + private val stakingActionTypeConverter by lazy(LazyThreadSafetyMode.NONE) { StakingActionTypeConverter() } + + override fun convert(value: BalanceDTO.PendingAction): PendingAction { + return PendingAction( + type = stakingActionTypeConverter.convert(value.type), + passthrough = value.passthrough, + args = with(value.args) { + PendingAction.PendingActionArgs( + amount = this?.amount?.let { + PendingAction.PendingActionArgs.Amount( + required = it.required, + minimum = it.minimum, + maximum = it.maximum, + ) + }, + duration = this?.duration?.let { + PendingAction.PendingActionArgs.Duration( + required = it.required, + minimum = it.minimum, + maximum = it.maximum, + ) + }, + validatorAddress = this?.validatorAddress?.required, + validatorAddresses = this?.validatorAddresses?.required, + tronResource = this?.tronResource?.let { + PendingAction.PendingActionArgs.TronResource( + required = it.required, + options = it.options, + ) + }, + signatureVerification = this?.signatureVerification?.required, + ) + }, + ) + } +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt index 6ed53d2486..b82ff8bf02 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt @@ -1,5 +1,6 @@ package com.tangem.domain.staking.model +import com.tangem.domain.staking.model.action.StakingActionType import java.math.BigDecimal sealed class YieldBalance { @@ -36,8 +37,41 @@ data class BalanceItem( val pricePerShare: BigDecimal, val rawCurrencyId: String?, val validatorAddress: String?, + val pendingActions: List, ) +data class PendingAction( + val type: StakingActionType, + val passthrough: String, + val args: PendingActionArgs?, +) { + data class PendingActionArgs( + val amount: Amount?, + val duration: Duration?, + val validatorAddress: Boolean?, + val validatorAddresses: Boolean?, + val tronResource: TronResource?, + val signatureVerification: Boolean?, + ) { + data class Amount( + val required: Boolean, + val minimum: BigDecimal?, + val maximum: BigDecimal?, + ) + + data class Duration( + val required: Boolean, + val minimum: Int?, + val maximum: Int?, + ) + + data class TronResource( + val required: Boolean, + val options: List, + ) + } +} + enum class BalanceType { AVAILABLE, STAKED, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt index 59f5e782d9..56e96d2caa 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt @@ -3,4 +3,5 @@ package com.tangem.domain.staking.model.action enum class StakingActionCommonType { ENTER, EXIT, + PENDING, } \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt index ab5aa8f530..0d84c4146a 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt @@ -2,6 +2,7 @@ package com.tangem.domain.staking.model.transaction import com.tangem.domain.staking.model.Token import com.tangem.domain.staking.model.action.StakingActionCommonType +import com.tangem.domain.staking.model.action.StakingActionType import java.math.BigDecimal data class ActionParams( @@ -11,4 +12,6 @@ data class ActionParams( val address: String, val validatorAddress: String, val token: Token, + val passthrough: String? = null, + val type: StakingActionType? = null, ) \ No newline at end of file From 21ef61585b8395a4fae6223d9596024374ab336e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 17:43:48 +0300 Subject: [PATCH 151/223] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tap/common/extensions/Navigation.kt | 9 + .../tap/di/domain/MarketsDomainModule.kt | 22 ++ .../com/tangem/datasource/di/NetworkModule.kt | 36 ++- .../com/tangem/pagination/BatchListSource.kt | 59 +++- .../OperationWIthTheSameIdInProgress.kt | 5 + .../ui/components/inputrow/InputRowChecked.kt | 74 +++++ .../inputrow/inner/DividerContainer.kt | 4 +- .../core/ui/utils/BigDecimalFormatter.kt | 74 +++++ .../ui/utils/BigDecimalFormatterCompat.kt | 52 ++++ .../data/markets/MarketsBatchUpdateFetcher.kt | 12 +- features/markets/impl/build.gradle.kts | 5 + .../impl/DefaultMarketsListComponent.kt | 5 + .../markets/model/MarketsListModel.kt | 187 +++++++++--- .../model/SortByBottomSheetContentUM.kt | 9 + .../converters/MarketsTokenItemConverter.kt | 144 +++++++++ .../statemanager/MarketsListUMStateManager.kt | 106 +++++++ .../statemanager/MarketsListUiItemsManager.kt | 275 ++++++++++++++++++ .../markets/model/utils/LoggingUtils.kt | 48 +++ .../tangem/features/markets/ui/MarketsList.kt | 87 +++++- .../markets/ui/components/MarketsListItem.kt | 31 +- .../MarketsListSortByBottomSheet.kt | 88 ++++++ .../markets/ui/entity/MarketsListItemUM.kt | 2 +- .../markets/ui/entity/MarketsListUM.kt | 4 + 24 files changed, 1255 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt create mode 100644 core/pagination/src/main/java/com/tangem/pagination/exception/OperationWIthTheSameIdInProgress.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowChecked.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c58fa8b592..35083a7d65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -74,6 +74,7 @@ dependencies { implementation(projects.domain.qrScanning.models) implementation(projects.domain.staking) implementation(projects.domain.walletConnect) + implementation(projects.domain.markets) implementation(projects.common) implementation(projects.common.routing) @@ -109,6 +110,7 @@ dependencies { implementation(projects.data.qrScanning) implementation(projects.data.staking) implementation(projects.data.walletConnect) + implementation(projects.data.markets) /** Features */ implementation(projects.features.onboarding) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 251edacdf4..5d7a14709f 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -8,6 +8,15 @@ import com.tangem.wallet.R import timber.log.Timber fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider) { + if (backStackEntryCount > 0) { + val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name + + if (name == currentFragmentName) { + Timber.d("Fragment $name is already at the top of the stack") + return + } + } + Timber.d("Showing $name route") val isPoppedBack = popBackStackImmediate(name, 0) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt new file mode 100644 index 0000000000..765e3bcb7f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object MarketsDomainModule { + + @Provides + @Singleton + fun provideGetMarketsTokenListFlowUseCase( + marketsTokenRepository: MarketsTokenRepository, + ): GetMarketsTokenListFlowUseCase { + return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository) + } +} \ 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 9b2c5b5d29..6951732450 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 @@ -124,7 +124,9 @@ class NetworkModule { context = context, appVersionProvider = appVersionProvider, baseUrl = PROD_V1_TANGEM_TECH_BASE_URL, - timeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, + timeouts = Timeouts( + callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS, + ), requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)), ) } @@ -142,6 +144,11 @@ class NetworkModule { context = context, appVersionProvider = appVersionProvider, baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, + timeouts = Timeouts( + callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + connectTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + readTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, + ), requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)), ) } @@ -151,16 +158,25 @@ class NetworkModule { context: Context, appVersionProvider: AppVersionProvider, baseUrl: String, - timeoutSeconds: Long? = null, + timeouts: Timeouts = Timeouts(), requestHeaders: List = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)), ): T { val client = OkHttpClient.Builder() .let { builder -> - if (timeoutSeconds != null) { - builder.callTimeout(timeoutSeconds, TimeUnit.SECONDS) - } else { - builder + var b = builder + if (timeouts.callTimeoutSeconds != null) { + b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS) } + if (timeouts.connectTimeoutSeconds != null) { + b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.readTimeoutSeconds != null) { + b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS) + } + if (timeouts.writeTimeoutSeconds != null) { + b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS) + } + b } .addHeaders( *requestHeaders.toTypedArray(), @@ -179,6 +195,13 @@ class NetworkModule { .create(T::class.java) } + private data class Timeouts( + val callTimeoutSeconds: Long? = null, + val connectTimeoutSeconds: Long? = null, + val readTimeoutSeconds: Long? = null, + val writeTimeoutSeconds: Long? = null, + ) + private companion object { const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/" const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/" @@ -191,5 +214,6 @@ class NetworkModule { const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/" const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L + const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L } } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index 1cc865396d..d286dafab8 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -1,5 +1,6 @@ package com.tangem.pagination +import com.tangem.pagination.exception.OperationWIthTheSameIdInProgress import com.tangem.pagination.fetcher.BatchFetcher import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow @@ -59,6 +60,7 @@ fun BatchListSource( ): BatchListSource = DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) +@Suppress("LargeClass") private class DefaultBatchListSource( private val fetchDispatcher: CoroutineDispatcher, private val context: BatchingContext, @@ -90,19 +92,21 @@ private class DefaultBatchListSource awaitCancellation() } finally { withContext(NonCancellable) { + stopAllUpdates() loadMoreActionJob = null loadMoreActionJob = null lastRequestResult.value = null - stopAllUpdates() state.value = BatchListState(emptyList(), PaginationStatus.None) } } } scope.launch { - context.actionsFlow.collect { action -> - collectActions(action) - } + context.actionsFlow + .conflate() + .collect { action -> + collectActions(action) + } } } @@ -113,7 +117,7 @@ private class DefaultBatchListSource loadMoreActionJob?.cancel() reloadActionJob?.cancel() stopAllUpdates() - reloadActionJob = scope.launch(fetchDispatcher) { + reloadActionJob = scope.launchFetch { reloadTask(action) } } @@ -122,15 +126,25 @@ private class DefaultBatchListSource return } - loadMoreActionJob = scope.launch(fetchDispatcher) { + loadMoreActionJob = scope.launchFetch { reloadActionJob?.join() loadMoreTask(action) } } is BatchAction.UpdateBatches -> { + if (reloadActionJob?.isActive == true) { + return + } + if (updateFetcher == null) return // If the request with the same operationId is in progress, skip the request - if (updateInProgressExists(action.operationId)) return + if (updateInProgressExists(action.operationId)) { + updateResults.tryEmit( + action.updateRequest to BatchUpdateResult.Error( + OperationWIthTheSameIdInProgress(action.operationId), + ), + ) + } if (action.async) { collectAsyncUpdateAction(action) @@ -140,6 +154,7 @@ private class DefaultBatchListSource } BatchAction.CancelAllUpdates -> { if (updateFetcher == null) return + stopAllUpdates() } is BatchAction.CancelUpdates -> { @@ -154,9 +169,10 @@ private class DefaultBatchListSource } private fun collectAsyncUpdateAction(action: BatchAction.UpdateBatches) { - val job = scope.launch(fetchDispatcher) { + val job = scope.launchFetch { updateBatchesAsyncTask(action) } + val actionJob = action to job updateAsyncJobs.update { it + actionJob } @@ -172,7 +188,7 @@ private class DefaultBatchListSource private fun collectSyncUpdateAction(action: BatchAction.UpdateBatches) { // Lazily start a job so we can avoid batch update collisions // by waiting for other tasks with the same keys to complete - val job = scope.launch(fetchDispatcher, start = CoroutineStart.LAZY) { + val job = scope.launchFetch(start = CoroutineStart.LAZY) { updateBatchesTask(action) } @@ -180,7 +196,7 @@ private class DefaultBatchListSource waitingUpdateJobs.update { it + actionJob } - scope.launch(fetchDispatcher) { + scope.launchFetch { // Wait for other update tasks that mutate batches with the same keys updateJobs.first { workingJobs -> action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() @@ -217,6 +233,8 @@ private class DefaultBatchListSource BatchFetchResult.Error(it) } + currentCoroutineContext().ensureActive() + state.value = when (res) { is BatchFetchResult.Success -> { val key = generateNewKey(listOf()) @@ -306,10 +324,11 @@ private class DefaultBatchListSource updateRequest = action.updateRequest, ) } catch (t: Throwable) { - currentCoroutineContext().ensureActive() BatchUpdateResult.Error(t) } + currentCoroutineContext().ensureActive() + if (result is BatchUpdateResult.Success) { state.update { currentState -> val resMap = result.data.associateBy { it.key } @@ -333,7 +352,13 @@ private class DefaultBatchListSource val updateContext = UpdateContext(request = action.updateRequest, action.keys) with(updateFetcher) { - updateContext.fetchUpdateAsync(batchesToUpdate, action.updateRequest) + runCatching { + updateContext.fetchUpdateAsync(batchesToUpdate, action.updateRequest).also { + currentCoroutineContext().ensureActive() + } + }.getOrElse { + updateResults.emit(action.updateRequest to BatchUpdateResult.Error(it)) + } } } @@ -342,6 +367,8 @@ private class DefaultBatchListSource object : BatchUpdateFetcher.UpdateContext { override suspend fun update(update: List>.() -> BatchUpdateResult) { + currentCoroutineContext().ensureActive() + val stateToFetchUpdateBasedOn = state.value.data.filter { keysToUpdate.contains(it.key) } @@ -349,7 +376,6 @@ private class DefaultBatchListSource val result = runCatching { stateToFetchUpdateBasedOn.update() }.getOrElse { - currentCoroutineContext().ensureActive() BatchUpdateResult.Error(it) } @@ -430,4 +456,11 @@ private class DefaultBatchListSource } } } + + private fun CoroutineScope.launchFetch( + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit, + ): Job { + return launch(context = fetchDispatcher + SupervisorJob(), start = start, block = block) + } } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/exception/OperationWIthTheSameIdInProgress.kt b/core/pagination/src/main/java/com/tangem/pagination/exception/OperationWIthTheSameIdInProgress.kt new file mode 100644 index 0000000000..5cf81ea1cd --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/exception/OperationWIthTheSameIdInProgress.kt @@ -0,0 +1,5 @@ +package com.tangem.pagination.exception + +class OperationWIthTheSameIdInProgress(operationId: String) : RuntimeException( + "Operation is already in progress - id:$operationId ", +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowChecked.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowChecked.kt new file mode 100644 index 0000000000..ca8da83f36 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowChecked.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components.inputrow + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun InputRowChecked(text: TextReference, checked: Boolean, modifier: Modifier = Modifier) { + Row( + modifier = modifier.padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + AnimatedVisibility( + modifier = Modifier, + visible = checked, + ) { + Icon( + painter = rememberVectorPainter(image = ImageVector.vectorResource(id = R.drawable.ic_check_24)), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + if (checked.not()) { + Box(Modifier.height(TangemTheme.dimens.size24)) + } + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Column(Modifier.background(TangemTheme.colors.background.primary)) { + InputRowChecked( + text = stringReference("Title Title Title Title Title Title Title Title Title"), + checked = false, + modifier = Modifier.width(300.dp), + ) + InputRowChecked( + text = stringReference("Title Title Title Title Title Title Title Title Title"), + checked = true, + modifier = Modifier.width(300.dp), + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt index a5510a378d..434b7751e7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/DividerContainer.kt @@ -3,7 +3,7 @@ package com.tangem.core.ui.components.inputrow.inner import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -19,7 +19,7 @@ fun DividerContainer( Box(modifier = modifier) { content() if (showDivider) { - Divider( + HorizontalDivider( modifier = Modifier .align(Alignment.BottomCenter) .padding(paddingValues), 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 4e5256d1d8..e7c941c536 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 @@ -1,5 +1,7 @@ package com.tangem.core.ui.utils +import android.icu.text.CompactDecimalFormat +import android.os.Build import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.LOWER_SIGN @@ -219,5 +221,77 @@ object BigDecimalFormatter { } } + /** + * Adds a proper currency sign for the provided formatted [amount] + * ex. '10.0k" -> "$10.0k", "string" -> "$string" + */ + fun addCurrencySymbolToStringAmount( + amount: String, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + val sampleAmount = BigDecimal.TEN + val currency = getCurrency(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + maximumFractionDigits = 0 + minimumFractionDigits = 0 + this.currency = currency + } + + val formatted = formatter.format(sampleAmount) + .replace(currency.getSymbol(locale), fiatCurrencySymbol) + .replace(sampleAmount.toString(), amount) + + return formatted + } + + /** + * "123456.6" -> "$123.457K" + * "12345.6" -> "$123.046K" + */ + @Suppress("MagicNumber") + fun formatCompactAmount( + amount: BigDecimal, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return BigDecimalFormatterCompat.formatCompactAmountNoLocaleContext( + amount = amount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } + + val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.longValueExact().toString().count() + val digitsToFormat = 6 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ).apply { + minimumSignificantDigits = 4 + maximumSignificantDigits = digitsToFormat + } + + val rawAmount = formatter.format(amount.setScale(0, RoundingMode.HALF_UP)) + + return addCurrencySymbolToStringAmount( + amount = rawAmount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } + private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt new file mode 100644 index 0000000000..82aab46559 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatterCompat.kt @@ -0,0 +1,52 @@ +package com.tangem.core.ui.utils + +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.Locale + +internal object BigDecimalFormatterCompat { + + /** + * Formats value as [BigDecimalFormatter.formatCompactAmount] does using only "T","B","M","K" suffixes + * Used for < API24 compatibility + */ + @Suppress("MagicNumber", "UnnecessaryParentheses") + fun formatCompactAmountNoLocaleContext( + amount: BigDecimal, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + val value = amount.setScale(0, RoundingMode.HALF_UP).longValueExact() + + val formatted = when { + value > 1_000_000_000_000L -> { + val trillion = value / 1_000_000_000_000 + val billion = (value % 1_000_000_000_000) / 1_000_000_000 + "$trillion.${billion}T" + } + value > 1_000_000_000L -> { + val billion = value / 1_000_000_000 + val million = (value % 1_000_000_000) / 1_000_000 + "$billion.${million}B" + } + value > 1_000_000L -> { + val million = value / 1_000_000 + val thousand = (value % 1_000_000) / 1_000 + "$million.${thousand}M" + } + value > 1_000L -> { + val thousand = value / 1_000 + "${thousand}K" + } + else -> return value.toString() + } + + return BigDecimalFormatter.addCurrencySymbolToStringAmount( + amount = formatted, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt index fa24fa4144..625f90797d 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/MarketsBatchUpdateFetcher.kt @@ -63,11 +63,13 @@ internal class MarketsBatchUpdateFetcher( } } is TokenMarketUpdateRequest.UpdateQuotes -> { - val quotesRes = tangemTechApi.getQuotes( - currencyId = updateRequest.currencyId, - coinIds = idsToUpdate.joinToString(separator = ","), - fields = quoteFields.joinToString(separator = ","), - ).getOrThrow() + val quotesRes = retryOnError { + tangemTechApi.getQuotes( + currencyId = updateRequest.currencyId, + coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), + fields = quoteFields.joinToString(separator = ","), + ).getOrThrow() + } update { val res = toUpdate.map { batch -> diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 86ae97c792..201feb6d22 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -15,6 +15,11 @@ dependencies { /* Project - API */ api(projects.features.markets.api) + /* Domain */ + implementation(projects.domain.markets) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + /* Compose */ implementation(deps.compose.coil) implementation(deps.compose.foundation) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt index 316720a3b6..55757a3a42 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.component.impl import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -30,6 +31,10 @@ internal class DefaultMarketsListComponent @AssistedInject constructor( ) { val state by model.state.collectAsStateWithLifecycle() + LaunchedEffect(bottomSheetState.value) { + model.containerBottomSheetState.value = bottomSheetState.value + } + MarketsList( onHeaderSizeChange = onHeaderSizeChange, state = state, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt index 6cbf590bc5..b0d1dac2b3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt @@ -1,61 +1,174 @@ package com.tangem.features.markets.model +import androidx.compose.runtime.Stable +import arrow.core.getOrElse import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.components.fields.entity.SearchBarUM -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.markets.impl.R +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.features.markets.component.BottomSheetState +import com.tangem.features.markets.model.statemanager.MarketsListUMStateManager +import com.tangem.features.markets.model.statemanager.MarketsListUiItemsManager import com.tangem.features.markets.ui.entity.ListUM -import com.tangem.features.markets.ui.entity.MarketsListUM import com.tangem.features.markets.ui.entity.SortByTypeUM -import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toImmutableList +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L + @ComponentScoped +@Stable internal class MarketsListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, ) : Model() { - val state = MutableStateFlow( - MarketsListUM( - list = ListUM.Loading, - searchBar = SearchBarUM( - placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), - query = "", - onQueryChange = {}, - isActive = false, - onActiveChange = { }, - ), - selectedSortBy = SortByTypeUM.Rating, - selectedInterval = MarketsListUM.TrendInterval.H24, - onIntervalClick = {}, - onSortByButtonClick = {}, - ), + private val currentAppCurrency = getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private val visibleItemIds = MutableStateFlow>(emptyList()) + val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) + private val marketsListUMStateManager = MarketsListUMStateManager( + onLoadMoreUiItems = { activeListManager.loadMore() }, + visibleItemsChanged = { visibleItemIds.value = it }, ) + private val marketsListManager = MarketsListUiItemsManager( + logTag = "main", + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + currentAppCurrency = Provider { currentAppCurrency.value }, + currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + private val searchMarketsListManager = MarketsListUiItemsManager( + logTag = "search", + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + currentAppCurrency = Provider { currentAppCurrency.value }, + currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + + private var activeListManager: MarketsListUiItemsManager = marketsListManager + + val state = marketsListUMStateManager.state.asStateFlow() + init { modelScope.launch { - delay(timeMillis = 5000) - state.update { - it.copy( - list = ListUM.Content( - items = MarketChartListItemPreviewDataProvider().values - .flatMap { item -> List(size = 10) { item } } - .mapIndexed { index, item -> - item.copy(id = index.toString()) - } - .toImmutableList(), - ), - ) - } + marketsListManager.uiItems + .collectLatest { + marketsListUMStateManager.onUiItemsChanged(it) + } } + + state.onEach { + if (it.list !is ListUM.Content) { + visibleItemIds.value = emptyList() + } + }.launchIn(modelScope) + + // update all lists when user's currency has changed + currentAppCurrency + .drop(1) + .onEach { + marketsListManager.reload( + interval = marketsListUMStateManager.selectedInterval, + sortBy = marketsListUMStateManager.selectedSortByType, + ) + if (marketsListUMStateManager.isInSearchState) { + // TODO + searchMarketsListManager.reload( + interval = marketsListUMStateManager.selectedInterval, + sortBy = marketsListUMStateManager.selectedSortByType, + ) + } + }.launchIn(modelScope) + + // load charts when new batch is being loaded + marketsListManager.onLastBatchLoadedSuccess + .onEach { + marketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + } + .launchIn(modelScope) + + // listen currently selected interval, update charts if sorting=rating, or reload all list + modelScope.launch(dispatchers.default) { + marketsListUMStateManager.state + .map { it.selectedInterval } + .distinctUntilChanged() + .drop(1) + .collectLatest { interval -> + when (marketsListUMStateManager.selectedSortByType) { + SortByTypeUM.Rating -> { + marketsListManager.updateUIWithSameState() + val batchKeys = marketsListManager.getBatchKeysByItemIds(visibleItemIds.value) + marketsListManager.loadCharts(batchKeys, interval) + } + else -> marketsListManager.reload(interval, marketsListUMStateManager.selectedSortByType) + } + } + } + + // reload list when sorting type has changed + modelScope.launch { + marketsListUMStateManager.state + .map { it.selectedSortBy } + .distinctUntilChanged() + .drop(1) + .collectLatest { + marketsListManager.reload(marketsListUMStateManager.selectedInterval, it) + } + } + + // listen current visible batch and update charts + modelScope.launch(dispatchers.default) { + visibleItemIds + .mapNotNull { + if (it.isNotEmpty()) { + activeListManager.getBatchKeysByItemIds(visibleItemIds.value) + } else { + null + } + } + .distinctUntilChanged() + .collectLatest { visibleBatchKeys -> + activeListManager.loadCharts(visibleBatchKeys, marketsListUMStateManager.selectedInterval) + } + } + + // initial loading + marketsListManager.reload( + interval = marketsListUMStateManager.selectedInterval, + sortBy = marketsListUMStateManager.selectedSortByType, + ) } - // TODO + private var updateQuotesJob = JobHolder() + private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { + launch { + while (true) { + delay(timeMillis) + // Update quotes only when the container bottom sheet is in the expanded state + containerBottomSheetState.first { it == BottomSheetState.EXPANDED } + activeListManager.updateQuotes() + } + }.saveIn(updateQuotesJob) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt new file mode 100644 index 0000000000..092b37423a --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.markets.model + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.markets.ui.entity.SortByTypeUM + +data class SortByBottomSheetContentUM( + val selectedOption: SortByTypeUM, + val onOptionClicked: (SortByTypeUM) -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt new file mode 100644 index 0000000000..6366b68d38 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt @@ -0,0 +1,144 @@ +package com.tangem.features.markets.model.converters + +import com.tangem.common.ui.charts.state.DefaultPointValuesConverter +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarket +import com.tangem.features.markets.ui.entity.MarketsListItemUM +import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import java.math.RoundingMode + +internal class MarketsTokenItemConverter( + private val currentTrendInterval: TrendInterval, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: TokenMarket): MarketsListItemUM { + return MarketsListItemUM( + id = value.id, + name = value.name, + currencySymbol = value.symbol, + ratingPosition = value.marketRating?.toString(), + marketCap = value.getMarketCap(), + iconUrl = value.imageUrlLarge, + price = value.getCurrentPrice(), + trendPercentText = value.getTrendPercent(), + trendType = value.getTrendType(), + chardData = value.getChartData(), + ) + } + + fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM { + require(prev.id == new.id) { + "Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]" + } + + return prevUI.copy( + name = new.name, + currencySymbol = new.symbol, + ratingPosition = new.marketRating?.toString(), + marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() }, + iconUrl = new.imageUrlLarge, + price = ifChanged(prev = prev.tokenQuotes, new = new.tokenQuotes, prevR = prevUI.price) { + new.getCurrentPrice( + prev = prev, + ) + }, + trendPercentText = ifChanged( + prev.tokenQuotes, + new.tokenQuotes, + prevUI.trendPercentText, + ) { new.getTrendPercent() }, + trendType = ifChanged(prev.tokenQuotes, new.tokenQuotes, prevUI.trendType) { new.getTrendType() }, + chardData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chardData) { new.getChartData() }, + ) + } + + private inline fun ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R { + return if (force || prev != new) change(new) else prevR + } + + private fun TokenMarket.getMarketCap(): String? { + val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null + + return BigDecimalFormatter.formatCompactAmount( + value, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { + val prevPrice = prev?.tokenQuotes?.currentPrice + + val priceText = BigDecimalFormatter.formatFiatAmount( + fiatAmount = tokenQuotes.currentPrice, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + + val changeType = if (prevPrice != null) { + if (tokenQuotes.currentPrice > prevPrice) { + PriceChangeType.UP + } else { + PriceChangeType.DOWN + } + } else { + null + } + + return MarketsListItemUM.Price( + text = priceText, + changeType = changeType, + ) + } + + private fun TokenMarket.getChartData(): MarketChartRawData? { + val chart = when (currentTrendInterval) { + TrendInterval.H24 -> tokenCharts.h24 + TrendInterval.D7 -> tokenCharts.week + TrendInterval.M1 -> tokenCharts.month + } + + return chart?.let { ct -> + DefaultPointValuesConverter.convert( + MarketChartData.Data( + y = ct.priceY, + x = ct.timeStamp.map { it.toBigDecimal() }, + ), + ) + } + } + + private fun TokenMarket.getTrendType(): PriceChangeType { + val percent = when (currentTrendInterval) { + TrendInterval.H24 -> tokenQuotes.h24Percent() + TrendInterval.D7 -> tokenQuotes.weekPercent() + TrendInterval.M1 -> tokenQuotes.monthPercent() + }.setScale(2, RoundingMode.UP) + + return when (percent.compareTo(BigDecimal.ZERO)) { + 1 -> PriceChangeType.UP + -1 -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } + } + + private fun TokenMarket.getTrendPercent(): String { + val percent = when (currentTrendInterval) { + TrendInterval.H24 -> tokenQuotes.h24Percent() + TrendInterval.D7 -> tokenQuotes.weekPercent() + TrendInterval.M1 -> tokenQuotes.monthPercent() + } + + return BigDecimalFormatter.formatPercent( + percent = percent, + useAbsoluteValue = true, + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt new file mode 100644 index 0000000000..052239a869 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt @@ -0,0 +1,106 @@ +package com.tangem.features.markets.model.statemanager + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.model.SortByBottomSheetContentUM +import com.tangem.features.markets.ui.entity.ListUM +import com.tangem.features.markets.ui.entity.MarketsListItemUM +import com.tangem.features.markets.ui.entity.MarketsListUM +import com.tangem.features.markets.ui.entity.SortByTypeUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +@Stable +internal class MarketsListUMStateManager( + private val onLoadMoreUiItems: () -> Unit, + private val visibleItemsChanged: (itemsKeys: List) -> Unit, +) { + + private var sortByBottomSheetIsShown + get() = state.value.sortByBottomSheet.isShow + set(value) = state.update { it.copy(sortByBottomSheet = it.sortByBottomSheet.copy(isShow = value)) } + + private val isInSearchStateFlow = MutableStateFlow(false) + + var isInSearchState + get() = isInSearchStateFlow.value + set(value) { isInSearchStateFlow.value = value } + + var selectedSortByType + get() = state.value.selectedSortBy + set(value) = state.update { + it.copy( + selectedSortBy = value, + sortByBottomSheet = it.sortByBottomSheet.copy( + content = (it.sortByBottomSheet.content as SortByBottomSheetContentUM).copy( + selectedOption = value, + ), + ), + ) + } + + var selectedInterval + get() = state.value.selectedInterval + set(value) = state.update { it.copy(selectedInterval = value) } + + val state = MutableStateFlow(state()) + + fun onUiItemsChanged(uiItems: ImmutableList) { + state.update { + if (uiItems.isEmpty()) { + it.copy( + list = ListUM.Loading, + ) + } else { + it.copy( + list = ListUM.Content( + items = uiItems, + loadMore = onLoadMoreUiItems, + visibleIdsChanged = visibleItemsChanged, + ), + ) + } + } + } + + private fun state(): MarketsListUM = MarketsListUM( + list = ListUM.Loading, + searchBar = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", // TODO + onQueryChange = {}, // TODO + isActive = false, // TODO + onActiveChange = { }, // TODO + ), + selectedSortBy = SortByTypeUM.Rating, + selectedInterval = MarketsListUM.TrendInterval.H24, + onIntervalClick = { selectedInterval = it }, + onSortByButtonClick = { sortByBottomSheetIsShown = true }, + sortByBottomSheet = TangemBottomSheetConfig( + isShow = false, + onDismissRequest = { sortByBottomSheetIsShown = false }, + content = SortByBottomSheetContentUM( + selectedOption = SortByTypeUM.Rating, + onOptionClicked = ::onBottomSheetOptionClicked, + ), + ), + ) + + private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) { + state.update { + it.copy( + selectedSortBy = sortByTypeUM, + sortByBottomSheet = it.sortByBottomSheet.copy( + isShow = false, + content = (it.sortByBottomSheet.content as SortByBottomSheetContentUM).copy( + selectedOption = sortByTypeUM, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt new file mode 100644 index 0000000000..53147eb848 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt @@ -0,0 +1,275 @@ +package com.tangem.features.markets.model.statemanager + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.* +import com.tangem.features.markets.model.converters.MarketsTokenItemConverter +import com.tangem.features.markets.model.utils.logAction +import com.tangem.features.markets.model.utils.logUpdateResults +import com.tangem.features.markets.ui.entity.MarketsListItemUM +import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval +import com.tangem.features.markets.ui.entity.SortByTypeUM +import com.tangem.pagination.* +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +private const val LOG_EVENTS = false + +internal class MarketsListUiItemsManager( + private val logTag: String = "main", + getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + private val currentTrendInterval: Provider, + private val currentAppCurrency: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val actionsFlow = MutableSharedFlow>() + + private val batchFlow = getMarketsTokenListFlowUseCase( + TokenListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + ) + + val uiItems: StateFlow> + get() = uiBatches + .map { batches -> + batches.asSequence() + .map { it.data } + .flatten() + .toImmutableList() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = persistentListOf(), + ) + + val onLastBatchLoadedSuccess = batchFlow.state + .distinctUntilChanged { old, new -> old.status === new.status } + .mapNotNull { + when (val status = it.status) { + is PaginationStatus.Paginating -> { + if (status.lastResult is BatchFetchResult.Success) { + it.data.lastOrNull()?.key + } else { + null + } + } + is PaginationStatus.EndOfPagination -> { + it.data.lastOrNull()?.key + } + else -> null + } + } + + private val uiBatches = MutableStateFlow>>>(emptyList()) + + init { + batchFlow.state + .map { it.data } + .distinctUntilChanged { a, b -> + a.size == b.size && a.map { it.data }.flatten() == b.map { it.data }.flatten() + } + .onEachWithPrevious { prev, list -> + updateState(prev, list) + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + + if (LOG_EVENTS) { + batchFlow.updateResults + .onEach { logUpdateResults(logTag, it) } + .launchIn(modelScope) + + actionsFlow + .onEach { logAction(logTag, it) } + .launchIn(modelScope) + } + } + + private fun updateState( + previousList: List>>?, + list: List>>, + forceUpdate: Boolean = false, + ) = uiBatches.update { items -> + val converter = MarketsTokenItemConverter(currentTrendInterval(), appCurrency = currentAppCurrency()) + + if (previousList == null || list.size < previousList.size || forceUpdate) { + list.map { + Batch( + key = it.key, + data = converter.convertList(it.data), + ) + } + } else { + if (previousList.size != list.size) { + val keysToAdd = list.map { it.key }.subtract(previousList.map { it.key }.toSet()) + val newBatches = list.filter { keysToAdd.contains(it.key) } + + items + newBatches.map { + Batch( + key = it.key, + data = converter.convertList(it.data), + ) + } + } else { + items.mapIndexed { batchIndex, batch -> + val prevBatch = previousList[batchIndex] + val newBatch = list[batchIndex] + if (previousList == newBatch) return@mapIndexed batch + + Batch( + key = batch.key, + data = batch.data.mapIndexed { index, marketsListItemUM -> + val prevItem = prevBatch.data[index] + val newItem = newBatch.data[index] + + converter.update( + prevItem, + marketsListItemUM, + newItem, + ) + }, + ) + } + } + } + } + + fun reload(interval: TrendInterval, sortBy: SortByTypeUM) { + modelScope.launch { + uiBatches.value = emptyList() + actionsFlow.emit( + BatchAction.Reload( + requestParams = TokenMarketListConfig( + fiatPriceCurrency = currentAppCurrency().code, + searchText = null, + showUnder100kMarketCapTokens = false, + priceChangeInterval = interval.toBatchRequestInterval(), + order = sortBy.toRequestOrder(), + ), + ), + ) + } + } + + fun loadMore() { + modelScope.launch { + actionsFlow.emit(BatchAction.LoadMore()) + } + } + + fun updateUIWithSameState() { + modelScope.launch(dispatchers.default) { + val current = batchFlow.state.value.data + updateState(current, current, forceUpdate = true) + } + } + + fun loadCharts(batchKeys: Set, interval: TrendInterval) { + modelScope.launch(dispatchers.default) { + val currentData = batchFlow.state.value.data + val alreadyLoadedChartsBatchKeys = currentData + .filter { + val first = it.data.firstOrNull() ?: return@filter false + val chartByInterval = when (interval) { + TrendInterval.H24 -> first.tokenCharts.h24 + TrendInterval.D7 -> first.tokenCharts.week + TrendInterval.M1 -> first.tokenCharts.month + } + chartByInterval != null + } + .map { it.key } + .toSet() + + val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys) + + if (batchesKeysToLoad.isNotEmpty()) { + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchesKeysToLoad, + updateRequest = TokenMarketUpdateRequest.UpdateChart( + interval = interval.toRequestInterval(), + currency = currentAppCurrency().code, + ), + async = true, + operationId = batchesKeysToLoad.toString() + interval.toString(), + ), + ) + } + } + } + + fun updateQuotes() { + modelScope.launch { + actionsFlow.emit( + BatchAction.CancelUpdates { + it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes + }, + ) + + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchFlow.state.value.data.map { it.key }.toSet(), + updateRequest = TokenMarketUpdateRequest.UpdateQuotes( + currencyId = currentAppCurrency().code, + ), + async = true, + operationId = "update quotes", + ), + ) + } + } + + fun getBatchKeysByItemIds(ids: List): Set { + val currentData = batchFlow.state.value.data + + return currentData + .filter { d -> d.data.any { ids.contains(it.id) } } + .map { it.key } + .toSet() + } + + private fun SortByTypeUM.toRequestOrder(): TokenMarketListConfig.Order { + return when (this) { + SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating + SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending + SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers + SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers + SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers + } + } + + private fun TrendInterval.toBatchRequestInterval(): TokenMarketListConfig.Interval { + return when (this) { + TrendInterval.H24 -> TokenMarketListConfig.Interval.H24 + TrendInterval.D7 -> TokenMarketListConfig.Interval.WEEK + TrendInterval.M1 -> TokenMarketListConfig.Interval.MONTH + } + } + + private fun TrendInterval.toRequestInterval(): PriceChangeInterval { + return when (this) { + TrendInterval.H24 -> PriceChangeInterval.H24 + TrendInterval.D7 -> PriceChangeInterval.WEEK + TrendInterval.M1 -> PriceChangeInterval.MONTH + } + } + + private fun Flow.onEachWithPrevious(operation: suspend (prev: T?, value: T) -> Unit): Flow = flow { + var prev: T? = null + collect { value -> + operation(prev, value) + prev = value + emit(value) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt new file mode 100644 index 0000000000..a7594edf81 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt @@ -0,0 +1,48 @@ +package com.tangem.features.markets.model.utils + +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.TokenMarketUpdateRequest +import com.tangem.pagination.BatchAction +import com.tangem.pagination.BatchUpdateResult +import timber.log.Timber + +internal fun logAction(tag: String, action: BatchAction) { + when (action) { + is BatchAction.Reload -> Timber.tag(tag).d( + """ + Reload = ${action.requestParams} + """.trimIndent(), + ) + is BatchAction.UpdateBatches -> Timber.tag(tag).d( + """ + To update: + keys: ${action.keys.toList()} + updateType: ${action.updateRequest.javaClass.simpleName} + """.trimIndent(), + ) + else -> Timber.tag(tag).d( + """ + $action + """.trimIndent(), + ) + } +} + +internal fun logUpdateResults( + tag: String, + updateResult: Pair>>, +) { + val sec = when (val s = updateResult.second) { + is BatchUpdateResult.Success -> "Success" + is BatchUpdateResult.Error -> s.throwable.toString() + } + + Timber.tag(tag).d( + """ + updateResults + request: ${updateResult.first} + result: $sec + """.trimIndent(), + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt index 7929779ddb..4f61361468 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt @@ -3,12 +3,17 @@ package com.tangem.features.markets.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource @@ -16,6 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition @@ -27,8 +33,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R +import com.tangem.features.markets.model.SortByBottomSheetContentUM import com.tangem.features.markets.ui.components.MarketsListItem import com.tangem.features.markets.ui.components.MarketsListItemPlaceholder +import com.tangem.features.markets.ui.components.MarketsListSortByBottomSheet import com.tangem.features.markets.ui.entity.ListUM import com.tangem.features.markets.ui.entity.MarketsListUM import com.tangem.features.markets.ui.entity.SortByTypeUM @@ -43,6 +51,8 @@ internal fun MarketsList(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, state = state, onHeaderSizeChange = onHeaderSizeChange, ) + + MarketsListSortByBottomSheet(config = state.sortByBottomSheet) } @Composable @@ -121,6 +131,7 @@ private fun Options( MarketsListUM.TrendInterval.D7, MarketsListUM.TrendInterval.M1, ), + color = TangemTheme.colors.button.secondary, initialSelectedItem = trendInterval, onClick = onIntervalClick, modifier = Modifier @@ -153,11 +164,12 @@ private fun Items(state: ListUM, modifier: Modifier = Modifier) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( - modifier = modifier, + modifier = modifier.nestedScroll(DisableParentConnection), state = lazyListState, contentPadding = PaddingValues(bottom = bottomBarHeight), userScrollEnabled = scrollEnabled, ) { + // ATTENTION! There should be no elements with a string key value except MarketsListItem! when (state) { ListUM.Loading -> { items(count = 50, key = { it }) { @@ -179,6 +191,70 @@ private fun Items(state: ListUM, modifier: Modifier = Modifier) { } } } + + LaunchedEffect(state) { + if (state is ListUM.Loading) { + lazyListState.scrollToItem(0) + } + } + + VisibleItemsTracker(lazyListState, state) + + InfiniteListHandler( + listState = lazyListState, + buffer = 50, + onLoadMore = remember(state) { + { + if (state is ListUM.Content) { + state.loadMore() + } + } + }, + ) +} + +@Composable +fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { + val visibleItems by remember { + derivedStateOf { + listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String } + } + } + + LaunchedEffect(listState.isScrollInProgress, visibleItems) { + if (state is ListUM.Content && listState.isScrollInProgress.not()) { + state.visibleIdsChanged(visibleItems) + } + } +} + +@Composable +fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) { + val loadMore by remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + + lastVisibleItemIndex > totalItemsNumber - buffer + } + } + + val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } + var emitted by remember(totalItemsCount) { mutableStateOf(false) } + + LaunchedEffect(loadMore) { + if (loadMore && !emitted) { + emitted = true + onLoadMore() + } + } +} + +private object DisableParentConnection : NestedScrollConnection { + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return available.copy(x = 0f) + } } //region: Preview @@ -196,6 +272,8 @@ private fun Preview() { item.copy(id = index.toString()) } .toImmutableList(), + loadMore = {}, + visibleIdsChanged = {}, ), searchBar = SearchBarUM( placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), @@ -208,6 +286,11 @@ private fun Preview() { selectedInterval = MarketsListUM.TrendInterval.H24, onIntervalClick = {}, onSortByButtonClick = {}, + sortByBottomSheet = TangemBottomSheetConfig( + false, + onDismissRequest = {}, + content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, + ), ), onHeaderSizeChange = {}, ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt index 5bc81ee9f6..847f063309 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListItem.kt @@ -48,9 +48,6 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.ui.entity.MarketsListItemUM import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider import com.tangem.utils.StringsSigns.MINUS -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlin.math.roundToInt import kotlin.random.Random @@ -335,22 +332,18 @@ private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceCh val color = remember { Animatable(generalColor) } - LaunchedEffect(price, growColor, fallColor, generalColor) { - snapshotFlow { price } - .drop(1) - .distinctUntilChanged() - .collectLatest { - val nextColor = when (priceChangeType) { - PriceChangeType.NEUTRAL, - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - null -> generalColor - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) + LaunchedEffect(price) { + if (priceChangeType != null) { + val nextColor = when (priceChangeType) { + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + PriceChangeType.NEUTRAL -> return@LaunchedEffect } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } } Text( @@ -369,7 +362,7 @@ private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawD Box( modifier = Modifier .padding(vertical = TangemTheme.dimens.spacing2) - .size(height = TangemTheme.dimens.size32, width = chartWidth), + .size(height = TangemTheme.dimens.size24, width = chartWidth), ) { if (chartRawData != null) { MarketChartMini( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt new file mode 100644 index 0000000000..ae2a87f4a0 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt @@ -0,0 +1,88 @@ +package com.tangem.features.markets.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.model.SortByBottomSheetContentUM +import com.tangem.features.markets.ui.entity.SortByTypeUM + +@Composable +fun MarketsListSortByBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.markets_sort_by_title), + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: SortByBottomSheetContentUM) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + SortByTypeUM.entries.forEachIndexed { index, type -> + val cornersToRound = when (index) { + 0 -> CornersToRound.TOP_2 + SortByTypeUM.entries.lastIndex -> CornersToRound.BOTTOM_2 + else -> CornersToRound.ZERO + } + + DividerContainer( + modifier = Modifier + .clip(cornersToRound.getShape()) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClicked(type) }, + showDivider = index != SortByTypeUM.entries.lastIndex, + ) { + InputRowChecked( + text = type.text, + checked = type == content.selectedOption, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 640) +@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = true, + ) { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + MarketsListSortByBottomSheet( + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = {}, + content = SortByBottomSheetContentUM( + selectedOption = SortByTypeUM.Trending, + onOptionClicked = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt index a9f12c9a19..a6f2c0c038 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt @@ -28,6 +28,6 @@ data class MarketsListItemUM( @Immutable data class Price( val text: String, - val changeType: PriceChangeType = PriceChangeType.NEUTRAL, + val changeType: PriceChangeType? = null, ) } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt index b431e9e54c..602191058c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.markets.ui.entity import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -11,6 +12,7 @@ internal data class MarketsListUM( val list: ListUM, val searchBar: SearchBarUM, val selectedSortBy: SortByTypeUM, + val sortByBottomSheet: TangemBottomSheetConfig, val selectedInterval: TrendInterval, val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, @@ -35,6 +37,8 @@ sealed class ListUM { data class Content( val items: ImmutableList, + val loadMore: () -> Unit, + val visibleIdsChanged: (List) -> Unit, ) : ListUM() data object Loading : ListUM() From 154de76c6d27d090e6ae67cfb5e6bd087e7bdb8c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 17:49:15 +0400 Subject: [PATCH 152/223] Updated on 2026-08-14 --- .../tap/common/redux/global/GlobalAction.kt | 2 - .../tangem/tap/features/demo/DemoHelper.kt | 2 - .../features/details/redux/DetailsAction.kt | 40 ---- .../details/redux/DetailsMiddleware.kt | 221 +----------------- .../features/details/redux/DetailsReducer.kt | 220 ----------------- .../features/details/redux/DetailsState.kt | 53 +---- .../ui/appsettings/AppSettingsFragment.kt | 3 - .../ui/cardsettings/CardSettingsFragment.kt | 10 +- .../cardsettings/CardSettingsScreenState.kt | 10 +- .../ui/cardsettings/CardSettingsViewModel.kt | 218 ++++++++++------- .../AccessCodeRecoveryFragment.kt | 38 +-- .../coderecovery/AccessCodeRecoveryScreen.kt | 8 +- .../AccessCodeRecoveryScreenState.kt | 2 +- .../AccessCodeRecoveryViewModel.kt | 105 +++++++-- .../ui/common/utils/AccessCodeRecovery.kt | 13 ++ .../details/ui/common/utils/ResetToFactory.kt | 14 ++ .../ui/common/utils/SecurityOptionsUtils.kt | 44 ++++ .../details/ui/details/DetailsViewModel.kt | 14 +- .../details/ui/resetcard/ResetCardFragment.kt | 29 +-- .../details/ui/resetcard/ResetCardScreen.kt | 25 +- .../ui/resetcard/ResetCardScreenState.kt | 98 ++++---- .../ui/resetcard/ResetCardViewModel.kt | 167 ++++++++----- .../ui/resetcard/di/ResetCardModule.kt | 21 -- .../DefaultResetCardFeatureToggles.kt | 11 - .../featuretoggles/ResetCardFeatureToggles.kt | 6 - .../ui/securitymode/SecurityModeFragment.kt | 37 +-- .../ui/securitymode/SecurityModeViewModel.kt | 120 ++++++++-- .../tap/features/details/ui/utils/Mappers.kt | 13 -- .../features/welcome/ui/WelcomeViewModel.kt | 2 +- .../com/tangem/common/routing/AppRoute.kt | 44 +++- .../configs/feature_toggles_config.json | 4 - .../domain/common/TangemCardTypesResolver.kt | 8 +- .../common/util/ScanResponseExtensions.kt | 4 + .../wallet-settings/impl/build.gradle.kts | 1 + .../preview/PreviewWalletSettingsComponent.kt | 5 +- .../model/WalletSettingsModel.kt | 5 +- .../walletsettings/utils/ItemsBuilder.kt | 67 +++--- 37 files changed, 695 insertions(+), 989 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/common/utils/AccessCodeRecovery.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index ba7a0ac095..e7fba06fb7 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -16,7 +16,6 @@ import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Action sealed class GlobalAction : Action { @@ -75,7 +74,6 @@ sealed class GlobalAction : Action { ) : GlobalAction() data class HideWarningMessage(val warning: WarningMessage) : GlobalAction() - data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction() data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() 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 7dc282cacd..c8576de418 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 @@ -4,7 +4,6 @@ import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.store @@ -21,7 +20,6 @@ object DemoHelper { private val disabledActionFeatures = listOf( WalletConnectAction.StartWalletConnect::class.java, BackupAction.StartBackup::class.java, - DetailsAction.ResetToFactory.Start::class.java, ) fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index b48655b7c7..10bb1b383d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -14,26 +14,6 @@ sealed class DetailsAction : Action { val shouldSaveUserWallets: Boolean, ) : DetailsAction() - data object ReCreateTwinsWallet : DetailsAction() - - sealed class ResetToFactory : DetailsAction() { - data object Start : ResetToFactory() - data object Proceed : ResetToFactory() - data class AcceptCondition1(val accepted: Boolean) : ResetToFactory() - data class AcceptCondition2(val accepted: Boolean) : ResetToFactory() - data object Failure : ResetToFactory() - data object Success : ResetToFactory() - - data class ShowDialog(val dialog: CardSettingsState.Dialog) : ResetToFactory() - - data object DismissDialog : ResetToFactory() - } - - data object ScanCard : DetailsAction() - - data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction() - - data object ResetCardSettingsData : DetailsAction() data object ScanAndSaveUserWallet : DetailsAction() { data object Success : DetailsAction() @@ -43,26 +23,6 @@ sealed class DetailsAction : Action { data object DismissError : DetailsAction() - sealed class AccessCodeRecovery : DetailsAction() { - object Open : AccessCodeRecovery() - data class SaveChanges(val enabled: Boolean) : AccessCodeRecovery() { - data class Success(val enabled: Boolean) : AccessCodeRecovery() - } - - data class SelectOption(val enabled: Boolean) : AccessCodeRecovery() - } - - sealed class ManageSecurity : DetailsAction() { - data object OpenSecurity : ManageSecurity() - data class SelectOption(val option: SecurityOption) : ManageSecurity() - data object SaveChanges : ManageSecurity() { - data object Success : ManageSecurity() - data object Failure : ManageSecurity() - } - - data object ChangeAccessCode : ManageSecurity() - } - sealed class AppSettings : DetailsAction() { data class SwitchPrivacySetting( val enable: Boolean, 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 bde329b131..b544bd8d3d 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 @@ -1,55 +1,47 @@ package com.tangem.tap.features.details.redux import androidx.lifecycle.LifecycleCoroutineScope -import com.tangem.common.* +import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError -import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics - import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder -import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.inject +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.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.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn -import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.rekotlin.Action import org.rekotlin.Middleware import timber.log.Timber import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam class DetailsMiddleware { - private val eraseWalletMiddleware = EraseWalletMiddleware() - private val manageSecurityMiddleware = ManageSecurityMiddleware() private val appSettingsMiddleware = AppSettingsMiddleware() - private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware() val detailsMiddleware: Middleware = { _, stateProvider -> { next -> { action -> @@ -66,153 +58,11 @@ class DetailsMiddleware { private fun handleAction(state: DetailsState, action: Action) { when (action) { - is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) - is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state) is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) - is DetailsAction.ReCreateTwinsWallet -> { - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - - store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } - } - is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) - is DetailsAction.ScanCard -> scanCard(state) is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet() } } - class EraseWalletMiddleware { - @Suppress("CyclomaticComplexMethod") - fun handle(action: DetailsAction.ResetToFactory) { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - when (action) { - is DetailsAction.ResetToFactory.Start -> { - val card = store.state.detailsState.cardSettingsState?.card ?: return - if (card.isTangemTwins) { - store.dispatch(DetailsAction.ReCreateTwinsWallet) - return - } else { - store.dispatchNavigationAction { push(AppRoute.ResetToFactory) } - } - } - is DetailsAction.ResetToFactory.Proceed -> { - val card = store.state.detailsState.cardSettingsState?.card ?: return - scope.launch { - val userWalletId = UserWalletIdBuilder.card(card).build() - - // we must require a password regardless of biometric settings - val policy = tangemSdkManager.userCodeRequestPolicy - val doBeforeErase = { - val type = if (card.isAccessCodeSet) { - UserCodeType.AccessCode - } else if (card.isPasscodeSet == true) { - UserCodeType.Passcode - } else { - null - } - - type?.let { - tangemSdkManager.setUserCodeRequestPolicy(UserCodeRequestPolicy.Always(type)) - } - } - - val doAfterErase = { - tangemSdkManager.setUserCodeRequestPolicy(policy) - } - - doBeforeErase() - tangemSdkManager.resetToFactorySettings(card.cardId, true) - .flatMap { - userWalletsListManager.delete(listOfNotNull(userWalletId)) - } - .flatMap { - tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId)) - } - .doOnSuccess { - Analytics.send(Settings.CardSettings.FactoryResetFinished()) - - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync - if (selectedUserWallet != null) { - store.dispatchNavigationAction { popTo() } - store.onUserWalletSelected(selectedUserWallet) - } else { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } - .fold(onSuccess = { true }, onFailure = { false }) - if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } - } else { - store.dispatchNavigationAction { replaceAll(AppRoute.Home) } - } - } - } - .doOnFailure { error -> - if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error)) - } - } - .doOnResult { - doAfterErase() - } - } - } - else -> Unit - } - } - } - - class ManageSecurityMiddleware { - @Suppress("ComplexMethod") - fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) { - when (action) { - is DetailsAction.ManageSecurity.OpenSecurity -> { - store.dispatchNavigationAction { push(AppRoute.DetailsSecurity) } - } - is DetailsAction.ManageSecurity.SaveChanges -> { - val cardSettingsState = detailsState.cardSettingsState - val cardId = cardSettingsState?.card?.cardId - val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption - scope.launch { - val result = when (selectedOption) { - SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId) - SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId) - SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId) - else -> return@launch - } - withContext(Dispatchers.Main) { - val paramValue = AnalyticsParam.SecurityMode.from(selectedOption) - when (result) { - is CompletionResult.Success -> { - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) - store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption)) - store.dispatchNavigationAction(AppRouter::pop) - store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success) - } - is CompletionResult.Failure -> { - val error = result.error - if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error)) - } - store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure) - } - else -> Unit - } - } - } - } - is DetailsAction.ManageSecurity.ChangeAccessCode -> { - val card = store.state.detailsState.cardSettingsState?.card ?: return - scope.launch { - when (tangemSdkManager.setAccessCode(card.cardId)) { - is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) - is CompletionResult.Failure -> {} - } - } - } - else -> Unit - } - } - } - class AppSettingsMiddleware { private val checkBiometricsStatusJobHolder = JobHolder() @@ -429,63 +279,6 @@ class DetailsMiddleware { } } - class AccessCodeRecoveryMiddleware { - fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) { - when (action) { - is DetailsAction.AccessCodeRecovery.Open -> { - Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton()) - store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) } - } - is DetailsAction.AccessCodeRecovery.SaveChanges -> { - scope.launch { - tangemSdkManager - .setAccessCodeRecoveryEnabled(state.cardSettingsState?.card?.cardId, action.enabled) - .doOnSuccess { - Analytics.send( - Settings.CardSettings.AccessCodeRecoveryChanged( - AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled), - ), - ) - store.dispatchNavigationAction(AppRouter::pop) - store.dispatchOnMain( - DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled), - ) - } - } - } - is DetailsAction.AccessCodeRecovery.SelectOption -> Unit - is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> Unit - } - } - } - - private fun scanCard(state: DetailsState) = scope.launch { - store.inject(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)) - val currentUserWalletId = state.scanResponse - ?.let { UserWalletIdBuilder.scanResponse(it).build() } - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) - .build() - val isSameWallet = currentUserWalletId == scannedUserWalletId - - if (isSameWallet) { - store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse), - ) - } else { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.error_wrong_wallet_tapped, - ), - ) - } - } - } - private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) { val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 837fd35328..564603a0b2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -1,21 +1,15 @@ package com.tangem.tap.features.details.redux -import com.tangem.common.routing.AppRoute import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.common.CardTypesResolver -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.common.extensions.inject import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.runBlocking import org.rekotlin.Action -import java.util.EnumSet object DetailsReducer { fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) @@ -29,16 +23,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { is DetailsAction.PrepareScreen -> { handlePrepareScreen(action, state) } - is DetailsAction.PrepareCardSettingsData -> { - handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState) - } - is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null) - is DetailsAction.ResetToFactory -> { - handleEraseWallet(action, detailsState) - } - is DetailsAction.ManageSecurity -> { - handleSecurityAction(action, detailsState) - } is DetailsAction.AppSettings -> { handlePrivacyAction(action, detailsState) } @@ -47,7 +31,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { selectedAppCurrency = action.currency, ), ) - is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy( isScanningInProgress = true, ) @@ -61,21 +44,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { is DetailsAction.DismissError -> detailsState.copy( error = null, ) - else -> detailsState } } private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState { - val router = store.inject(DaggerGraphState::appRouter) - return DetailsState( scanResponse = action.scanResponse, - // If the current screen is ResetToFactory, we must save the ResetToFactory's state - cardSettingsState = if (router.stack.lastOrNull() is AppRoute.ResetToFactory) { - state.detailsState.cardSettingsState - } else { - null - }, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, appSettingsState = AppSettingsState( isBiometricsAvailable = tangemSdkManager.canUseBiometry, @@ -96,139 +70,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS ) } -private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState { - val cardTypesResolver = scanResponse.cardTypesResolver - val card = scanResponse.card - val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() - val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active - - val cardSettingsState = CardSettingsState( - cardInfo = card.toCardInfo(cardTypesResolver), - scanResponse = scanResponse, - manageSecurityState = prepareSecurityOptions(card, cardTypesResolver), - card = scanResponse.card, - resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver), - resetButtonEnabled = false, - condition1Checked = false, - condition2Checked = false, - accessCodeRecovery = if (cardTypesResolver.isWallet2()) { - val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false - AccessCodeRecoveryState( - enabledOnCard = enabled, - enabledSelection = enabled, - ) - } else { - null - }, - isShowPasswordResetRadioButton = isShowPasswordResetRadioButton, - dialog = null, - ) - - return state.copy(cardSettingsState = cardSettingsState) -} - -private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesResolver): ManageSecurityState { - val securityOption = when { - card.isAccessCodeSet -> { - SecurityOption.AccessCode - } - - card.isPasscodeSet == true -> { - SecurityOption.PassCode - } - - else -> { - SecurityOption.LongTap - } - } - val allowedSecurityOptions = when { - cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> { - EnumSet.of(SecurityOption.LongTap) - } - card.settings.isBackupAllowed -> { - EnumSet.of(securityOption) - } - else -> - prepareAllowedSecurityOptions(cardTypesResolver = cardTypesResolver, currentSecurityOption = securityOption) - } - return ManageSecurityState( - currentOption = securityOption, - allowedOptions = allowedSecurityOptions, - selectedOption = securityOption, - ) -} - -private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean { - val hasPermanentWallet = card.wallets.any { it.settings.isPermanent } - val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin() - return !isNotAllowed -} - -private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState { - val cardSettingsState = state.cardSettingsState - return when (action) { - is DetailsAction.ResetToFactory.AcceptCondition1 -> { - val warning1Checked = action.accepted - val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) { - warning1Checked && cardSettingsState.condition2Checked - } else { - warning1Checked - } - state.copy( - cardSettingsState = cardSettingsState?.copy( - condition1Checked = action.accepted, - resetButtonEnabled = resetButtonEnabled, - ), - ) - } - is DetailsAction.ResetToFactory.AcceptCondition2 -> { - val warning2Checked = action.accepted - state.copy( - cardSettingsState = cardSettingsState?.copy( - condition2Checked = action.accepted, - resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked, - ), - ) - } - is DetailsAction.ResetToFactory.ShowDialog -> { - state.copy(cardSettingsState = cardSettingsState?.copy(dialog = action.dialog)) - } - is DetailsAction.ResetToFactory.DismissDialog -> { - state.copy(cardSettingsState = cardSettingsState?.copy(dialog = null)) - } - else -> state - } -} - -private fun handleSecurityAction(action: DetailsAction.ManageSecurity, state: DetailsState): DetailsState { - return when (action) { - is DetailsAction.ManageSecurity.SelectOption -> { - val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy( - selectedOption = action.option, - ) - state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState)) - } - is DetailsAction.ManageSecurity.SaveChanges.Success -> { - // Setting options to show only LongTap from now on for non-twins - val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy( - currentOption = state.cardSettingsState.manageSecurityState.selectedOption, - allowedOptions = state.scanResponse?.cardTypesResolver?.let { - prepareAllowedSecurityOptions( - cardTypesResolver = it, - currentSecurityOption = state.cardSettingsState.manageSecurityState.selectedOption, - ) - } ?: EnumSet.of(SecurityOption.LongTap), - ) - state.copy( - cardSettingsState = state.cardSettingsState?.copy( - manageSecurityState = manageSecurityState, - ), - ) - } - else -> state - } -} - private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { return when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( @@ -288,65 +129,4 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail is DetailsAction.AppSettings.CheckBiometricsStatus, -> state } -} - -private fun handleAccessCodeRecoveryAction( - action: DetailsAction.AccessCodeRecovery, - state: DetailsState, -): DetailsState { - return when (action) { - DetailsAction.AccessCodeRecovery.Open -> { - val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy( - enabledSelection = state.cardSettingsState.accessCodeRecovery.enabledOnCard, - ) - state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery)) - } - is DetailsAction.AccessCodeRecovery.SaveChanges -> state - is DetailsAction.AccessCodeRecovery.SelectOption -> { - val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy( - enabledSelection = action.enabled, - ) - state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery)) - } - is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> { - val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy( - enabledOnCard = action.enabled, - enabledSelection = action.enabled, - ) - state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery)) - } - } -} - -private fun prepareAllowedSecurityOptions( - cardTypesResolver: CardTypesResolver, - currentSecurityOption: SecurityOption?, -): EnumSet { - val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap) - - if (cardTypesResolver.isTangemTwins()) { - allowedSecurityOptions.add(SecurityOption.PassCode) - } - if (currentSecurityOption == SecurityOption.AccessCode) { - allowedSecurityOptions.add(SecurityOption.AccessCode) - } - if (currentSecurityOption == SecurityOption.PassCode) { - allowedSecurityOptions.add(SecurityOption.PassCode) - } - return allowedSecurityOptions -} - -@Suppress("MagicNumber") -private fun CardDTO.toCardInfo(cardTypesResolver: CardTypesResolver): CardInfo { - val cardId = this.cardId.chunked(4).joinToString(separator = " ") - val issuer = this.issuer.name - val signedHashes = this.signedHashesCount() - - return CardInfo( - cardId = cardId, - issuer = issuer, - signedHashes = signedHashes, - isTwin = cardTypesResolver.isTangemTwins(), - hasBackup = backupStatus?.isActive == true, - ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 8e7e583ecf..c35d0c8d83 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -3,15 +3,11 @@ package com.tangem.tap.features.details.redux import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.entities.Button import org.rekotlin.StateType -import java.util.EnumSet data class DetailsState( val scanResponse: ScanResponse? = null, - val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, val isScanningInProgress: Boolean = false, @@ -19,52 +15,13 @@ data class DetailsState( val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType -data class CardInfo( - val cardId: String, - val issuer: String, - val signedHashes: Int, - val isTwin: Boolean, - val hasBackup: Boolean, -) - -/** - * @property enabledOnCard whether access code recovery is enabled on card - * @property enabledSelection current selected option in app (not saved on card yet) - */ -data class AccessCodeRecoveryState( - val enabledOnCard: Boolean, - val enabledSelection: Boolean, -) - -data class CardSettingsState( - val cardInfo: CardInfo, - val card: CardDTO, - val scanResponse: ScanResponse, - val manageSecurityState: ManageSecurityState?, - val resetCardAllowed: Boolean, - val resetButtonEnabled: Boolean, - val condition1Checked: Boolean, - val condition2Checked: Boolean, - val accessCodeRecovery: AccessCodeRecoveryState? = null, - val isShowPasswordResetRadioButton: Boolean, - val dialog: Dialog?, -) { - - sealed interface Dialog { - data object StartResetDialog : Dialog - data object ContinueResetDialog : Dialog - data object InterruptedResetDialog : Dialog - data object CompletedResetDialog : Dialog - } +sealed class ResetCardDialog { + data object StartResetDialog : ResetCardDialog() + data object ContinueResetDialog : ResetCardDialog() + data object InterruptedResetDialog : ResetCardDialog() + data object CompletedResetDialog : ResetCardDialog() } -data class ManageSecurityState( - val currentOption: SecurityOption = SecurityOption.LongTap, - val selectedOption: SecurityOption = currentOption, - val allowedOptions: EnumSet = EnumSet.allOf(SecurityOption::class.java), - val buttonProceed: Button = Button(true), -) - data class AppSettingsState( val saveWallets: Boolean = false, val saveAccessCodes: Boolean = false, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 2d6e9d554c..9036d7d1fd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -7,12 +7,10 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.routing.AppRouter - import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -37,7 +35,6 @@ internal class AppSettingsFragment : ComposeFragment() { modifier = modifier, state = state, onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) store.dispatchNavigationAction(AppRouter::pop) }, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 6fc835cc9c..6054581056 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -1,15 +1,14 @@ package com.tangem.tap.features.details.ui.cardsettings import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.routing.AppRouter - import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -24,13 +23,12 @@ internal class CardSettingsFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) + val state by viewModel.screenState.collectAsStateWithLifecycle() CardSettingsScreen( modifier = modifier, - state = viewModel.screenState.value, + state = state, onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) store.dispatchNavigationAction(AppRouter::pop) }, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index 4cf5e68186..46425706b4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -4,16 +4,12 @@ import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.res.stringResource -import com.tangem.tap.features.details.redux.AccessCodeRecoveryState import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.securitymode.toTitleRes -import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import com.tangem.wallet.R -import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo internal data class CardSettingsScreenState( val cardDetails: List? = null, - val accessCodeRecoveryState: AccessCodeRecoveryState? = null, val onScanCardClick: () -> Unit, val onElementClick: (CardInfo) -> Unit, ) @@ -44,7 +40,7 @@ internal sealed class CardInfo( clickable = clickable, ) - object ChangeAccessCode : CardInfo( + data object ChangeAccessCode : CardInfo( titleRes = TextReference.Res(R.string.card_settings_change_access_code), subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer), clickable = true, @@ -60,9 +56,9 @@ internal sealed class CardInfo( clickable = true, ) - class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo( + class ResetToFactorySettings(description: TextReference) : CardInfo( titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory), - subtitle = cardInfo.toResetCardDescriptionText(), + subtitle = description, clickable = true, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 64c00b50f4..94be2ca2fd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -1,105 +1,124 @@ package com.tangem.tap.features.details.ui.cardsettings -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import arrow.core.Either +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.CompletionResult +import com.tangem.common.doOnSuccess +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.TapWorkarounds.isTangemTwins -import com.tangem.domain.common.getTwinCardIdForUser -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.features.details.redux.CardSettingsState -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.domain.extensions.signedHashesCount +import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.features.details.ui.common.utils.* import com.tangem.tap.store +import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel -import org.rekotlin.StoreSubscriber +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @HiltViewModel internal class CardSettingsViewModel @Inject constructor( - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, -) : - ViewModel(), DefaultLifecycleObserver, StoreSubscriber { + private val scanCardProcessor: ScanCardProcessor, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val tangemSdkManager: TangemSdkManager, + savedStateHandle: SavedStateHandle, +) : ViewModel() { - var screenState: MutableState = - mutableStateOf(updateState(store.state.detailsState.cardSettingsState)) + private val userWalletId = savedStateHandle.get(AppRoute.CardSettings.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("User wallet ID is required for CardSettingsViewModel") - override fun onStart(owner: LifecycleOwner) { - when (val selectedWalletEither = getSelectedWalletSyncUseCase()) { - is Either.Left -> { - Timber.e(selectedWalletEither.value.toString()) + val screenState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun getInitialState() = CardSettingsScreenState( + cardDetails = null, + onElementClick = ::handleClickingItem, + onScanCardClick = ::scanCard, + ) + + private fun scanCard() = viewModelScope.launch { + scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { scanResponse -> + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + val isCorrectUserWalletScanned = scannedUserWalletId == userWalletId + + if (isCorrectUserWalletScanned) { + val userWallet = getUserWallet() + + updateCardDetails(userWallet) + } else { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = R.string.error_wrong_wallet_tapped, + ), + ) + } + } + } + + private fun updateCardDetails(userWallet: UserWallet) { + val card = userWallet.scanResponse.card + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + val cardId = cardTypesResolver.getCardId() + + val currentSecurityOption = getCurrentSecurityOption(card) + val allowedSecurityOptions = getAllowedSecurityOptions( + card = card, + cardTypesResolver = cardTypesResolver, + currentSecurityOption = currentSecurityOption, + ) + val isResetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver) + + val cardDetails = buildList { + CardInfo.CardId(cardId).let(::add) + CardInfo.Issuer(card.issuer.name).let(::add) + + if (!cardTypesResolver.isTangemTwins()) { + CardInfo.SignedHashes(card.signedHashesCount().toString()).let(::add) + } + + CardInfo.SecurityMode( + currentSecurityOption, + clickable = allowedSecurityOptions.size > 1, + ).let(::add) + + if (card.backupStatus?.isActive == true && card.isAccessCodeSet) { + CardInfo.ChangeAccessCode.let(::add) + } + + if (isAccessCodeRecoveryAllowed(cardTypesResolver)) { + CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)).let(::add) + } + + if (isResetCardAllowed) { + CardInfo.ResetToFactorySettings( + description = getResetToFactoryDescription(card, cardTypesResolver), + ).let(::add) } - is Either.Right -> Unit } - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop(owner: LifecycleOwner) { - store.unsubscribe(this) - } - - override fun newState(state: DetailsState) { - screenState.value = updateState(state.cardSettingsState) - } - - private fun updateState(state: CardSettingsState?): CardSettingsScreenState { - return if (state?.manageSecurityState == null) { - CardSettingsScreenState( - cardDetails = null, - accessCodeRecoveryState = null, - onElementClick = {}, - onScanCardClick = { - store.dispatch(DetailsAction.ScanCard) - }, - ) - } else { - val cardId = if (state.card.isTangemTwins) { - state.card.getTwinCardIdForUser() - } else { - state.cardInfo.cardId - } - val cardDetails: MutableList = mutableListOf( - CardInfo.CardId(cardId), - CardInfo.Issuer(state.cardInfo.issuer), - ) - - if (!state.card.isTangemTwins) { - cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString())) - } - cardDetails.add( - CardInfo.SecurityMode( - securityOption = state.manageSecurityState.currentOption, - clickable = state.manageSecurityState.allowedOptions.size > 1, - ), - ) - if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) { - cardDetails.add(CardInfo.ChangeAccessCode) - } - if (state.accessCodeRecovery != null) { - cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard)) - } - if (state.resetCardAllowed) { - cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo)) - } - CardSettingsScreenState( - cardDetails = cardDetails, - accessCodeRecoveryState = state.accessCodeRecovery, - onScanCardClick = { }, - onElementClick = { - handleClickingItem(it) - }, - ) + screenState.update { state -> + state.copy(cardDetails = cardDetails) } } @@ -107,20 +126,43 @@ internal class CardSettingsViewModel @Inject constructor( when (item) { is CardInfo.ChangeAccessCode -> { Analytics.send(Settings.CardSettings.ButtonChangeUserCode(AnalyticsParam.UserCode.AccessCode)) - store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode) + changeAccessCode() } is CardInfo.ResetToFactorySettings -> { Analytics.send(Settings.CardSettings.ButtonFactoryReset()) - store.dispatch(DetailsAction.ResetToFactory.Start) + store.dispatchNavigationAction { AppRoute.ResetToFactory(userWalletId) } } is CardInfo.SecurityMode -> { Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode()) - store.dispatch(DetailsAction.ManageSecurity.OpenSecurity) + store.dispatchNavigationAction { AppRoute.DetailsSecurity(userWalletId) } } is CardInfo.AccessCodeRecovery -> { - store.dispatch(DetailsAction.AccessCodeRecovery.Open) + store.dispatchNavigationAction { AppRoute.AccessCodeRecovery(userWalletId) } } else -> {} } } + + private fun changeAccessCode() = viewModelScope.launch { + val card = getUserWallet().scanResponse.card + + when (val result = tangemSdkManager.setAccessCode(card.cardId)) { + is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) + is CompletionResult.Failure -> { + Timber.e("Failed to change access code: ${result.error}") + } + } + } + + private fun getUserWallet(): UserWallet { + return getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet $userWalletId: $it") + } + } + + private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean { + val hasPermanentWallet = card.wallets.any { it.settings.isPermanent } + val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin() + return !isNotAllowed + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt index 3beea0503a..bcfe7cd28d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt @@ -1,55 +1,33 @@ package com.tangem.tap.features.details.ui.cardsettings.coderecovery import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint -import org.rekotlin.StoreSubscriber import javax.inject.Inject @AndroidEntryPoint -class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber { +class AccessCodeRecoveryFragment : ComposeFragment() { - private val viewModel = AccessCodeRecoveryViewModel(store) - - private var screenState: MutableState = - mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)) + private val viewModel: AccessCodeRecoveryViewModel by viewModels() @Inject override lateinit var uiDependencies: UiDependencies @Composable override fun ScreenContent(modifier: Modifier) { + val state by viewModel.screenState.collectAsStateWithLifecycle() + AccessCodeRecoveryScreen( - state = screenState.value, + state = state, onBackClick = { store.dispatchNavigationAction(AppRouter::pop) }, ) } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - screenState.value = - viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt index 12edf5a47e..d8107e0c4d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt @@ -1,10 +1,6 @@ package com.tangem.tap.features.details.ui.cardsettings.coderecovery -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -57,7 +53,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) { DetailsMainButton( title = stringResource(id = R.string.common_save_changes), enabled = state.isSaveChangesEnabled, - onClick = { state.onSaveChangesClick(state.enabledSelection) }, + onClick = { state.onSaveChangesClick() }, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt index f1a825067a..b4b6c68535 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt @@ -11,6 +11,6 @@ data class AccessCodeRecoveryScreenState( val enabledOnCard: Boolean, val enabledSelection: Boolean, val isSaveChangesEnabled: Boolean, - val onSaveChangesClick: (Boolean) -> Unit, + val onSaveChangesClick: () -> Unit, val onOptionClick: (Boolean) -> Unit, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt index a2948075f8..8404af2beb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt @@ -1,30 +1,91 @@ package com.tangem.tap.features.details.ui.cardsettings.coderecovery -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.details.redux.AccessCodeRecoveryState -import com.tangem.tap.features.details.redux.DetailsAction -import org.rekotlin.Store +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.doOnSuccess +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.bundle.unbundle +import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled +import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject -class AccessCodeRecoveryViewModel(val store: Store) { +@HiltViewModel +internal class AccessCodeRecoveryViewModel @Inject constructor( + private val getUserWalletUseCase: GetUserWalletUseCase, + private val tangemSdkManager: TangemSdkManager, + savedStateHandle: SavedStateHandle, +) : ViewModel() { - fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState { - // We shouldn't get to this screen here when this state is null - return if (state == null) { - AccessCodeRecoveryScreenState( - enabledOnCard = false, - enabledSelection = false, - isSaveChangesEnabled = false, - onSaveChangesClick = {}, - onOptionClick = {}, - ) - } else { - AccessCodeRecoveryScreenState( - enabledOnCard = state.enabledOnCard, - enabledSelection = state.enabledSelection, - isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection, - onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) }, - onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) }, + private val userWalletId = savedStateHandle.get(AppRoute.AccessCodeRecovery.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("UserWalletId is required for AccessCodeRecoveryViewModel") + + val screenState = MutableStateFlow( + value = getInitialState(), + ) + + private fun getInitialState(): AccessCodeRecoveryScreenState { + val userWallet = getUserWallet() + + val isEnabled = isAccessCodeRecoveryEnabled( + typeResolver = userWallet.scanResponse.cardTypesResolver, + card = userWallet.scanResponse.card, + ) + + return AccessCodeRecoveryScreenState( + enabledOnCard = isEnabled, + enabledSelection = isEnabled, + isSaveChangesEnabled = false, + onSaveChangesClick = ::saveChanges, + onOptionClick = ::selectOption, + ) + } + + private fun saveChanges() = viewModelScope.launch { + val userWallet = getUserWallet() + val isEnabled = screenState.value.enabledSelection + + tangemSdkManager + .setAccessCodeRecoveryEnabled(userWallet.cardId, isEnabled) + .doOnSuccess { + Analytics.send( + Settings.CardSettings.AccessCodeRecoveryChanged( + AnalyticsParam.AccessCodeRecoveryStatus.from(isEnabled), + ), + ) + + store.dispatchNavigationAction(AppRouter::pop) + } + } + + private fun selectOption(isEnabled: Boolean) { + screenState.update { + it.copy( + isSaveChangesEnabled = isEnabled != it.enabledOnCard, ) } } + + private fun getUserWallet(): UserWallet { + return getUserWalletUseCase(userWalletId).getOrElse { + error("Unable to get user wallet $userWalletId: $it") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/AccessCodeRecovery.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/AccessCodeRecovery.kt new file mode 100644 index 0000000000..c08b50c4e5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/AccessCodeRecovery.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.features.details.ui.common.utils + +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.models.scan.CardDTO + +internal fun isAccessCodeRecoveryAllowed(typeResolver: CardTypesResolver): Boolean = typeResolver.isWallet2() + +internal fun isAccessCodeRecoveryEnabled(typeResolver: CardTypesResolver, card: CardDTO): Boolean = + if (typeResolver.isWallet2()) { + card.userSettings?.isUserCodeRecoveryAllowed ?: false + } else { + false + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt new file mode 100644 index 0000000000..b576e8193e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.details.ui.common.utils + +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.models.scan.CardDTO +import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.wallet.R + +internal fun getResetToFactoryDescription(card: CardDTO, typesResolver: CardTypesResolver): TextReference { + return if (card.backupStatus?.isActive != true || typesResolver.isTangemTwins()) { + TextReference.Res(R.string.reset_card_without_backup_to_factory_message) + } else { + TextReference.Res(R.string.reset_card_with_backup_to_factory_message) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt new file mode 100644 index 0000000000..0a62abe82e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt @@ -0,0 +1,44 @@ +package com.tangem.tap.features.details.ui.common.utils + +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.models.scan.CardDTO +import com.tangem.tap.features.details.redux.SecurityOption +import java.util.EnumSet + +internal fun getCurrentSecurityOption(card: CardDTO): SecurityOption = when { + card.isAccessCodeSet -> SecurityOption.AccessCode + card.isPasscodeSet == true -> SecurityOption.PassCode + else -> SecurityOption.LongTap +} + +internal fun getAllowedSecurityOptions( + card: CardDTO, + cardTypesResolver: CardTypesResolver, + currentSecurityOption: SecurityOption, +): EnumSet = when { + cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> EnumSet.of(SecurityOption.LongTap) + card.settings.isBackupAllowed -> EnumSet.of(currentSecurityOption) + else -> prepareAllowedSecurityOptions( + cardTypesResolver = cardTypesResolver, + currentSecurityOption = currentSecurityOption, + ) +} + +private fun prepareAllowedSecurityOptions( + cardTypesResolver: CardTypesResolver, + currentSecurityOption: SecurityOption?, +): EnumSet { + val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap) + + if (cardTypesResolver.isTangemTwins()) { + allowedSecurityOptions.add(SecurityOption.PassCode) + } + if (currentSecurityOption == SecurityOption.AccessCode) { + allowedSecurityOptions.add(SecurityOption.AccessCode) + } + if (currentSecurityOption == SecurityOption.PassCode) { + allowedSecurityOptions.add(SecurityOption.PassCode) + } + + return allowedSecurityOptions +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 4cc6783772..d2c84a43db 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.flow.onEach import org.rekotlin.Store import timber.log.Timber -// TODO: change to Android ViewModel [REDACTED_JIRA] internal class DetailsViewModel( private val store: Store, private val walletsRepository: WalletsRepository, @@ -90,10 +89,6 @@ internal class DetailsViewModel( SettingsItem.AppSettings(::navigateToAppSettings) .let(::add) - // removed chat in task [REDACTED_TASK_KEY] - // SettingsItem.Chat(::navigateToChat) - // .let(::add) - SettingsItem.SendFeedback(::sendFeedback) .let(::add) @@ -137,7 +132,10 @@ internal class DetailsViewModel( } private fun navigateToReferralProgram() { - store.dispatchNavigationAction { push(AppRoute.ReferralProgram) } + val userWallet = userWalletsListManager.selectedUserWalletSync + ?: error("Selected wallet must be not null") + + store.dispatchNavigationAction { push(AppRoute.ReferralProgram(userWallet.walletId)) } } private fun sendFeedback() { @@ -157,8 +155,10 @@ internal class DetailsViewModel( } private fun navigateToCardSettings() { + val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId + ?: error("UserWalletId must be not null") Analytics.send(Settings.ButtonCardSettings()) - store.dispatchNavigationAction { push(AppRoute.CardSettings) } + store.dispatchNavigationAction { push(AppRoute.CardSettings(userWalletId)) } } private fun linkMoreCards() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index a17e32ffd9..041a91ee08 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.ui.resetcard import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -8,26 +9,21 @@ import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.MutableStateFlow -import org.rekotlin.StoreSubscriber import javax.inject.Inject @AndroidEntryPoint -internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { +internal class ResetCardFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies private val viewModel: ResetCardViewModel by viewModels() - private var screenState = MutableStateFlow(ResetCardScreenState.InitialState) - @Composable override fun ScreenContent(modifier: Modifier) { - val state = screenState.collectAsStateWithLifecycle().value + val state by viewModel.screenState.collectAsStateWithLifecycle() ResetCardScreen( state = state, @@ -35,23 +31,4 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - screenState.value = viewModel.updateState(state.cardSettingsState) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 85bec44728..1b52a50fd1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -20,24 +20,19 @@ import com.tangem.tap.features.details.ui.cardsettings.resolveReference import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R -import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.ResetCardScreenContent.Dialog as ResetCardDialog +import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog @Composable internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( modifier = modifier, content = { - when (state) { - is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state) - ResetCardScreenState.InitialState -> { - // do nothing for now, just white screen - } - } + ResetCardView(state = state) }, onBackClick = onBackClick, ) - when (val dialog = (state as? ResetCardScreenState.ResetCardScreenContent)?.dialog) { + when (val dialog = state.dialog) { is ResetCardDialog.StartReset, is ResetCardDialog.ContinueReset, is ResetCardDialog.InterruptedReset, @@ -48,7 +43,7 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni } @Composable -private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) { +private fun ResetCardView(state: ResetCardScreenState) { val scrollState = rememberScrollState() Column( @@ -110,7 +105,7 @@ private fun Description(text: TextReference) { } @Composable -private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) { +private fun Conditions(state: ResetCardScreenState) { state.warningsToShow.forEach { when (it) { ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> { @@ -120,7 +115,6 @@ private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) { description = TextReference.Res(R.string.reset_card_to_factory_condition_1), ) } - ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> { ConditionCheckBox( checkedState = state.acceptCondition2Checked, @@ -192,7 +186,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { } @Composable -private fun CommonResetDialog(dialog: ResetCardScreenState.ResetCardScreenContent.Dialog) { +private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) { BasicDialog( title = stringResource(dialog.titleResId), message = stringResource(dialog.messageResId), @@ -231,10 +225,13 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { .background(TangemTheme.colors.background.secondary), ) { ResetCardScreen( - state = ResetCardScreenState.ResetCardScreenContent( - accepted = true, + state = ResetCardScreenState( + resetButtonEnabled = true, + showResetPasswordButton = false, warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS), descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message), + acceptCondition1Checked = false, + acceptCondition2Checked = false, onAcceptCondition1ToggleClick = {}, onAcceptCondition2ToggleClick = {}, onResetButtonClick = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index 0bcff503bc..27dacf387f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -4,63 +4,57 @@ import androidx.annotation.StringRes import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.wallet.R -internal sealed class ResetCardScreenState { +internal data class ResetCardScreenState( + val resetButtonEnabled: Boolean, + val descriptionText: TextReference, + val warningsToShow: List, + val showResetPasswordButton: Boolean, + val acceptCondition1Checked: Boolean, + val acceptCondition2Checked: Boolean, + val onAcceptCondition1ToggleClick: (Boolean) -> Unit, + val onAcceptCondition2ToggleClick: (Boolean) -> Unit, + val onResetButtonClick: () -> Unit, + val dialog: Dialog?, +) { - object InitialState : ResetCardScreenState() + sealed class Dialog( + @StringRes val titleResId: Int, + @StringRes val messageResId: Int, + ) { - data class ResetCardScreenContent( - val accepted: Boolean = false, - val descriptionText: TextReference, - val warningsToShow: List, - val acceptCondition1Checked: Boolean = false, - val acceptCondition2Checked: Boolean = false, - val onAcceptCondition1ToggleClick: (Boolean) -> Unit, - val onAcceptCondition2ToggleClick: (Boolean) -> Unit, - val onResetButtonClick: () -> Unit, - val dialog: Dialog? = null, - ) : ResetCardScreenState() { - val resetButtonEnabled: Boolean - get() = accepted + abstract val onConfirmClick: () -> Unit + abstract val onDismiss: () -> Unit - sealed class Dialog( - @StringRes val titleResId: Int, - @StringRes val messageResId: Int, + data class StartReset( + override val onConfirmClick: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog( + titleResId = R.string.common_attention, + messageResId = R.string.card_settings_action_sheet_title, + ) + + data class ContinueReset( + override val onConfirmClick: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog( + titleResId = R.string.card_settings_continue_reset_alert_title, + messageResId = R.string.card_settings_continue_reset_alert_message, + ) + + data class InterruptedReset( + override val onConfirmClick: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog( + titleResId = R.string.card_settings_interrupted_reset_alert_title, + messageResId = R.string.card_settings_interrupted_reset_alert_message, + ) + + data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog( + titleResId = R.string.card_settings_completed_reset_alert_title, + messageResId = R.string.card_settings_completed_reset_alert_message, ) { - abstract val onConfirmClick: () -> Unit - abstract val onDismiss: () -> Unit - - data class StartReset( - override val onConfirmClick: () -> Unit, - override val onDismiss: () -> Unit, - ) : Dialog( - titleResId = R.string.common_attention, - messageResId = R.string.card_settings_action_sheet_title, - ) - - data class ContinueReset( - override val onConfirmClick: () -> Unit, - override val onDismiss: () -> Unit, - ) : Dialog( - titleResId = R.string.card_settings_continue_reset_alert_title, - messageResId = R.string.card_settings_continue_reset_alert_message, - ) - - data class InterruptedReset( - override val onConfirmClick: () -> Unit, - override val onDismiss: () -> Unit, - ) : Dialog( - titleResId = R.string.card_settings_interrupted_reset_alert_title, - messageResId = R.string.card_settings_interrupted_reset_alert_message, - ) - - data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog( - titleResId = R.string.card_settings_completed_reset_alert_title, - messageResId = R.string.card_settings_completed_reset_alert_message, - ) { - - override val onDismiss: () -> Unit = {} - } + override val onDismiss: () -> Unit = {} } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 491f7dfc81..d578f0875f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -1,8 +1,12 @@ package com.tangem.tap.features.details.ui.resetcard +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.DeleteSavedAccessCodesUseCase @@ -10,96 +14,132 @@ import com.tangem.domain.card.ResetCardUseCase 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.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.features.details.redux.CardSettingsState -import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles -import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText +import com.tangem.tap.features.details.redux.ResetCardDialog +import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription import com.tangem.tap.store import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject -import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog @Suppress("LongParameterList") @HiltViewModel internal class ResetCardViewModel @Inject constructor( - private val resetCardFeatureToggles: ResetCardFeatureToggles, + private val getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val resetCardUseCase: ResetCardUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, private val userWalletsListManager: UserWalletsListManager, private val analyticsEventHandler: AnalyticsEventHandler, + savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse - ?: error("ScanResponse can't be null") - - private val currentUserWalletId = createUserWalletId(firstCardScanResponse) + private val userWalletId = savedStateHandle.get(AppRoute.ResetToFactory.USER_WALLET_ID) + ?.unbundle(UserWalletId.serializer()) + ?: error("User wallet ID must be provided for ResetCardViewModel") // TODO: move logic to separate domain entity private var resetBackupCardCount = 0 - fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent { - val descriptionText = state?.cardInfo - ?.toResetCardDescriptionText() - ?: TextReference.Str(value = "") + val screenState: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + private fun getInitialState(): ResetCardScreenState { + val userWallet = getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet $userWalletId: $it") + } + val card = userWallet.scanResponse.card + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + val descriptionText = getResetToFactoryDescription(card, cardTypesResolver) + val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() + val showResetPasswordButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active val warningsToShow = buildList { add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS) - if (state?.isShowPasswordResetRadioButton == true) { + if (showResetPasswordButton) { add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE) } } - return ResetCardScreenState.ResetCardScreenContent( - accepted = state?.resetButtonEnabled ?: false, + return ResetCardScreenState( + resetButtonEnabled = false, descriptionText = descriptionText, warningsToShow = warningsToShow, - acceptCondition1Checked = state?.condition1Checked ?: false, - acceptCondition2Checked = state?.condition2Checked ?: false, - onAcceptCondition1ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition1(it)) }, - onAcceptCondition2ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition2(it)) }, - onResetButtonClick = { showDialog(CardSettingsDialog.StartResetDialog) }, - dialog = state?.dialog?.let(::createDialog), + showResetPasswordButton = showResetPasswordButton, + acceptCondition1Checked = false, + acceptCondition2Checked = false, + onAcceptCondition1ToggleClick = ::toggleFirstCondition, + onAcceptCondition2ToggleClick = ::toggleSecondCondition, + onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) }, + dialog = null, ) } - private fun createDialog(dialog: CardSettingsDialog): ResetCardScreenState.ResetCardScreenContent.Dialog { + private fun toggleFirstCondition(isAccepted: Boolean) { + screenState.update { prevState -> + val resetButtonEnabled = if (prevState.showResetPasswordButton) { + isAccepted && prevState.acceptCondition2Checked + } else { + isAccepted + } + + prevState.copy( + acceptCondition1Checked = isAccepted, + resetButtonEnabled = resetButtonEnabled, + ) + } + } + + private fun toggleSecondCondition(isAccepted: Boolean) { + screenState.update { prevState -> + val resetButtonEnabled = prevState.acceptCondition1Checked && isAccepted + + prevState.copy( + acceptCondition2Checked = isAccepted, + resetButtonEnabled = resetButtonEnabled, + ) + } + } + + private fun createDialog(dialog: ResetCardDialog): ResetCardScreenState.Dialog { return when (dialog) { - CardSettingsDialog.StartResetDialog -> { - ResetCardScreenState.ResetCardScreenContent.Dialog.StartReset( + ResetCardDialog.StartResetDialog -> { + ResetCardScreenState.Dialog.StartReset( onConfirmClick = ::onStartResetClick, onDismiss = ::dismissDialog, ) } - CardSettingsDialog.ContinueResetDialog -> { - ResetCardScreenState.ResetCardScreenContent.Dialog.ContinueReset( + ResetCardDialog.ContinueResetDialog -> { + ResetCardScreenState.Dialog.ContinueReset( onConfirmClick = ::onContinueResetClick, onDismiss = ::onContinueResetDialogDismiss, ) } - CardSettingsDialog.InterruptedResetDialog -> { - ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset( + ResetCardDialog.InterruptedResetDialog -> { + ResetCardScreenState.Dialog.InterruptedReset( onConfirmClick = ::onContinueResetClick, onDismiss = ::onInterruptedResetDialogDismiss, ) } - CardSettingsDialog.CompletedResetDialog -> { - ResetCardScreenState.ResetCardScreenContent.Dialog.CompletedReset( + ResetCardDialog.CompletedResetDialog -> { + ResetCardScreenState.Dialog.CompletedReset( onConfirmClick = ::dismissAndFinishFullReset, ) } @@ -109,21 +149,26 @@ internal class ResetCardViewModel @Inject constructor( private fun onStartResetClick() { dismissDialog() - if (resetCardFeatureToggles.isFullResetEnabled) { - makeFullReset() - } else { - store.dispatch(ResetToFactory.Proceed) - } + makeFullReset() } private fun makeFullReset() { viewModelScope.launch { - resetCardUseCase(card = firstCardScanResponse.card).onRight { - deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId) - deleteWalletUseCase(currentUserWalletId) + val userWallet = getUserWallet() + val scanResponse = userWallet.scanResponse + + resetCardUseCase(card = scanResponse.card).onRight { + deleteSavedAccessCodesUseCase(scanResponse.card.cardId) + val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + Timber.e("Unable to delete user wallet: $it") + return@launch + } + + if (hasUserWallets) { + val newSelectedWallet = getSelectedWalletSyncUseCase().getOrElse { + error("Failed to get selected wallet: $it") + } - val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull() - if (newSelectedWallet != null) { store.onUserWalletSelected(newSelectedWallet) } @@ -138,10 +183,11 @@ internal class ResetCardViewModel @Inject constructor( dismissDialog() viewModelScope.launch { + val userWallet = getUserWallet() resetCardUseCase( cardNumber = resetBackupCardCount + 1, - card = firstCardScanResponse.card, - userWalletId = currentUserWalletId, + card = userWallet.scanResponse.card, + userWalletId = userWalletId, ) .onRight { resetBackupCardCount++ @@ -150,14 +196,14 @@ internal class ResetCardViewModel @Inject constructor( checkRemainingBackupCards() } - .onLeft { showDialog(CardSettingsDialog.InterruptedResetDialog) } + .onLeft { showDialog(ResetCardDialog.InterruptedResetDialog) } } } private fun onContinueResetDialogDismiss() { dismissDialog() - showDialog(CardSettingsDialog.InterruptedResetDialog) + showDialog(ResetCardDialog.InterruptedResetDialog) } private fun onInterruptedResetDialogDismiss() { @@ -167,20 +213,26 @@ internal class ResetCardViewModel @Inject constructor( } private fun checkRemainingBackupCards() { - val backupCardsCount = firstCardScanResponse.getBackupCardsCount() + val backupCardsCount = getUserWallet().scanResponse.getBackupCardsCount() when { - backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog) + backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog) backupCardsCount == resetBackupCardCount -> { analyticsEventHandler.send( event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1), ) - showDialog(CardSettingsDialog.CompletedResetDialog) + showDialog(ResetCardDialog.CompletedResetDialog) } else -> finishFullReset() } } + private fun getUserWallet(): UserWallet { + return getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet $userWalletId: $it") + } + } + private fun dismissAndFinishFullReset() { dismissDialog() @@ -202,12 +254,12 @@ internal class ResetCardViewModel @Inject constructor( } } - private fun showDialog(dialog: CardSettingsDialog) { - store.dispatch(ResetToFactory.ShowDialog(dialog)) + private fun showDialog(dialog: ResetCardDialog) { + screenState.update { it.copy(dialog = createDialog(dialog)) } } private fun dismissDialog() { - store.dispatch(ResetToFactory.DismissDialog) + screenState.update { it.copy(dialog = null) } } private fun ScanResponse.getBackupCardsCount(): Int { @@ -221,9 +273,4 @@ internal class ResetCardViewModel @Inject constructor( -> 0 } } - - private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId { - return UserWalletIdBuilder.scanResponse(scanResponse).build() - ?: error("UserWalletId can't be null") - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt deleted file mode 100644 index 2de1e70012..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.details.ui.resetcard.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.tap.features.details.ui.resetcard.featuretoggles.DefaultResetCardFeatureToggles -import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles -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 ResetCardModule { - - @Provides - @Singleton - fun provideResetCardFeatureToggles(featureTogglesManager: FeatureTogglesManager): ResetCardFeatureToggles { - return DefaultResetCardFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt deleted file mode 100644 index a5b76a410d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.features.details.ui.resetcard.featuretoggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -internal class DefaultResetCardFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : ResetCardFeatureToggles { - - override val isFullResetEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "FULL_RESET_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt deleted file mode 100644 index d290d4d1d9..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.features.details.ui.resetcard.featuretoggles - -interface ResetCardFeatureToggles { - - val isFullResetEnabled: Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index f61e54fd78..1c5fe7f90b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -1,55 +1,34 @@ package com.tangem.tap.features.details.ui.securitymode import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint -import org.rekotlin.StoreSubscriber import javax.inject.Inject @AndroidEntryPoint -internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { +internal class SecurityModeFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies - private val viewModel = SecurityModeViewModel(store) - - private var screenState: MutableState = - mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState)) + private val viewModel: SecurityModeViewModel by viewModels() @Composable override fun ScreenContent(modifier: Modifier) { + val state by viewModel.screenState.collectAsStateWithLifecycle() + SecurityModeScreen( modifier = modifier, - state = screenState.value, + state = state, onBackClick = { store.dispatchNavigationAction(AppRouter::pop) }, ) } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } - } - } - - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt index 7aa863a7df..5f0d54717e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt @@ -1,29 +1,109 @@ package com.tangem.tap.features.details.ui.securitymode -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.ManageSecurityState +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.bundle.unbundle +import com.tangem.core.analytics.Analytics +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.features.details.redux.SecurityOption -import org.rekotlin.Store +import com.tangem.tap.features.details.ui.common.utils.getAllowedSecurityOptions +import com.tangem.tap.features.details.ui.common.utils.getCurrentSecurityOption +import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject -internal class SecurityModeViewModel(val store: Store) { +@HiltViewModel +internal class SecurityModeViewModel @Inject constructor( + private val getUserWalletUseCase: GetUserWalletUseCase, + private val tangemSdkManager: TangemSdkManager, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val userWalletId = savedStateHandle.get(AppRoute.DetailsSecurity.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("UserWalletId is required for SecurityModeViewModel") + + val screenState = MutableStateFlow( + value = getInitialState(), + ) + + private fun getInitialState(): SecurityModeScreenState { + val userWallet = getUserWallet() + val scanResponse = userWallet.scanResponse + val card = scanResponse.card + val cardTypesResolver = scanResponse.cardTypesResolver + + val currentSecurityOption = getCurrentSecurityOption(card) + val allowedSecurityOptions = getAllowedSecurityOptions(card, cardTypesResolver, currentSecurityOption) - fun updateState(state: ManageSecurityState?): SecurityModeScreenState { - if (state == null) { - return SecurityModeScreenState( - availableOptions = emptyList(), - selectedSecurityMode = SecurityOption.LongTap, - isSaveChangesEnabled = false, - onNewModeSelected = {}, - onSaveChangesClicked = {}, - ) - } return SecurityModeScreenState( - availableOptions = state.allowedOptions.toList(), - selectedSecurityMode = state.selectedOption, - isSaveChangesEnabled = state.selectedOption != state.currentOption, - onNewModeSelected = { store.dispatch(DetailsAction.ManageSecurity.SelectOption(it)) }, - onSaveChangesClicked = { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) }, + availableOptions = allowedSecurityOptions.toList(), + selectedSecurityMode = currentSecurityOption, + isSaveChangesEnabled = false, + onNewModeSelected = ::selectOption, + onSaveChangesClicked = ::saveChanges, ) } + + private fun selectOption(securityOption: SecurityOption) { + screenState.update { state -> + state.copy( + selectedSecurityMode = securityOption, + isSaveChangesEnabled = securityOption != getCurrentSecurityOption(getUserWallet().scanResponse.card), + ) + } + } + + private fun saveChanges() { + val userWallet = getUserWallet() + val cardId = userWallet.cardId + val selectedOption = screenState.value.selectedSecurityMode + + viewModelScope.launch { + val result = when (selectedOption) { + SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId) + SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId) + SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId) + } + + val paramValue = AnalyticsParam.SecurityMode.from(selectedOption) + when (result) { + is CompletionResult.Success -> { + Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) + + store.dispatchNavigationAction(AppRouter::pop) + } + is CompletionResult.Failure -> { + val error = result.error + if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { + Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error)) + } + } + else -> Unit + } + } + } + + private fun getUserWallet(): UserWallet { + return getUserWalletUseCase(userWalletId).getOrElse { + error("Unable to get user wallet $userWalletId: $it") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt b/app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt deleted file mode 100644 index 459e5ba49a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.features.details.ui.utils - -import com.tangem.tap.features.details.redux.CardInfo -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.wallet.R - -internal fun CardInfo.toResetCardDescriptionText(): TextReference { - return if (!this.hasBackup || this.isTwin) { - TextReference.Res(R.string.reset_card_without_backup_to_factory_message) - } else { - TextReference.Res(R.string.reset_card_with_backup_to_factory_message) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index bb885005f9..0ecd71bd5d 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -30,7 +30,7 @@ internal class WelcomeViewModel @Inject constructor( DefaultLifecycleObserver { private val initialIntent: SerializableIntent? = savedStateHandle.get(AppRoute.Welcome.INITIAL_INTENT_KEY) - ?.let { it.unbundle(SerializableIntent.serializer()) } + ?.unbundle(SerializableIntent.serializer()) private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 8bd5b8a2c1..136385e388 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -117,19 +117,55 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/details/${userWalletId.stringValue}") @Serializable - data object DetailsSecurity : AppRoute(path = "/details/security") + data class DetailsSecurity( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/details/security"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + } + } @Serializable - data object CardSettings : AppRoute(path = "/card_settings") + data class CardSettings( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/card_settings/${userWalletId.stringValue}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + } + } @Serializable data object AppSettings : AppRoute(path = "/app_settings") @Serializable - data object ResetToFactory : AppRoute(path = "/reset_to_factory") + data class ResetToFactory( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID = "userWalletId" + } + } @Serializable - data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery") + data class AccessCodeRecovery( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/access_code_recovery/${userWalletId.stringValue}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + } + } @Serializable data object ManageTokens : AppRoute(path = "/manage_tokens") 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 8c540a7cbd..7def5136f7 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -27,10 +27,6 @@ "name": "STAKING_ENABLED", "version": "undefined" }, - { - "name": "FULL_RESET_ENABLED", - "version": "5.12.0" - }, { "name": "DETAILS_REDESIGN_ENABLED", "version": "5.13.0" 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 82208040e8..6a4afdcfd9 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 @@ -143,7 +143,13 @@ internal class TangemCardTypesResolver( override fun getRemainingSignatures(): Int? = card.wallets.firstOrNull()?.remainingSignatures - override fun getCardId(): String = card.cardId + override fun getCardId(): String { + return if (isTangemTwins()) { + card.getTwinCardIdForUser() + } else { + card.cardId + } + } override fun isTestCard(): Boolean = card.isTestCard diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt index 61ec0cc72e..1c3519f606 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt @@ -13,6 +13,7 @@ import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.configs.Wallet2CardConfig import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet val ScanResponse.cardTypesResolver: CardTypesResolver get() = TangemCardTypesResolver( @@ -27,6 +28,9 @@ val ScanResponse.derivationStyleProvider: DerivationStyleProvider card, ) +val UserWallet.cardTypesResolver: CardTypesResolver + get() = scanResponse.cardTypesResolver + fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index a94b469537..8c61fc867a 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.common.routing) /* Project - Domain */ + implementation(projects.domain.legacy) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 1ba3218c38..7649ef9bd7 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -3,6 +3,7 @@ package com.tangem.feature.walletsettings.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.ui.WalletSettingsScreen @@ -15,7 +16,9 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { items = ItemsBuilder( router = DummyRouter(), ).buildItems( - walletName = "My wallet", + userWalletId = UserWalletId("011"), + userWalletName = "My Wallet", + isReferralAvailable = true, renameWallet = {}, forgetWallet = {}, ), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 188a3ba043..e7d755f454 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase @@ -75,7 +76,9 @@ internal class WalletSettingsModel @Inject constructor( userWallet: UserWallet, dialogNavigation: SlotNavigation, ): PersistentList = itemsBuilder.buildItems( - walletName = userWallet.name, + userWalletId = userWallet.walletId, + userWalletName = userWallet.name, + isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, forgetWallet = { forgetWallet(userWallet.walletId) }, ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 52f95c08b8..deb049021a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -6,9 +6,12 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject @ComponentScoped @@ -16,9 +19,15 @@ internal class ItemsBuilder @Inject constructor( private val router: Router, ) { - fun buildItems(walletName: String, forgetWallet: () -> Unit, renameWallet: () -> Unit) = persistentListOf( - buildNameItem(walletName, renameWallet), - buildCardItem(), + fun buildItems( + userWalletId: UserWalletId, + userWalletName: String, + isReferralAvailable: Boolean, + forgetWallet: () -> Unit, + renameWallet: () -> Unit, + ): PersistentList = persistentListOf( + buildNameItem(userWalletName, renameWallet), + buildCardItem(userWalletId, isReferralAvailable), buildForgetItem(forgetWallet), ) @@ -29,37 +38,37 @@ internal class ItemsBuilder @Inject constructor( onClick = renameWallet, ) - private fun buildCardItem() = WalletSettingsItemUM.WithItems( - id = "card", - blocks = buildCardBlocks(), - description = resourceReference(R.string.settings_card_settings_footer), - ) + private fun buildCardItem(userWalletId: UserWalletId, isReferralAvailable: Boolean) = + WalletSettingsItemUM.WithItems( + id = "card", + description = resourceReference(R.string.settings_card_settings_footer), + blocks = buildList { + BlockUM( + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, + ).let(::add) - private fun buildCardBlocks() = persistentListOf( - BlockUM( - text = resourceReference(R.string.card_settings_title), - iconRes = R.drawable.ic_card_settings_24, - onClick = { router.push(AppRoute.CardSettings) }, - ), - BlockUM( - text = resourceReference(R.string.referral_title), - iconRes = R.drawable.ic_add_friends_24, - onClick = { router.push(AppRoute.ReferralProgram) }, - ), - ) + if (isReferralAvailable) { + BlockUM( + text = resourceReference(R.string.referral_title), + iconRes = R.drawable.ic_add_friends_24, + onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) }, + ).let(::add) + } + }.toImmutableList(), + ) private fun buildForgetItem(forgetWallet: () -> Unit) = WalletSettingsItemUM.WithItems( id = "forget", - blocks = buildForgetBlocks(forgetWallet), description = resourceReference(R.string.settings_forget_wallet_footer), - ) - - private fun buildForgetBlocks(forgetWallet: () -> Unit) = persistentListOf( - BlockUM( - text = resourceReference(R.string.settings_forget_wallet), - iconRes = R.drawable.ic_card_foget_24, - onClick = forgetWallet, - accentType = BlockUM.AccentType.WARNING, + blocks = persistentListOf( + BlockUM( + text = resourceReference(R.string.settings_forget_wallet), + iconRes = R.drawable.ic_card_foget_24, + onClick = forgetWallet, + accentType = BlockUM.AccentType.WARNING, + ), ), ) } \ No newline at end of file From 1aba736b1e73cc5e6b4172068b1430466e217cdd Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 17:50:49 +0400 Subject: [PATCH 153/223] Updated on 2026-08-14 --- .../com/tangem/common/routing/AppRoute.kt | 11 ++++++++++- .../referral/domain/ReferralInteractor.kt | 5 +++-- .../referral/domain/ReferralInteractorImpl.kt | 15 ++++++++------- .../referral/domain/di/ReferralDomainModule.kt | 6 +++--- .../referral/presentation/build.gradle.kts | 1 + .../referral/viewmodels/ReferralViewModel.kt | 18 ++++++++++++++---- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 136385e388..8a2676a038 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -191,7 +191,16 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data object ReferralProgram : AppRoute(path = "/referral_program") + data class ReferralProgram( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/referral_program"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + } + } @Serializable data class Swap( diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt index 8024d1cc67..2bf7174f00 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt @@ -1,12 +1,13 @@ package com.tangem.feature.referral.domain +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.referral.domain.models.ReferralData interface ReferralInteractor { val isDemoMode: Boolean - suspend fun getReferralStatus(): ReferralData + suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData - suspend fun startReferral(): ReferralData + suspend fun startReferral(userWalletId: UserWalletId): ReferralData } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 154764b162..36782f4e1d 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -3,7 +3,8 @@ package com.tangem.feature.referral.domain import arrow.core.getOrElse import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData import com.tangem.lib.crypto.UserWalletManager @@ -14,7 +15,7 @@ internal class ReferralInteractorImpl( private val repository: ReferralRepository, private val userWalletManager: UserWalletManager, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, ) : ReferralInteractor { @@ -22,20 +23,20 @@ internal class ReferralInteractorImpl( override val isDemoMode: Boolean get() = repository.isDemoMode - override suspend fun getReferralStatus(): ReferralData { - val referralData = repository.getReferralData(userWalletManager.getWalletId()) + override suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData { + val referralData = repository.getReferralData(userWalletId.stringValue) saveReferralTokens(referralData.tokens) return referralData } - override suspend fun startReferral(): ReferralData { + override suspend fun startReferral(userWalletId: UserWalletId): ReferralData { if (tokensForReferral.isEmpty()) error("Tokens for ref is empty") val tokenData = tokensForReferral.first() - val userWallet = getSelectedWalletSyncUseCase().getOrElse { - error("Failed to get selected wallet: $it") + val userWallet = getUserWalletUseCase(userWalletId).getOrElse { + error("Failed to get user wallet $userWalletId: $it") } val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt index 0cd466fed7..172c41b27d 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -2,7 +2,7 @@ package com.tangem.feature.referral.domain.di import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.ReferralInteractorImpl import com.tangem.feature.referral.domain.ReferralRepository @@ -23,14 +23,14 @@ class ReferralDomainModule { referralRepository: ReferralRepository, userWalletManager: UserWalletManager, derivePublicKeysUseCase: DerivePublicKeysUseCase, - getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + getUserWalletUseCase: GetUserWalletUseCase, addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, ): ReferralInteractor { return ReferralInteractorImpl( repository = referralRepository, userWalletManager = userWalletManager, derivePublicKeysUseCase = derivePublicKeysUseCase, - getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase, + getUserWalletUseCase = getUserWalletUseCase, addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase, ) } diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 8a99c77aa6..ef6062abd5 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(deps.compose.ui.tooling) /** Domain */ + implementation(projects.domain.wallets.models) implementation(projects.features.referral.domain) /** Other libraries */ diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index 82c0d08bb8..6565947793 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -1,11 +1,16 @@ package com.tangem.feature.referral.viewmodels +import android.os.Bundle 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.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.referral.analytics.ReferralEvents import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.models.DiscountType @@ -29,15 +34,20 @@ internal class ReferralViewModel @Inject constructor( private val referralInteractor: ReferralInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, + savedStateHandle: SavedStateHandle, ) : ViewModel() { - var uiState: ReferralStateHolder by mutableStateOf(createInitiallyUiState()) - private set + private val userWalletId = savedStateHandle.get(AppRoute.ReferralProgram.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("User wallet ID is required for Referral screen") private var referralRouter: ReferralRouter by Delegates.notNull() private var lastReferralData: ReferralData? = null + var uiState: ReferralStateHolder by mutableStateOf(createInitiallyUiState()) + private set + init { loadReferralData() } @@ -66,7 +76,7 @@ internal class ReferralViewModel @Inject constructor( uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { - referralInteractor.getReferralStatus().apply { + referralInteractor.getReferralStatus(userWalletId).apply { lastReferralData = this } } @@ -82,7 +92,7 @@ internal class ReferralViewModel @Inject constructor( analyticsEventHandler.send(ReferralEvents.ClickParticipate) uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) viewModelScope.launch(dispatchers.main) { - runCatching(dispatchers.io) { referralInteractor.startReferral() } + runCatching(dispatchers.io) { referralInteractor.startReferral(userWalletId) } .onSuccess(::showContent) .onFailure { throwable -> if (throwable is UserCancelledException) { From c62feef82e5fa0a222802f8e322e872dd1d9b0da Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 09:58:22 +0100 Subject: [PATCH 154/223] Updated on 2026-08-14 --- .../src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 8961c88d8a..1c22bba006 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -343,5 +343,4 @@ private val excludedBlockchains = listOf( Blockchain.NexaTestnet, Blockchain.Mantle, Blockchain.MantleTestnet, - Blockchain.Bittensor, ) \ No newline at end of file From 337b101c1ba0b088c03353970654acdf90ceba0f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 10:01:05 +0100 Subject: [PATCH 155/223] Updated on 2026-08-14 --- .../extension/AppExtensionConfigurations.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 885f32c1d7..d65c211b5d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -65,7 +65,9 @@ private fun AppExtension.configureBuildTypes() { private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buildType: BuildType) { when (buildType) { - BuildType.Release -> { + BuildType.Release, + BuildType.External, + -> { isDebuggable = false isMinifyEnabled = false proguardFiles(extension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") @@ -75,12 +77,12 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil isMinifyEnabled = false } BuildType.Internal, - BuildType.External, - BuildType.Mocked + BuildType.Mocked, -> { initWith(extension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id) + isDebuggable = true } } From 6d74f3936cb8c28f5f433b94dbe965115895ff03 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 12:02:38 +0300 Subject: [PATCH 156/223] Updated on 2026-08-14 --- .../java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt index dc15eb3e95..10299c8916 100644 --- a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt +++ b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt @@ -22,5 +22,6 @@ internal class AndroidAppFinisher( flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } appContext.startActivity(intent) + Runtime.getRuntime().exit(0) } } \ No newline at end of file From 87def6a0a5f5f26e5f0fcf13942d005914c2f560 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 13:11:19 +0400 Subject: [PATCH 157/223] Updated on 2026-08-14 --- .../com/tangem/domain/wallets/legacy/UserWalletsListManager.kt | 2 ++ .../domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt | 1 + .../tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt | 1 + .../wallet/presentation/wallet/viewmodels/WalletViewModel.kt | 2 ++ 4 files changed, 6 insertions(+) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt index 9b63847e73..bd117a8c8e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt @@ -16,12 +16,14 @@ interface UserWalletsListManager { val userWallets: Flow> /** [Flow] with selected [UserWallet] updates */ + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") val selectedUserWallet: Flow /** [List] with all saved [UserWallet]s updates */ val userWalletsSync: List /** Selected [UserWallet] */ + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") val selectedUserWalletSync: UserWallet? /** Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet] */ diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index e9f51327a6..ebcd1378d7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -17,6 +17,7 @@ import com.tangem.domain.wallets.models.UserWallet */ class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either { return either { ensureNotNull( diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index bda3c953ee..4751238a0f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow */ class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either> { return either { userWalletsListManager.selectedUserWallet 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 e13c2f56af..9c94f8bcdf 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 @@ -176,6 +176,8 @@ internal class WalletViewModel @Inject constructor( } } + // It's okay here because we need to be able to observe the selected wallet changes + @Suppress("DEPRECATION") private fun subscribeOnSelectedWalletFlow() { getSelectedWalletUseCase().onRight { it From f8884d521f9646f94c897c07e3ae20fbba935c35 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 15:12:57 +0400 Subject: [PATCH 158/223] Updated on 2026-08-14 --- features/details/impl/build.gradle.kts | 1 - features/manage-tokens/api/build.gradle.kts | 7 +- .../component/ManageTokensComponent.kt | 14 ++++ features/manage-tokens/impl/build.gradle.kts | 70 ++++++------------- .../preview/PreviewManageTokensComponent.kt | 23 ++++++ .../managetokens/ui/ManageTokensScreen.kt | 35 ++++++++++ .../extension/BaseExtensionConfigurations.kt | 1 - 7 files changed, 98 insertions(+), 53 deletions(-) create mode 100644 features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index c6694323ba..82ca657a96 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { implementation(deps.tangem.blockchain) /* AndroidX */ - implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.compose) diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index cbfb3f7b1f..0cf94d3840 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -9,5 +9,10 @@ android { } dependencies { - implementation(deps.compose.foundation) + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Project - Core */ + implementation(projects.core.ui) + implementation(projects.core.decompose) } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt new file mode 100644 index 0000000000..54cb358614 --- /dev/null +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.managetokens.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface ManageTokensComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 88d462b52c..8e3cc2bddb 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) alias(deps.plugins.kotlin.kapt) alias(deps.plugins.hilt.android) id("configuration") @@ -11,61 +12,30 @@ android { } dependencies { - /** AndroidX */ - implementation(deps.androidx.activity.compose) - implementation(deps.material) + /* Project - API */ + implementation(projects.features.manageTokens.api) - /** Compose */ - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.coil) - implementation(deps.compose.constraintLayout) - 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) + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.common.routing) + + /* AndroidX */ + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /* Compose */ implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) - /** Other libraries */ - implementation(deps.arrow.core) - implementation(deps.jodatime) - implementation(deps.kotlin.immutable.collections) - implementation(deps.tangem.card.core) - implementation(deps.timber) - - /** DI */ + /* DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) - /** Core modules */ - implementation(projects.core.analytics) - implementation(projects.core.analytics.models) - implementation(projects.core.featuretoggles) - implementation(projects.core.navigation) - implementation(projects.core.ui) - implementation(projects.core.utils) - - /** Project - Data */ - implementation(projects.data.tokens) - - /** Domain modules */ - implementation(projects.domain.card) - implementation(projects.domain.demo) - implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) - implementation(projects.domain.models) - implementation(projects.domain.settings) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets) - implementation(projects.domain.wallets.models) - implementation(projects.domain.appCurrency) - implementation(projects.domain.appCurrency.models) - - /** Feature Apis */ - implementation(projects.features.manageTokens.api) + /* Other */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt new file mode 100644 index 0000000000..5bf83bf727 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -0,0 +1,23 @@ +package com.tangem.features.managetokens.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.ui.ManageTokensScreen +import kotlinx.collections.immutable.persistentListOf + +internal class PreviewManageTokensComponent : ManageTokensComponent { + + private val previewState: ManageTokensUM = ManageTokensUM( + popBack = {}, + ) + + @Composable + override fun Content(modifier: Modifier) { + ManageTokensScreen( + modifier = modifier, + state = previewState, + ) + } +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt new file mode 100644 index 0000000000..8a4be14f9c --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -0,0 +1,35 @@ +package com.tangem.features.managetokens.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxWidth +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.TangemThemePreview +import com.tangem.feature.managetokens.component.preview.PreviewManageTokensComponent +import com.tangem.feature.managetokens.entity.ManageTokensUM + +@Composable +internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.popBack) + + Scaffold( + modifier = modifier, + content = { + TODO("Not yet implemented") + }, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_ManageTokens() { + TangemThemePreview { + PreviewManageTokensComponent().Content(Modifier.fillMaxWidth()) + } +} +// endregion Preview \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index 1612e170ed..49a6869c47 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -24,7 +24,6 @@ internal fun BaseExtension.configureCompose(project: Project) { contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] - contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) } From 0c2999918a9ca377206e9a468c1fdee4fe74628b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 19:42:09 +0400 Subject: [PATCH 159/223] Updated on 2026-08-14 --- .../preview/PreviewManageTokensComponent.kt | 45 ++++++++- .../managetokens/entity/CurrencyItemUM.kt | 23 +++++ .../managetokens/entity/ManageTokensUM.kt | 10 ++ .../managetokens/ui/ManageTokensScreen.kt | 95 ++++++++++++++++++- 4 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 5bf83bf727..7fad0628a4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -2,15 +2,58 @@ package com.tangem.features.managetokens.component.preview import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.ManageTokensScreen -import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList internal class PreviewManageTokensComponent : ManageTokensComponent { private val previewState: ManageTokensUM = ManageTokensUM( popBack = {}, + items = List(size = 30) { index -> + if (index < 2) { + CurrencyItemUM.Custom( + id = index.toString(), + model = ChainRowUM( + name = stringReference("Custom $index"), + type = stringReference("CM$index"), + icon = TokenIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + networkBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, + ), + showCustom = true, + ), + onRemoveClick = {}, + ) + } else { + CurrencyItemUM.Basic( + id = index.toString(), + model = ChainRowUM( + name = stringReference("Basic $index"), + type = stringReference("BT$index"), + icon = TokenIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), + isExpanded = false, + onExpandClick = {}, + ) + } + }.toImmutableList(), ) @Composable diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt new file mode 100644 index 0000000000..1fd59cebc8 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -0,0 +1,23 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.rows.model.ChainRowUM + +@Immutable +internal sealed class CurrencyItemUM { + + abstract val id: String + + data class Basic( + override val id: String, + val model: ChainRowUM, + val isExpanded: Boolean, + val onExpandClick: () -> Unit, + ) : CurrencyItemUM() + + data class Custom( + override val id: String, + val model: ChainRowUM, + val onRemoveClick: () -> Unit, + ) : CurrencyItemUM() +} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt new file mode 100644 index 0000000000..3bd1757c82 --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class ManageTokensUM( + val popBack: () -> Unit, + val items: ImmutableList, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 8a4be14f9c..7ceeac64f8 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -2,14 +2,33 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.rows.ChainRow +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.managetokens.component.preview.PreviewManageTokensComponent -import com.tangem.feature.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent +import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.ManageTokensUM +import com.tangem.features.managetokens.impl.R +import kotlinx.collections.immutable.ImmutableList @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { @@ -17,12 +36,80 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi Scaffold( modifier = modifier, - content = { - TODO("Not yet implemented") + containerColor = TangemTheme.colors.background.primary, + content = { innerPadding -> + Currencies( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + items = state.items, + ) }, ) } +private const val CHEVRON_ROTATION_EXPANDED = 180f +private const val CHEVRON_ROTATION_COLLAPSED = 0f + +@Composable +private fun Currencies(items: ImmutableList, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier, + ) { + items( + items = items, + key = CurrencyItemUM::id, + ) { item -> + when (item) { + is CurrencyItemUM.Basic -> { + ChainRow( + modifier = Modifier.fillMaxWidth(), + model = item.model, + action = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = item.onExpandClick, + ) { + val rotation by animateFloatAsState( + targetValue = if (item.isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) + + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + }, + ) + } + is CurrencyItemUM.Custom -> { + ChainRow( + modifier = Modifier.fillMaxWidth(), + model = item.model, + action = { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.manage_tokens_remove), + onClick = item.onRemoveClick, + ), + ) + }, + ) + } + } + } + } +} + // region Preview @Preview(showBackground = true, widthDp = 360, heightDp = 800) @Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) From 88f84097f7f90957bb8e1a3c643c662655322771 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 26 Jun 2024 20:19:55 +0400 Subject: [PATCH 160/223] Updated on 2026-08-14 --- .../preview/PreviewManageTokensComponent.kt | 172 +++++++++++++----- .../managetokens/entity/CurrencyItemUM.kt | 16 +- .../managetokens/entity/CurrencyNetworkUM.kt | 12 ++ .../managetokens/ui/ManageTokensScreen.kt | 147 ++++++++++----- 4 files changed, 261 insertions(+), 86 deletions(-) create mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 7fad0628a4..9abc1cde2c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -1,66 +1,156 @@ package com.tangem.features.managetokens.component.preview import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.stringReference import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyNetworkUM import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.ui.ManageTokensScreen +import kotlinx.collections.immutable.mutate import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlin.random.Random internal class PreviewManageTokensComponent : ManageTokensComponent { - private val previewState: ManageTokensUM = ManageTokensUM( - popBack = {}, - items = List(size = 30) { index -> - if (index < 2) { - CurrencyItemUM.Custom( - id = index.toString(), - model = ChainRowUM( - name = stringReference("Custom $index"), - type = stringReference("CM$index"), - icon = TokenIconState.CustomTokenIcon( - tint = Color.White, - background = Color.Black, - networkBadgeIconResId = R.drawable.img_eth_22, - isGrayscale = false, - showCustomBadge = true, - ), - showCustom = true, - ), - onRemoveClick = {}, - ) - } else { - CurrencyItemUM.Basic( - id = index.toString(), - model = ChainRowUM( - name = stringReference("Basic $index"), - type = stringReference("BT$index"), - icon = TokenIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_btc_22, - isGrayscale = false, - showCustomBadge = false, - ), - showCustom = false, - ), - isExpanded = false, - onExpandClick = {}, - ) - } - }.toImmutableList(), + private var items = List(size = 30) { index -> + if (index < 2) { + getCustomItem(index) + } else { + getBasicItem(index) + } + }.toPersistentList() + + private val previewState = MutableStateFlow( + value = ManageTokensUM( + popBack = {}, + items = items, + ), ) @Composable override fun Content(modifier: Modifier) { + val state by previewState.collectAsState() + ManageTokensScreen( modifier = modifier, - state = previewState, + state = state, ) } + + private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( + id = index.toString(), + model = ChainRowUM( + name = stringReference("Custom token $index"), + type = stringReference("CT$index"), + icon = CurrencyIconState.CustomTokenIcon( + tint = Color.White, + background = Color.Black, + networkBadgeIconResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = true, + ), + showCustom = true, + ), + onRemoveClick = {}, + ) + + private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( + id = index.toString(), + model = ChainRowUM( + name = stringReference("Currency $index"), + type = stringReference("C$index"), + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), + networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, + onExpandClick = { toggleCurrency(index) }, + ) + + private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 5) { networkIndex -> + val selectedIndex = Random.nextInt(from = 0, until = 5) + + CurrencyNetworkUM( + id = networkIndex.toString(), + model = BlockchainRowUM( + name = stringReference("NETWORK$networkIndex"), + type = stringReference("N$networkIndex"), + icon = CurrencyIconState.Locked, + isAccented = networkIndex == 0, + usePrimaryTextColor = networkIndex == selectedIndex, + ), + isSelected = networkIndex == selectedIndex, + onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, + ) + }.toImmutableList() + + private fun toggleCurrency(index: Int) { + val updatedItem = when (val item = items[index]) { + is CurrencyItemUM.Basic -> item.copy( + networks = if (item.networks is CurrencyItemUM.Basic.NetworksUM.Collapsed) { + CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) + } else { + CurrencyItemUM.Basic.NetworksUM.Collapsed + }, + ) + is CurrencyItemUM.Custom -> return + } + + previewState.update { state -> + items = items.mutate { + it[index] = updatedItem + } + state.copy(items = items) + } + } + + private fun toggleNetwork(currencyIndex: Int, networkIndex: Int, isSelected: Boolean) { + val updatedItem = when (val item = items[currencyIndex]) { + is CurrencyItemUM.Basic -> { + val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) + ?.copy( + networks = item.networks.networks.toPersistentList().mutate { + it.fastForEachIndexed { index, network -> + if (index == networkIndex) { + it[index] = network.copy( + model = network.model.copy( + usePrimaryTextColor = isSelected, + ), + isSelected = isSelected, + ) + } + } + }, + ) + ?: return + + item.copy(networks = updatedNetworks) + } + is CurrencyItemUM.Custom -> return + } + + previewState.update { state -> + items = items.mutate { + it[currencyIndex] = updatedItem + } + state.copy(items = items) + } + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt index 1fd59cebc8..3cd78d349a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.rows.model.ChainRowUM +import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class CurrencyItemUM { @@ -11,9 +12,20 @@ internal sealed class CurrencyItemUM { data class Basic( override val id: String, val model: ChainRowUM, - val isExpanded: Boolean, + val networks: NetworksUM, val onExpandClick: () -> Unit, - ) : CurrencyItemUM() + ) : CurrencyItemUM() { + + @Immutable + sealed class NetworksUM { + + data object Collapsed : NetworksUM() + + data class Expanded( + val networks: ImmutableList, + ) : NetworksUM() + } + } data class Custom( override val id: String, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt new file mode 100644 index 0000000000..8ed8dc5f9d --- /dev/null +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyNetworkUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.managetokens.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.rows.model.BlockchainRowUM + +@Immutable +internal data class CurrencyNetworkUM( + val id: String, + val model: BlockchainRowUM, + val isSelected: Boolean, + val onSelectedStateChange: (Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 7ceeac64f8..af11b22917 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -2,11 +2,9 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Icon @@ -14,22 +12,31 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.rows.ArrowRow +import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM +import com.tangem.features.managetokens.entity.CurrencyItemUM.Basic.NetworksUM import com.tangem.features.managetokens.entity.ManageTokensUM import com.tangem.features.managetokens.impl.R import kotlinx.collections.immutable.ImmutableList +private const val CHEVRON_ROTATION_EXPANDED = 180f +private const val CHEVRON_ROTATION_COLLAPSED = 0f + @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.popBack) @@ -48,9 +55,6 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi ) } -private const val CHEVRON_ROTATION_EXPANDED = 180f -private const val CHEVRON_ROTATION_COLLAPSED = 0f - @Composable private fun Currencies(items: ImmutableList, modifier: Modifier = Modifier) { LazyColumn( @@ -62,47 +66,15 @@ private fun Currencies(items: ImmutableList, modifier: Modifier ) { item -> when (item) { is CurrencyItemUM.Basic -> { - ChainRow( + BasicCurrencyItem( modifier = Modifier.fillMaxWidth(), - model = item.model, - action = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = item.onExpandClick, - ) { - val rotation by animateFloatAsState( - targetValue = if (item.isExpanded) { - CHEVRON_ROTATION_EXPANDED - } else { - CHEVRON_ROTATION_COLLAPSED - }, - label = "chevron_rotation", - ) - - Icon( - modifier = Modifier - .rotate(rotation) - .size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - }, + item = item, ) } is CurrencyItemUM.Custom -> { - ChainRow( + CustomCurrencyItem( modifier = Modifier.fillMaxWidth(), - model = item.model, - action = { - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.manage_tokens_remove), - onClick = item.onRemoveClick, - ), - ) - }, + item = item, ) } } @@ -110,6 +82,95 @@ private fun Currencies(items: ImmutableList, modifier: Modifier } } +@Composable +private fun CustomCurrencyItem(item: CurrencyItemUM.Custom, modifier: Modifier = Modifier) { + ChainRow( + modifier = modifier, + model = item.model, + action = { + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.manage_tokens_remove), + onClick = item.onRemoveClick, + ), + ) + }, + ) +} + +@Composable +private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = Modifier) { + val isExpanded = item.networks is NetworksUM.Expanded + + Column(modifier = modifier) { + ChainRow( + model = item.model, + action = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = item.onExpandClick, + ) { + val rotation by animateFloatAsState( + targetValue = if (isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) + + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + }, + ) + + NetworksList( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + networks = item.networks, + currencyId = item.id, + ) + } +} + +@Composable +private fun ColumnScope.NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier, + visible = networks is NetworksUM.Expanded, + label = "networks_visibility", + ) { + Column { + val items = (networks as? NetworksUM.Expanded)?.networks + // to keep items on collapse, and avoid animation cancellation + val rememberedItems = remember(key1 = currencyId) { items } + + (items ?: rememberedItems)?.fastForEachIndexed { index, network -> + ArrowRow( + isLastItem = index == items?.lastIndex, + content = { + BlockchainRow( + model = network.model, + action = { + TangemSwitch( + checked = network.isSelected, + onCheckedChange = network.onSelectedStateChange, + ) + }, + ) + }, + ) + } + } + } +} + // region Preview @Preview(showBackground = true, widthDp = 360, heightDp = 800) @Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) From 21f95cf1b35128f5f609d278ad129ede8cba54b0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 27 Jun 2024 13:12:31 +0400 Subject: [PATCH 161/223] Updated on 2026-08-14 --- .../preview/PreviewManageTokensComponent.kt | 100 ++++++++--- .../managetokens/entity/CurrencyItemUM.kt | 5 +- .../managetokens/entity/ManageTokensUM.kt | 5 + .../managetokens/ui/ManageTokensScreen.kt | 162 ++++++++++++++++-- 4 files changed, 232 insertions(+), 40 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index 9abc1cde2c..d68e6ca5d0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -7,9 +7,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.rows.model.BlockchainRowUM import com.tangem.core.ui.components.rows.model.ChainRowUM -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.entity.CurrencyItemUM import com.tangem.features.managetokens.entity.CurrencyNetworkUM @@ -21,25 +22,55 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import kotlin.random.Random internal class PreviewManageTokensComponent : ManageTokensComponent { - private var items = List(size = 30) { index -> - if (index < 2) { - getCustomItem(index) - } else { - getBasicItem(index) - } - }.toPersistentList() + private val changedItemsIds: MutableSet = mutableSetOf() + + private var items = initItems() private val previewState = MutableStateFlow( value = ManageTokensUM( popBack = {}, items = items, + search = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = ::searchCurrencies, + isActive = false, + onActiveChange = ::toggleSearchBar, + ), + hasChanges = false, + onSaveClick = {}, + onAddCustomToken = {}, ), ) + private fun searchCurrencies(query: String) { + previewState.update { state -> + items = if (query.isBlank()) { + initItems() + } else { + items.filter { currency -> + currency.model.name.contains(query, ignoreCase = true) + }.toPersistentList() + } + + state.copy( + search = state.search.copy(query = query), + items = items, + ) + } + } + + private fun toggleSearchBar(isActive: Boolean) { + previewState.update { state -> + state.copy( + search = state.search.copy(isActive = isActive), + ) + } + } + @Composable override fun Content(modifier: Modifier) { val state by previewState.collectAsState() @@ -50,15 +81,23 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { ) } + private fun initItems() = List(size = 30) { index -> + if (index < 2) { + getCustomItem(index) + } else { + getBasicItem(index) + } + }.toPersistentList() + private fun getCustomItem(index: Int) = CurrencyItemUM.Custom( id = index.toString(), model = ChainRowUM( - name = stringReference("Custom token $index"), - type = stringReference("CT$index"), + name = "Custom token $index", + type = "CT$index", icon = CurrencyIconState.CustomTokenIcon( tint = Color.White, background = Color.Black, - networkBadgeIconResId = R.drawable.img_eth_22, + topBadgeIconResId = R.drawable.img_eth_22, isGrayscale = false, showCustomBadge = true, ), @@ -70,8 +109,8 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { private fun getBasicItem(index: Int) = CurrencyItemUM.Basic( id = index.toString(), model = ChainRowUM( - name = stringReference("Currency $index"), - type = stringReference("C$index"), + name = "Currency $index", + type = "C$index", icon = CurrencyIconState.CoinIcon( url = null, fallbackResId = R.drawable.img_btc_22, @@ -85,18 +124,16 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { ) private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 5) { networkIndex -> - val selectedIndex = Random.nextInt(from = 0, until = 5) - CurrencyNetworkUM( id = networkIndex.toString(), model = BlockchainRowUM( - name = stringReference("NETWORK$networkIndex"), - type = stringReference("N$networkIndex"), - icon = CurrencyIconState.Locked, - isAccented = networkIndex == 0, - usePrimaryTextColor = networkIndex == selectedIndex, + name = "NETWORK$networkIndex", + type = "N$networkIndex", + iconResId = R.drawable.ic_eth_16, + isMainNetwork = networkIndex == 0, + isSelected = false, ), - isSelected = networkIndex == selectedIndex, + isSelected = false, onSelectedStateChange = { toggleNetwork(currencyIndex, networkIndex, isSelected = it) }, ) }.toImmutableList() @@ -131,7 +168,12 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { if (index == networkIndex) { it[index] = network.copy( model = network.model.copy( - usePrimaryTextColor = isSelected, + iconResId = if (isSelected) { + R.drawable.img_eth_22 + } else { + R.drawable.ic_eth_16 + }, + isSelected = isSelected, ), isSelected = isSelected, ) @@ -146,11 +188,21 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { is CurrencyItemUM.Custom -> return } + val id = "${currencyIndex}_$networkIndex" + if (changedItemsIds.contains(id)) { + changedItemsIds.remove(id) + } else { + changedItemsIds.add(id) + } + previewState.update { state -> items = items.mutate { it[currencyIndex] = updatedItem } - state.copy(items = items) + state.copy( + items = items, + hasChanges = changedItemsIds.isNotEmpty(), + ) } } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt index 3cd78d349a..0cfc505545 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/CurrencyItemUM.kt @@ -8,10 +8,11 @@ import kotlinx.collections.immutable.ImmutableList internal sealed class CurrencyItemUM { abstract val id: String + abstract val model: ChainRowUM data class Basic( override val id: String, - val model: ChainRowUM, + override val model: ChainRowUM, val networks: NetworksUM, val onExpandClick: () -> Unit, ) : CurrencyItemUM() { @@ -29,7 +30,7 @@ internal sealed class CurrencyItemUM { data class Custom( override val id: String, - val model: ChainRowUM, + override val model: ChainRowUM, val onRemoveClick: () -> Unit, ) : CurrencyItemUM() } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt index 3bd1757c82..c8678bdd38 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/ManageTokensUM.kt @@ -1,10 +1,15 @@ package com.tangem.features.managetokens.entity import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM import kotlinx.collections.immutable.ImmutableList @Immutable internal data class ManageTokensUM( val popBack: () -> Unit, val items: ImmutableList, + val search: SearchBarUM, + val hasChanges: Boolean, + val onAddCustomToken: () -> Unit, + val onSaveClick: () -> Unit, ) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index af11b22917..462b1e2e79 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -2,25 +2,33 @@ package com.tangem.features.managetokens.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.appbar.models.TangemTopAppBarColors import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.rows.ArrowRow import com.tangem.core.ui.components.rows.BlockchainRow import com.tangem.core.ui.components.rows.ChainRow @@ -37,29 +45,151 @@ import kotlinx.collections.immutable.ImmutableList private const val CHEVRON_ROTATION_EXPANDED = 180f private const val CHEVRON_ROTATION_COLLAPSED = 0f +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + BackHandler(onBack = state.popBack) Scaffold( - modifier = modifier, + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = TangemTheme.colors.background.primary, + topBar = { + TopBar( + scrollBehavior = scrollBehavior, + onAddCustomToken = state.onAddCustomToken, + onPopBack = state.popBack, + ) + }, content = { innerPadding -> - Currencies( + Content( modifier = Modifier .padding(innerPadding) .fillMaxSize(), - items = state.items, + state = state, + ) + }, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + SaveChangesButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + isVisible = state.hasChanges, + onClick = state.onSaveClick, ) }, ) } @Composable -private fun Currencies(items: ImmutableList, modifier: Modifier = Modifier) { +@OptIn(ExperimentalMaterial3Api::class) +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + onPopBack: () -> Unit, + onAddCustomToken: () -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = TangemTopAppBarColors.copy( + containerColor = TangemTheme.colors.background.primary, + scrolledContainerColor = TangemTheme.colors.background.primary, + ), + navigationIcon = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onPopBack, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + ) + } + }, + title = { + Text( + text = stringResource(id = R.string.main_manage_tokens), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + }, + actions = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onAddCustomToken, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_plus_24), + contentDescription = null, + ) + } + }, + ) +} + +@Composable +private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier, + visible = isVisible, + enter = fadeIn(), + exit = fadeOut(), + label = "save_button_visibility", + ) { + PrimaryButtonIconEnd( + text = stringResource(id = R.string.common_save), + iconResId = R.drawable.ic_tangem_24, + onClick = onClick, + ) + } +} + +@Composable +private fun Content(state: ManageTokensUM, modifier: Modifier = Modifier) { + Box(modifier = modifier) { + Currencies( + modifier = Modifier.fillMaxSize(), + items = state.items, + search = state.search, + ) + + AnimatedVisibility( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth(), + visible = state.hasChanges, + label = "bottom_fade_visibility", + ) { + BottomFade() + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun Currencies(items: ImmutableList, search: SearchBarUM, modifier: Modifier = Modifier) { LazyColumn( modifier = modifier, ) { + stickyHeader(key = "search") { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) { + SearchBar(state = search) + } + } + items( items = items, key = CurrencyItemUM::id, @@ -132,7 +262,7 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M ) NetworksList( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), networks = item.networks, currencyId = item.id, ) @@ -140,20 +270,24 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M } @Composable -private fun ColumnScope.NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) { +private fun NetworksList(networks: NetworksUM, currencyId: String, modifier: Modifier = Modifier) { AnimatedVisibility( modifier = modifier, visible = networks is NetworksUM.Expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), label = "networks_visibility", ) { Column { val items = (networks as? NetworksUM.Expanded)?.networks - // to keep items on collapse, and avoid animation cancellation - val rememberedItems = remember(key1 = currencyId) { items } - (items ?: rememberedItems)?.fastForEachIndexed { index, network -> + // To keep items on collapse and avoid animation cancellation + val rememberedItems = remember(key1 = currencyId) { items } + val currentItems = items ?: rememberedItems + + currentItems?.fastForEachIndexed { index, network -> ArrowRow( - isLastItem = index == items?.lastIndex, + isLastItem = index == currentItems.lastIndex, content = { BlockchainRow( model = network.model, From 2c8b85a34ff3d7c135443229c9b21f9447c90c8a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 11 Jul 2024 13:14:52 +0400 Subject: [PATCH 162/223] Updated on 2026-08-14 --- .../managetokens/ui/ManageTokensScreen.kt | 75 ++++--------------- 1 file changed, 15 insertions(+), 60 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 462b1e2e79..bfe586ebd9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -9,14 +9,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.* +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -24,7 +26,8 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.TangemSwitch -import com.tangem.core.ui.components.appbar.models.TangemTopAppBarColors +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.fields.SearchBar @@ -45,21 +48,22 @@ import kotlinx.collections.immutable.ImmutableList private const val CHEVRON_ROTATION_EXPANDED = 180f private const val CHEVRON_ROTATION_COLLAPSED = 0f -@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modifier) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() - BackHandler(onBack = state.popBack) Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = modifier, containerColor = TangemTheme.colors.background.primary, topBar = { - TopBar( - scrollBehavior = scrollBehavior, - onAddCustomToken = state.onAddCustomToken, - onPopBack = state.popBack, + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + title = stringResource(id = R.string.main_manage_tokens), + startButton = TopAppBarButtonUM.Back(state.popBack), + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_plus_24, + onIconClicked = state.onAddCustomToken, + ), ) }, content = { innerPadding -> @@ -83,55 +87,6 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi ) } -@Composable -@OptIn(ExperimentalMaterial3Api::class) -private fun TopBar( - scrollBehavior: TopAppBarScrollBehavior, - onPopBack: () -> Unit, - onAddCustomToken: () -> Unit, - modifier: Modifier = Modifier, -) { - TopAppBar( - modifier = modifier, - scrollBehavior = scrollBehavior, - colors = TangemTopAppBarColors.copy( - containerColor = TangemTheme.colors.background.primary, - scrolledContainerColor = TangemTheme.colors.background.primary, - ), - navigationIcon = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onPopBack, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - ) - } - }, - title = { - Text( - text = stringResource(id = R.string.main_manage_tokens), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - }, - actions = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onAddCustomToken, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_plus_24), - contentDescription = null, - ) - } - }, - ) -} - @Composable private fun SaveChangesButton(isVisible: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { AnimatedVisibility( From e416e2bb5b02618fc63d67b1b780cfea12fca209 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 15:06:47 +0400 Subject: [PATCH 163/223] Updated on 2026-08-14 --- .../core/ui/components/rows/BlockchainRow.kt | 2 +- .../core/ui/components/rows/ChainRow.kt | 26 +++++++------------ .../preview/PreviewManageTokensComponent.kt | 8 ++++-- .../managetokens/ui/ManageTokensScreen.kt | 5 +++- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt index d0341755a7..4f8edf75c4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -29,7 +29,7 @@ fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Uni modifier = modifier .heightIn(min = TangemTheme.dimens.size52) .padding( - vertical = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing8, ), icon = { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt index 175ace75f5..c57b6ebc1a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -2,16 +2,17 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource 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.R +import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.rows.model.ChainRowUM @@ -27,9 +28,10 @@ fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composab RowContentContainer( modifier = modifier .heightIn(min = TangemTheme.dimens.size68) + .padding(vertical = TangemTheme.dimens.spacing8) .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing14, + start = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing12, ), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), icon = { @@ -65,17 +67,7 @@ private fun Preview_ChainRow(@PreviewParameter(ChainRowParameterProvider::class) modifier = Modifier.background(TangemTheme.colors.background.primary), model = state, action = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = { /* [REDACTED_TODO_COMMENT]*/ }, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.informative, - painter = painterResource(id = R.drawable.ic_chevron_24), - contentDescription = null, - ) - } + TangemSwitch(onCheckedChange = {}, checked = false) }, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index d68e6ca5d0..8fabf7cfd4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -119,11 +119,15 @@ internal class PreviewManageTokensComponent : ManageTokensComponent { ), showCustom = false, ), - networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, + networks = if (index == 2) { + CurrencyItemUM.Basic.NetworksUM.Expanded(getCurrencyNetworks(index)) + } else { + CurrencyItemUM.Basic.NetworksUM.Collapsed + }, onExpandClick = { toggleCurrency(index) }, ) - private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 5) { networkIndex -> + private fun getCurrencyNetworks(currencyIndex: Int) = List(size = 3) { networkIndex -> CurrencyNetworkUM( id = networkIndex.toString(), model = BlockchainRowUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index bfe586ebd9..5a29f66f24 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -217,7 +217,10 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M ) NetworksList( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing10, + end = TangemTheme.dimens.spacing8, + ), networks = item.networks, currencyId = item.id, ) From 9cbe154b8efc18fdff4a815679fb2d36fe6da6ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 16:32:18 +0400 Subject: [PATCH 164/223] Updated on 2026-08-14 --- .../core/ui/components/fields/SearchBar.kt | 49 +++++++++++++++++-- .../managetokens/ui/ManageTokensScreen.kt | 5 +- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index cf2abf3f5f..3de373a0e3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -1,13 +1,17 @@ package com.tangem.core.ui.components.fields import android.content.res.Configuration +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.focus.onFocusChanged @@ -18,6 +22,7 @@ import androidx.compose.ui.platform.SoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -33,8 +38,9 @@ import com.tangem.core.ui.res.TangemThemePreview fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarColors) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } - TextField( + BasicTextField( modifier = modifier .fillMaxWidth() .heightIn(min = TangemTheme.dimens.size48) @@ -47,6 +53,7 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie }, value = state.query, onValueChange = state.onQueryChange, + interactionSource = interactionSource, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Search, @@ -59,7 +66,43 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie ), singleLine = true, maxLines = 1, - textStyle = TangemTheme.typography.body2, + textStyle = TangemTheme.typography.body2.copy( + color = TangemTheme.colors.text.primary1, + ), + decorationBox = @Composable { innerTextField -> + DecorationBox( + state = state, + innerTextField = innerTextField, + interactionSource = interactionSource, + colors = colors, + focusManager = focusManager, + keyboardController = keyboardController, + ) + }, + ) +} + +@Suppress("LongParameterList") +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun DecorationBox( + state: SearchBarUM, + innerTextField: @Composable () -> Unit, + interactionSource: MutableInteractionSource, + colors: TextFieldColors, + focusManager: FocusManager, + keyboardController: SoftwareKeyboardController?, +) { + TextFieldDefaults.DecorationBox( + value = state.query, + innerTextField = innerTextField, + enabled = true, + singleLine = true, + visualTransformation = VisualTransformation.None, + interactionSource = interactionSource, + shape = TangemTheme.shapes.roundedCornersXLarge, + colors = colors, + contentPadding = PaddingValues(all = TangemTheme.dimens.spacing12), leadingIcon = { Icon( modifier = Modifier.size(TangemTheme.dimens.size20), @@ -84,8 +127,6 @@ fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFie overflow = TextOverflow.Ellipsis, ) }, - shape = TangemTheme.shapes.roundedCornersXLarge, - colors = colors, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 5a29f66f24..4f9b2f3874 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -136,9 +136,10 @@ private fun Currencies(items: ImmutableList, search: SearchBarUM modifier = Modifier .background(TangemTheme.colors.background.primary) .padding( - vertical = TangemTheme.dimens.spacing12, - horizontal = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing12, ) + .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), ) { SearchBar(state = search) From adddbad4ae5c2dfe1cb60b362ba62b4694053e5a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 17:54:39 +0400 Subject: [PATCH 165/223] Updated on 2026-08-14 --- .../tangem/core/ui/components/TangemSwitch.kt | 18 ++++----- .../managetokens/ui/ManageTokensScreen.kt | 40 +++++++++---------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index 8e0081a633..7b3c924e95 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -15,6 +15,7 @@ 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.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme @@ -23,6 +24,8 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, checkedColor: Color = TangemTheme.colors.control.checked, uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, @@ -39,23 +42,20 @@ fun TangemSwitch( (if (isChecked) checkedColor else uncheckedColor) .copy(alpha = if (enabled) 1f else .4f) } - val interactionSource = remember { MutableInteractionSource() } Box( - modifier = Modifier + modifier = modifier .clickable( - interactionSource = interactionSource, - indication = null, - enabled = enabled, - ) { - onCheckedChange(!checked) - } - .indication( interactionSource = interactionSource, indication = rememberRipple( bounded = false, color = Color.Transparent, ), + enabled = enabled, + role = Role.Switch, + onClick = { + onCheckedChange(!checked) + }, ), ) { BoxWithConstraints( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 4f9b2f3874..63a14af8d4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -6,12 +6,12 @@ import androidx.compose.animation.* import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -190,30 +190,26 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, modifier: Modifier = M Column(modifier = modifier) { ChainRow( + modifier = Modifier.clickable(onClick = item.onExpandClick), model = item.model, action = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = item.onExpandClick, - ) { - val rotation by animateFloatAsState( - targetValue = if (isExpanded) { - CHEVRON_ROTATION_EXPANDED - } else { - CHEVRON_ROTATION_COLLAPSED - }, - label = "chevron_rotation", - ) + val rotation by animateFloatAsState( + targetValue = if (isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) - Icon( - modifier = Modifier - .rotate(rotation) - .size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) }, ) From b6e042b77d91372d20e954796605972700bee5a9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 18:44:57 +0300 Subject: [PATCH 166/223] Updated on 2026-08-14 --- .../model/error/StakeKitErrorResponse.kt | 17 +++++++++++++++++ .../converters/error/StakeKitErrorConverter.kt | 7 +++++++ .../tangem/domain/staking/model/StakingError.kt | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt index cf62a7d43b..30396de002 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt @@ -9,6 +9,18 @@ class StakeKitErrorResponse( val message: StakeKitErrorMessageDTO? = null, @Json(name = "level") val level: String? = null, // unused + + // 403 + @Json(name = "type") + val type: AccessDeniedErrorTypeDTO? = null, + @Json(name = "code") + val code: String? = null, + @Json(name = "countryCode") + val countryCode: String, + @Json(name = "regionCode") + val regionCode: String? = null, + @Json(name = "tags") + val tags: List? = null, ) data class StakeKitErrorDetailsDTO( @@ -20,6 +32,11 @@ data class StakeKitErrorDetailsDTO( val yieldId: String? = null, ) +enum class AccessDeniedErrorTypeDTO { + @Json(name = "GEO_LOCATION") + GEO_LOCATION, +} + enum class StakeKitErrorMessageDTO { @Json(name = "MissingArgumentsError") MISSING_ARGUMENTS_ERROR, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt index 30020c0708..eccee22fac 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt @@ -1,6 +1,7 @@ package com.tangem.data.staking.converters.error import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.stakekit.models.response.model.error.AccessDeniedErrorTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.domain.staking.model.StakingError @@ -15,6 +16,12 @@ internal class StakeKitErrorConverter( return try { val stakeKitErrorResponse = jsonAdapter.fromJson(value) ?: return StakingError.UnknownError + if (stakeKitErrorResponse.type == AccessDeniedErrorTypeDTO.GEO_LOCATION) { + StakingError.UnavailableDueToGeolocationError( + tags = stakeKitErrorResponse.tags ?: emptyList(), + ) + } + when (stakeKitErrorResponse.message) { StakeKitErrorMessageDTO.MINIMUM_AMOUNT_NOT_REACHED -> StakingError.MinimumAmountNotReachedError( amount = stakeKitErrorResponse.details?.amount ?: "", diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt index 0f1ff03f2b..a604be48ae 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt @@ -84,6 +84,10 @@ sealed class StakingError { data object GRTStakingDisabledLedgerLiveError : StakingError() + data class UnavailableDueToGeolocationError( + val tags: List, + ) : StakingError() + // endregion data class DataError(val cause: Throwable) : StakingError() From da1d75b51dfb76cc7138e3c3fe1042008df3cb47 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 17:52:55 +0400 Subject: [PATCH 167/223] Updated on 2026-08-14 --- .../features/details/utils/UserWalletSaver.kt | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index f1bb1fd398..47ae7ed4f7 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -39,7 +39,7 @@ internal class UserWalletSaver @Inject constructor( suspend fun scanAndSaveUserWallet() = recover( block = { - val response = scanCard() + val response = scanCard() ?: return@recover val userWallet = createUserWallet(response) saveWallet(userWallet) @@ -61,7 +61,7 @@ internal class UserWalletSaver @Inject constructor( is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet) is SaveWalletError.DataError -> { val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { - Error.Unkonwn + Error.Unknown } raise(Error.Message(messageRef)) @@ -73,7 +73,7 @@ internal class UserWalletSaver @Inject constructor( } private suspend fun Raise.selectUserWallet(userWallet: UserWallet) { - withError({ Error.Unkonwn }) { + withError({ Error.Unknown }) { selectWalletUseCase(userWallet.walletId).bind() } @@ -83,20 +83,19 @@ internal class UserWalletSaver @Inject constructor( private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build() - return ensureNotNull(userWallet) { Error.Unkonwn } + return ensureNotNull(userWallet) { Error.Unknown } } - private suspend fun Raise.scanCard(): ScanResponse { + private suspend fun Raise.scanCard(): ScanResponse? { var response: ScanResponse? = null scanCardProcessor.scan( analyticsSource = AnalyticsParam.ScreensSources.Settings, onWalletNotCreated = { - raise(Error.WalletNotCreated) + /* no-op */ }, disclaimerWillShow = { router.pop() - raise(Error.DisclaimerWillShow) }, onSuccess = { response = it @@ -116,22 +115,18 @@ internal class UserWalletSaver @Inject constructor( }, ) - return response!! + return response } sealed class Error { open val message: TextReference? = null - data object WalletNotCreated : Error() - - data object DisclaimerWillShow : Error() - data object Silent : Error() data class Message(override val message: TextReference) : Error() - data object Unkonwn : Error() { + data object Unknown : Error() { override val message: TextReference = resourceReference(R.string.common_unknown_error) } From bccd93464f2a6628c13ef4e9aa1abe6d93d8e33d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 17:53:37 +0400 Subject: [PATCH 168/223] Updated on 2026-08-14 --- .../ui/decompose/ComposableDialogComponent.kt | 2 +- .../wallets/models/UpdateWalletError.kt | 4 +- .../wallets/usecase/RenameWalletUseCase.kt | 30 ++++++------- .../wallets/usecase/UpdateWalletUseCase.kt | 16 +++---- .../impl/DefaultRenameWalletComponent.kt | 20 +++++---- .../preview/PreviewRenameWalletComponent.kt | 2 +- .../model/WalletSettingsModel.kt | 45 +++++++++++++------ .../walletsettings/ui/RenameWalletDialog.kt | 5 +-- 8 files changed, 67 insertions(+), 57 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt index 24eed9ce86..f87116ef80 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.Stable @Stable interface ComposableDialogComponent { - val doOnDismiss: () -> Unit + fun dismiss() @Composable fun Dialog() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt index b15ec332d1..ae3c5bba65 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UpdateWalletError.kt @@ -2,7 +2,7 @@ package com.tangem.domain.wallets.models sealed interface UpdateWalletError { - data object DataError : UpdateWalletError - data object NameAlreadyExists : UpdateWalletError + + data class DataError(val cause: Throwable) : UpdateWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt index e03da26f6b..4206d4854f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -1,11 +1,9 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.left import arrow.core.raise.either -import arrow.core.right -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess +import arrow.core.raise.ensure +import com.tangem.common.CompletionResult import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet @@ -18,19 +16,17 @@ import com.tangem.domain.wallets.models.UserWalletId */ class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { - suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either { - val existingNames = userWalletsListManager.userWalletsSync + suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = + either { + val existingNames = userWalletsListManager.userWalletsSync - if (existingNames.any { it.name == name && it.walletId != userWalletId }) { - return UpdateWalletError.NameAlreadyExists.left() + ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } - - return either { - userWalletsListManager.update(userWalletId) { it.copy(name = name) } - .doOnSuccess { return it.right() } - .doOnFailure { return UpdateWalletError.DataError.left() } - - return UpdateWalletError.DataError.left() - } - } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index b060418b2e..5e9f4eb656 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -1,11 +1,8 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.left import arrow.core.raise.either -import arrow.core.right -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess +import com.tangem.common.CompletionResult import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet @@ -23,13 +20,10 @@ class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListMan suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, - ): Either { - return either { - userWalletsListManager.update(userWalletId, update) - .doOnSuccess { return it.right() } - .doOnFailure { return UpdateWalletError.DataError.left() } - - return UpdateWalletError.DataError.left() + ): Either = either { + when (val result = userWalletsListManager.update(userWalletId, update)) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data } } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt index a4c28c649c..15ab597e72 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -21,18 +21,16 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber internal class DefaultRenameWalletComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: RenameWalletComponent.Params, + @Assisted private val params: RenameWalletComponent.Params, private val renameWalletUseCase: RenameWalletUseCase, ) : RenameWalletComponent, AppComponentContext by context { - private val userWalletId = params.userWalletId private val currentWalletName = params.currentName - override val doOnDismiss: () -> Unit = params.onDismiss - private val stateFlow: MutableStateFlow = MutableStateFlow( value = RenameWalletUM( walletNameValue = TextFieldValue(text = params.currentName), @@ -42,13 +40,17 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( ), ) + override fun dismiss() { + params.onDismiss() + } + @Composable override fun Dialog() { val model by stateFlow.collectAsStateWithLifecycle() RenameWalletDialog( model = model, - onDismiss = doOnDismiss, + onDismiss = ::dismiss, ) } @@ -66,11 +68,13 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() if (maybeError != null) { + Timber.e("Unable to rename wallet: $maybeError") + val message = when (maybeError) { - UpdateWalletError.DataError -> resourceReference( + is UpdateWalletError.DataError -> resourceReference( id = R.string.common_unknown_error, ) - UpdateWalletError.NameAlreadyExists -> resourceReference( + is UpdateWalletError.NameAlreadyExists -> resourceReference( id = R.string.user_wallet_list_rename_popup_error_already_exists, formatArgs = wrappedList(newName), ) @@ -79,7 +83,7 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( messageSender.send(message = SnackbarMessage(message)) } - doOnDismiss() + dismiss() } @AssistedFactory diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt index 4c7837a830..ff42b59689 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt @@ -15,7 +15,7 @@ internal class PreviewRenameWalletComponent : RenameWalletComponent { onConfirm = {}, ) - override val doOnDismiss: () -> Unit = {} + override fun dismiss() {} @Composable override fun Dialog() { diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index e7d755f454..e066301748 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -1,5 +1,6 @@ package com.tangem.feature.walletsettings.model +import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -9,11 +10,13 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.walletsettings.component.WalletSettingsComponent @@ -27,6 +30,7 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -55,15 +59,7 @@ internal class WalletSettingsModel @Inject constructor( getWalletUseCase.invokeFlow(params.userWalletId) .distinctUntilChanged() .onEach { maybeWallet -> - val wallet = maybeWallet.getOrElse { error -> - error( - """ - Failed to get user wallet - |- User wallet ID: $params - |- Cause: $error - """.trimIndent(), - ) - } + val wallet = maybeWallet.getOrNull() ?: return@onEach state.update { value -> value.copy(items = buildItems(wallet, dialogNavigation)) @@ -80,7 +76,28 @@ internal class WalletSettingsModel @Inject constructor( userWalletName = userWallet.name, isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, - forgetWallet = { forgetWallet(userWallet.walletId) }, + forgetWallet = { + messageSender.send( + ContentMessage { onDismiss -> + BasicDialog( + message = stringResource(R.string.user_wallet_list_delete_prompt), + onDismissDialog = onDismiss, + confirmButton = DialogButton( + title = stringResource(R.string.common_delete), + warning = true, + onClick = { + forgetWallet() + onDismiss() + }, + ), + dismissButton = DialogButton( + title = stringResource(R.string.common_cancel), + onClick = onDismiss, + ), + ) + }, + ) + }, ) private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { @@ -92,8 +109,10 @@ internal class WalletSettingsModel @Inject constructor( dialogNavigation.activate(config) } - private fun forgetWallet(userWalletId: UserWalletId) = modelScope.launch { - val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + private fun forgetWallet() = modelScope.launch { + val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { + Timber.e("Unable to delete wallet: $it") + messageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index 62a9f39c72..fa02de24c9 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -24,10 +24,7 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { confirmButton = DialogButton( title = stringResource(id = R.string.common_ok), enabled = model.isNameCorrect, - onClick = { - model.onConfirm() - onDismiss() - }, + onClick = model.onConfirm, ), dismissButton = DialogButton( title = stringResource(id = R.string.common_cancel), From 4354a1f64d3b52d80476e02c4fa4e8d7dc264e7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 17 Jul 2024 18:15:15 +0400 Subject: [PATCH 169/223] Updated on 2026-08-14 --- .../java/com/tangem/core/ui/components/TextFields.kt | 8 ++++++-- .../src/main/java/com/tangem/core/ui/res/TangemTheme.kt | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt index 055eaab6f7..da346fafa1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt @@ -26,8 +26,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=213%3A218&t=TmfD6UBHPg9uYfev-4) @@ -392,7 +392,11 @@ data class TangemTextFieldColors( else -> unfocusedIndicatorColor } return if (enabled) { - animateColorAsState(targetValue, tween(durationMillis = 120)) + animateColorAsState( + targetValue = targetValue, + animationSpec = tween(durationMillis = 120), + label = "IndicatorColor", + ) } else { rememberUpdatedState(targetValue) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 1bd2251a68..300231f9c3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -1,5 +1,7 @@ package com.tangem.core.ui.res +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle @@ -50,6 +52,7 @@ fun TangemTheme( LocalHapticManager provides hapticManager, LocalSnackbarHostState provides snackbarHostState, LocalWindowSize provides windowSize, + LocalTextSelectionColors provides TangemTextSelectionColors, ) { CompositionLocalProvider( LocalTangemShimmer provides TangemShimmer, @@ -205,6 +208,12 @@ private fun darkThemeColors(): TangemColors { ) } +@Stable +private val TangemTextSelectionColors = TextSelectionColors( + handleColor = TangemColorPalette.Azure, + backgroundColor = TangemColorPalette.Azure.copy(alpha = 0.4f), +) + private val LocalTangemColors = staticCompositionLocalOf { error("No TangemColors provided") } From 97ff092d70ae8e3db79cbde5068131c2cfc59035 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 19:05:50 +0300 Subject: [PATCH 170/223] Updated on 2026-08-14 --- .../middlewares/TradeCryptoMiddleware.kt | 18 ++++++++++++++ .../domain/tokens/legacy/TradeCryptoAction.kt | 8 +++++++ .../viewmodels/TokenDetailsViewModel.kt | 24 +++++++++++++------ .../WalletCurrencyActionsClickIntents.kt | 24 ++++++++++++++++--- 4 files changed, 64 insertions(+), 10 deletions(-) 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 a4233d6ea8..0f96c671b9 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 @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkAddress @@ -56,6 +57,11 @@ object TradeCryptoMiddleware { is TradeCryptoAction.Buy -> proceedBuyAction(state, action) is TradeCryptoAction.Sell -> proceedSellAction(action) is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency) + is TradeCryptoAction.Stake -> openStaking( + userWalletId = action.userWalletId, + cryptoCurrencyId = action.cryptoCurrencyId, + yield = action.yield, + ) is TradeCryptoAction.SendToken -> { if (isSendRedesignedEnabled) { handleNewSendToken(action = action) @@ -159,6 +165,18 @@ object TradeCryptoMiddleware { store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) } } + private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) { + store.dispatchNavigationAction { + push( + AppRoute.Staking( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrencyId, + yield = yield, + ), + ) + } + } + private fun handleSendToken(action: TradeCryptoAction.SendToken) { val currency = action.tokenCurrency val blockchain = Blockchain.fromId(currency.network.id.value) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 092ad6d3ba..92a29e7a39 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,8 +1,10 @@ package com.tangem.domain.tokens.legacy +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import org.rekotlin.Action import java.math.BigDecimal @@ -40,6 +42,12 @@ sealed class TradeCryptoAction : Action { data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() + data class Stake( + val userWalletId: UserWalletId, + val cryptoCurrencyId: CryptoCurrency.ID, + val yield: Yield, + ) : TradeCryptoAction() + data class TransactionInfo( val transactionId: String, val destinationAddress: String, 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 4c32037a24..6bdc2a3375 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 @@ -453,12 +453,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onStakeBannerClick() { - viewModelScope.launch { - val yield = getYieldUseCase.invoke(cryptoCurrency.id, cryptoCurrency.symbol).getOrNull() - yield ?: error("Staking is unavailable") - - router.openStaking(userWalletId, cryptoCurrency, yield) - } + openStaking() } override fun onReloadClick() { @@ -559,7 +554,9 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) { - Timber.e("Not implemented yet") + if (handleUnavailabilityReason(unavailabilityReason)) return + + openStaking() } override fun onGenerateExtendedKey() { @@ -835,6 +832,19 @@ internal class TokenDetailsViewModel @Inject constructor( return true } + private fun openStaking() { + viewModelScope.launch { + val yield = getYieldUseCase.invoke( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ).getOrElse { + error("Staking is unavailable for ${cryptoCurrency.name}") + } + + router.openStaking(userWalletId, cryptoCurrency, yield) + } + } + private companion object { const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 55f1538573..c6b5eb1cff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.clipboard.ClipboardManager @@ -18,6 +19,7 @@ import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency @@ -44,7 +46,6 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -92,6 +93,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val reduxStateHolder: ReduxStateHolder, private val hapticManager: HapticManager, private val clipboardManager: ClipboardManager, + private val getYieldUseCase: GetYieldUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -368,8 +370,24 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { - // TODO staking - Timber.e("Not implemented yet") + viewModelScope.launch { + val userWalletId = stateHolder.getSelectedWalletId() + val cryptoCurrency = cryptoCurrencyStatus.currency + val yield = getYieldUseCase.invoke( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ).getOrElse { + error("Staking is unavailable for ${cryptoCurrency.name}") + } + + reduxStateHolder.dispatch( + TradeCryptoAction.Stake( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrency.id, + yield = yield, + ), + ) + } } private fun openExplorer() { From 7af70694cadc8fd63f04af9bb14721446b83daa3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Jul 2024 12:10:24 +0300 Subject: [PATCH 171/223] Updated on 2026-08-14 --- .../redux/middlewares/TradeCryptoMiddleware.kt | 2 +- .../kotlin/com/tangem/common/routing/AppRoute.kt | 2 +- core/datasource/build.gradle.kts | 2 +- .../stakekit/models/request/ActionRequestBody.kt | 2 +- .../data/staking/DefaultStakingErrorResolver.kt | 2 +- .../data/staking/DefaultStakingRepository.kt | 15 +++++++++------ .../converters/StakingNetworkTypeConverter.kt | 2 +- .../data/staking/converters/TokenConverter.kt | 2 +- .../staking/converters/YieldBalanceConverter.kt | 8 ++++---- .../converters/YieldBalanceListConverter.kt | 2 +- .../data/staking/converters/YieldConverter.kt | 3 ++- .../converters/action/ActionStatusConverter.kt | 2 +- .../action/EnterActionResponseConverter.kt | 2 +- .../converters/action/PendingActionConverter.kt | 2 +- .../action/StakingActionTypeConverter.kt | 2 +- .../converters/error/StakeKitErrorConverter.kt | 2 +- .../transaction/GasEstimateConverter.kt | 2 +- .../transaction/StakingTransactionConverter.kt | 2 +- .../StakingTransactionStatusConverter.kt | 2 +- .../StakingTransactionTypeConverter.kt | 2 +- .../domain/staking/model/StakingAvailability.kt | 0 .../domain/staking/model/StakingEntryInfo.kt | 0 .../tangem/domain/staking/model/StakingToken.kt | 0 .../domain/staking/model/StakingTokenWithYield.kt | 0 .../model/UnsubmittedTransactionMetadata.kt | 0 .../staking/model/{ => stakekit}/NetworkType.kt | 2 +- .../staking/model/{ => stakekit}/StakingError.kt | 2 +- .../domain/staking/model/{ => stakekit}/Yield.kt | 2 +- .../staking/model/{ => stakekit}/YieldBalance.kt | 4 ++-- .../model/{ => stakekit}/YieldBalanceList.kt | 2 +- .../model/{ => stakekit}/action/StakingAction.kt | 4 ++-- .../action/StakingActionCommonType.kt | 2 +- .../{ => stakekit}/action/StakingActionStatus.kt | 2 +- .../{ => stakekit}/action/StakingActionType.kt | 2 +- .../{ => stakekit}/transaction/ActionParams.kt | 8 ++++---- .../transaction/StakingGasEstimate.kt | 4 ++-- .../transaction/StakingTransaction.kt | 4 ++-- .../transaction/StakingTransactionStatus.kt | 2 +- .../transaction/StakingTransactionType.kt | 2 +- .../tangem/domain/staking/EstimateGasUseCase.kt | 6 +++--- .../domain/staking/FetchStakingTokensUseCase.kt | 2 +- .../staking/FetchStakingYieldBalanceUseCase.kt | 2 +- .../staking/GetStakingAvailabilityUseCase.kt | 3 +-- .../domain/staking/GetStakingEntryInfoUseCase.kt | 2 +- .../staking/GetStakingTransactionUseCase.kt | 6 +++--- .../staking/GetStakingYieldBalanceUseCase.kt | 4 ++-- .../com/tangem/domain/staking/GetYieldUseCase.kt | 5 ++--- .../domain/staking/IsStakeMoreAvailableUseCase.kt | 2 +- .../domain/staking/SaveUnsubmittedHashUseCase.kt | 2 +- .../staking/SendUnsubmittedHashesUseCase.kt | 2 +- .../tangem/domain/staking/SubmitHashUseCase.kt | 2 +- .../staking/repositories/StakingErrorResolver.kt | 2 +- .../staking/repositories/StakingRepository.kt | 11 +++++++---- .../domain/tokens/model/CryptoCurrencyStatus.kt | 2 +- .../domain/tokens/legacy/TradeCryptoAction.kt | 2 +- .../operations/CurrenciesStatusesLceOperations.kt | 4 ++-- .../operations/CurrenciesStatusesOperations.kt | 4 ++-- .../tokens/operations/CurrencyStatusOperations.kt | 2 +- .../operations/TokenListFiatBalanceOperations.kt | 2 +- .../GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt | 2 +- .../tangem/domain/tokens/mock/MockTokensStates.kt | 2 +- .../tokens/repository/MockStakingRepository.kt | 9 +++++---- .../presentation/state/InnerYieldBalanceState.kt | 2 +- .../impl/presentation/state/ValidatorState.kt | 2 +- .../converters/RewardsValidatorStateConverter.kt | 8 ++++---- .../state/converters/YieldBalancesConverter.kt | 2 +- .../previewdata/ConfirmationStatePreviewData.kt | 2 +- .../previewdata/InitialStakingStatePreview.kt | 2 +- .../state/stub/StakingClickIntentsStub.kt | 2 +- .../SetConfirmationStateAssentTransformer.kt | 2 +- .../SetConfirmationStateCompletedTransformer.kt | 2 +- .../SetConfirmationStateLoadingTransformer.kt | 2 +- .../SetInitialDataStateTransformer.kt | 4 ++-- .../validator/ValidatorSelectChangeTransformer.kt | 2 +- .../presentation/viewmodel/StakingClickIntents.kt | 2 +- .../presentation/viewmodel/StakingViewModel.kt | 8 ++++---- .../router/DefaultTokenDetailsRouter.kt | 2 +- .../router/InnerTokenDetailsRouter.kt | 2 +- .../factory/TokenDetailsLoadedBalanceConverter.kt | 2 +- .../state/factory/TokenDetailsStateFactory.kt | 2 +- .../state/factory/TokenStakingStateConverter.kt | 2 +- .../converter/TokenItemStateConverter.kt | 2 +- 82 files changed, 121 insertions(+), 115 deletions(-) rename domain/staking/{src/main/java => models/src/main/kotlin}/com/tangem/domain/staking/model/StakingAvailability.kt (100%) rename domain/staking/{src/main/java => models/src/main/kotlin}/com/tangem/domain/staking/model/StakingEntryInfo.kt (100%) rename domain/staking/{src/main/java => models/src/main/kotlin}/com/tangem/domain/staking/model/StakingToken.kt (100%) rename domain/staking/{src/main/java => models/src/main/kotlin}/com/tangem/domain/staking/model/StakingTokenWithYield.kt (100%) rename domain/staking/{src/main/java => models/src/main/kotlin}/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt (100%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/NetworkType.kt (94%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/StakingError.kt (98%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/Yield.kt (98%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/YieldBalance.kt (94%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/YieldBalanceList.kt (91%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/action/StakingAction.kt (75%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/action/StakingActionCommonType.kt (56%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/action/StakingActionStatus.kt (71%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/action/StakingActionType.kt (82%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/transaction/ActionParams.kt (54%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/transaction/StakingGasEstimate.kt (54%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/transaction/StakingTransaction.kt (79%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/transaction/StakingTransactionStatus.kt (76%) rename domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/{ => stakekit}/transaction/StakingTransactionType.kt (92%) 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 0f96c671b9..abb2a86125 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 @@ -7,7 +7,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkAddress diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 8a2676a038..bdd1ea3030 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -6,7 +6,7 @@ import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index b706481bfe..0a236d6e5a 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.txhistory.models) - implementation(projects.domain.staking) // TODO staking create staking models module + implementation(projects.domain.staking.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt index fa3ec28913..d6c09517c1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType data class PendingActionRequestBody( @Json(name = "type") diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt index 8fed7f5841..a991a4e1f6 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt @@ -2,7 +2,7 @@ package com.tangem.data.staking import com.tangem.data.staking.converters.error.StakeKitErrorConverter import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver internal class DefaultStakingErrorResolver( diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 646ff8d040..ddb05b9851 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -24,12 +24,15 @@ import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* -import com.tangem.domain.staking.model.action.StakingAction -import com.tangem.domain.staking.model.action.StakingActionCommonType -import com.tangem.domain.staking.model.action.StakingActionType -import com.tangem.domain.staking.model.transaction.ActionParams -import com.tangem.domain.staking.model.transaction.StakingGasEstimate -import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt index a768417427..e3b284f5fc 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO -import com.tangem.domain.staking.model.NetworkType +import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.utils.converter.TwoWayConverter @Suppress("CyclomaticComplexMethod", "LongMethod") diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt index 2148e18511..2f67326a0c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO -import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.stakekit.Token import com.tangem.utils.converter.TwoWayConverter class TokenConverter( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt index c3dd742fbe..c46a15f279 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -2,10 +2,10 @@ package com.tangem.data.staking.converters import com.tangem.data.staking.converters.action.PendingActionConverter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.BalanceItem -import com.tangem.domain.staking.model.BalanceType -import com.tangem.domain.staking.model.YieldBalance -import com.tangem.domain.staking.model.YieldBalanceItem +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceItem import com.tangem.utils.converter.Converter internal class YieldBalanceConverter : Converter { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index 883ba21acd..0476368b30 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.utils.converter.Converter internal class YieldBalanceListConverter : Converter, YieldBalanceList> { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 09e1026127..ddf093c628 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -2,7 +2,8 @@ package com.tangem.data.staking.converters import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.stakekit.AddressArgument +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.utils.converter.Converter class YieldConverter( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt index 360ebbbbf6..4bc04836a3 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.action import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO -import com.tangem.domain.staking.model.action.StakingActionStatus +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.utils.converter.Converter class ActionStatusConverter : Converter { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt index c228248f43..d06ab798d2 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -2,7 +2,7 @@ package com.tangem.data.staking.converters.action import com.tangem.data.staking.converters.transaction.StakingTransactionConverter import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse -import com.tangem.domain.staking.model.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.utils.converter.Converter class EnterActionResponseConverter( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt index d79c4fd101..c9ddee7f77 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.action import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO -import com.tangem.domain.staking.model.PendingAction +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.utils.converter.Converter internal class PendingActionConverter : Converter { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt index 941020c57d..95829958e7 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.action import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO -import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.utils.converter.Converter @Suppress("CyclomaticComplexMethod") diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt index eccee22fac..39aef18e1b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/error/StakeKitErrorConverter.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.JsonAdapter import com.tangem.datasource.api.stakekit.models.response.model.error.AccessDeniedErrorTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorMessageDTO import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.utils.converter.Converter internal class StakeKitErrorConverter( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt index 3219d87359..6220823faf 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt @@ -2,7 +2,7 @@ package com.tangem.data.staking.converters.transaction import com.tangem.data.staking.converters.TokenConverter import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO -import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.utils.converter.Converter class GasEstimateConverter( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt index 1ad9ca3198..ef86c8ef8b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt @@ -2,7 +2,7 @@ package com.tangem.data.staking.converters.transaction import com.tangem.data.staking.converters.StakingNetworkTypeConverter import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO -import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.utils.converter.Converter class StakingTransactionConverter( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt index 9569f61534..236c6e53cb 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.transaction import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO -import com.tangem.domain.staking.model.transaction.StakingTransactionStatus +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionStatus import com.tangem.utils.converter.Converter class StakingTransactionStatusConverter : Converter { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt index 15e156f691..8466b36a37 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.transaction import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO -import com.tangem.domain.staking.model.transaction.StakingTransactionType +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType import com.tangem.utils.converter.Converter @Suppress("CyclomaticComplexMethod") diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt similarity index 100% rename from domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt similarity index 100% rename from domain/staking/src/main/java/com/tangem/domain/staking/model/StakingEntryInfo.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingEntryInfo.kt diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingToken.kt similarity index 100% rename from domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingToken.kt diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingTokenWithYield.kt similarity index 100% rename from domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingTokenWithYield.kt diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt similarity index 100% rename from domain/staking/src/main/java/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/UnsubmittedTransactionMetadata.kt diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt similarity index 94% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt index 4359b0b39c..1d429683bc 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/NetworkType.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model +package com.tangem.domain.staking.model.stakekit enum class NetworkType { AVALANCHE_C, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/StakingError.kt similarity index 98% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/StakingError.kt index a604be48ae..9fc68971f3 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingError.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/StakingError.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model +package com.tangem.domain.staking.model.stakekit sealed class StakingError { diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt similarity index 98% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index 023b7ba52b..e173689c6f 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model +package com.tangem.domain.staking.model.stakekit import com.tangem.domain.core.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt similarity index 94% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index b82ff8bf02..1c521e2ab3 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -1,6 +1,6 @@ -package com.tangem.domain.staking.model +package com.tangem.domain.staking.model.stakekit -import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import java.math.BigDecimal sealed class YieldBalance { diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt similarity index 91% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt index c75d5c9f10..3d3ae126da 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model +package com.tangem.domain.staking.model.stakekit sealed class YieldBalanceList { diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingAction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt similarity index 75% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingAction.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt index 5922003ad7..be1545d6da 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingAction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingAction.kt @@ -1,6 +1,6 @@ -package com.tangem.domain.staking.model.action +package com.tangem.domain.staking.model.stakekit.action -import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import org.joda.time.DateTime import java.math.BigDecimal diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt similarity index 56% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt index 56e96d2caa..1caf4b8942 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionCommonType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model.action +package com.tangem.domain.staking.model.stakekit.action enum class StakingActionCommonType { ENTER, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionStatus.kt similarity index 71% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionStatus.kt index a3e5da8836..33e1750e27 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionStatus.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model.action +package com.tangem.domain.staking.model.stakekit.action enum class StakingActionStatus { CANCELED, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt similarity index 82% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt index 7346957e59..5098ec0ce1 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionType.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model.action +package com.tangem.domain.staking.model.stakekit.action enum class StakingActionType { STAKE, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt similarity index 54% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt index 0d84c4146a..2adfaed13b 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/ActionParams.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/ActionParams.kt @@ -1,8 +1,8 @@ -package com.tangem.domain.staking.model.transaction +package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.Token -import com.tangem.domain.staking.model.action.StakingActionCommonType -import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.Token +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import java.math.BigDecimal data class ActionParams( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt similarity index 54% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt index 292adc35d5..585ae1edb5 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingGasEstimate.kt @@ -1,6 +1,6 @@ -package com.tangem.domain.staking.model.transaction +package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.stakekit.Token import java.math.BigDecimal data class StakingGasEstimate( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt similarity index 79% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt index 0e937d1243..4222203a21 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransaction.kt @@ -1,6 +1,6 @@ -package com.tangem.domain.staking.model.transaction +package com.tangem.domain.staking.model.stakekit.transaction -import com.tangem.domain.staking.model.NetworkType +import com.tangem.domain.staking.model.stakekit.NetworkType data class StakingTransaction( val id: String, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransactionStatus.kt similarity index 76% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransactionStatus.kt index 9fc6d4464f..642276c7ca 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransactionStatus.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model.transaction +package com.tangem.domain.staking.model.stakekit.transaction enum class StakingTransactionStatus { NOT_FOUND, diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransactionType.kt similarity index 92% rename from domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt rename to domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransactionType.kt index cb52db3b39..34caa67d0f 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/transaction/StakingTransactionType.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.model.transaction +package com.tangem.domain.staking.model.stakekit.transaction enum class StakingTransactionType { SWAP, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt index bbbac8f73f..381308b7a7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/EstimateGasUseCase.kt @@ -1,9 +1,9 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.StakingError -import com.tangem.domain.staking.model.transaction.ActionParams -import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt index 24f9b37b0e..c591ae2bc2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.domain.staking import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index a2ff4120c1..333762d0e1 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.domain.staking import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyAddress diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 9000f0af29..46a2f010be 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -1,9 +1,8 @@ package com.tangem.domain.staking import arrow.core.Either -import arrow.core.raise.catch import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt index 451284a714..6192e9d92a 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt @@ -2,7 +2,7 @@ package com.tangem.domain.staking import arrow.core.Either import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt index 2c843b9161..02abd643a4 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingTransactionUseCase.kt @@ -1,9 +1,9 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.StakingError -import com.tangem.domain.staking.model.transaction.ActionParams -import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import kotlinx.coroutines.delay diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt index 77ac00f093..5c5528b931 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -4,8 +4,8 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.model.StakingError -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyAddress diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt index 92d0d6e3c4..3bf59574a6 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt @@ -1,9 +1,8 @@ package com.tangem.domain.staking import arrow.core.Either -import arrow.core.raise.catch -import com.tangem.domain.staking.model.StakingError -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt index 30e148dccd..4dd7fc8f38 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/IsStakeMoreAvailableUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.Network diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt index 4e7e5efedc..0a83c395cb 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SaveUnsubmittedHashUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt index 6a83ee4624..95f3ad9eea 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SendUnsubmittedHashesUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt index 8fd4ce8d18..d810cfac8c 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/SubmitHashUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt index c08fdb199d..f45ad158d8 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingErrorResolver.kt @@ -1,6 +1,6 @@ package com.tangem.domain.staking.repositories -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.stakekit.StakingError interface StakingErrorResolver { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index a374a30d09..356dc7aac9 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,10 +2,13 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.* -import com.tangem.domain.staking.model.action.StakingAction -import com.tangem.domain.staking.model.transaction.ActionParams -import com.tangem.domain.staking.model.transaction.StakingGasEstimate -import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index cda5592ee6..d6db9ae7e0 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.model -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 92a29e7a39..0b04bf321b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,6 +1,6 @@ package com.tangem.domain.tokens.legacy -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 0a542d0874..8dee89d0e6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -7,8 +7,8 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading -import com.tangem.domain.staking.model.YieldBalance -import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* 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 a1c6c428a3..34e8a290cb 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 @@ -3,8 +3,8 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.model.YieldBalance -import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository 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 ee8fa082a7..380f77dc4f 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,6 +1,6 @@ package com.tangem.domain.tokens.operations -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.* import java.math.BigDecimal 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 8f28b74ecc..e446ca55aa 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,7 +1,7 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.utils.extensions.orZero diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 4f52744205..824a8e89c2 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -4,7 +4,7 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes 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 e3f88d9b52..78858d98c1 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,7 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 68589655af..6116e0afda 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -3,10 +3,11 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.* -import com.tangem.domain.staking.model.action.StakingAction -import com.tangem.domain.staking.model.action.StakingActionStatus -import com.tangem.domain.staking.model.action.StakingActionType -import com.tangem.domain.staking.model.transaction.* +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.transaction.* import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index 4d5cfcff8a..b5a01e4cf9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt index 0510d12380..7871149443 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield @Immutable internal sealed class ValidatorState { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index e05f2dfcdf..c162e9df1f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -4,10 +4,10 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.BalanceItem -import com.tangem.domain.staking.model.BalanceType -import com.tangem.domain.staking.model.Yield -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.BalanceItem +import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index f6d436f3c3..dbecbf6274 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceGroupType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 3d1e0fcebf..9bb95df370 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType.Coin import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 2c9ac37b88..ce4bd5f41a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.* import kotlinx.collections.immutable.persistentListOf diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index e9a6ec4c62..5c29ffcb0e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.stub -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index df4fc57ae3..7c540291b5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index f83c870fc6..fa8af0041e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -3,7 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 0b12d8b8f8..382738bb2f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index c001927cd1..c361dcf147 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -10,8 +10,8 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.Yield -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.R diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index 714a89de67..273108e5b5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -1,6 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.validator -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.ValidatorState diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 05d121258a..e2cb8c86da 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,7 +1,7 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index c2b477f54b..0e182f302b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -15,10 +15,10 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.* -import com.tangem.domain.staking.model.Yield -import com.tangem.domain.staking.model.action.StakingActionCommonType -import com.tangem.domain.staking.model.transaction.ActionParams -import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.transaction.ActionParams +import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus 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 c2ddee4828..30a598c8df 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 @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment 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 index 7060190ec5..b7c55e065b 100644 --- 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 @@ -1,6 +1,6 @@ package com.tangem.feature.tokendetails.presentation.router -import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.tokendetails.navigation.TokenDetailsRouter diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 5f47f33ef5..b5b0f8a5c6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 512875fb4b..3d6080faba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -19,7 +19,7 @@ import com.tangem.domain.card.NetworkHasDerivationUseCase import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 6f3e41632f..3bc3e36023 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.StakingError +import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index 32651bb309..d7639404cc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents From b0d9d81348cdcf3719e933fa3244ae3b252602d9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 20:52:46 +0500 Subject: [PATCH 172/223] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 17 +++++++++++++++-- .../tap/di/domain/SettingsDomainModule.kt | 8 ++++++++ .../settings/DefaultPermissionRepository.kt | 15 +++++++++++---- .../NeverToInitiallyAskPermissionUseCase.kt | 12 ++++++++++++ .../repositories/PermissionRepository.kt | 5 +++++ .../viewmodel/PushNotificationViewModel.kt | 5 +++++ .../wallet/state/WalletStateController.kt | 11 +++++++++-- .../model/PushNotificationsBottomSheetConfig.kt | 3 ++- .../components/PushNotificationsBottomSheet.kt | 11 ++++++++--- .../wallet/viewmodels/WalletViewModel.kt | 14 ++++++++------ .../intents/WalletPushPermissionClickIntents.kt | 12 ++++++++++-- 11 files changed, 93 insertions(+), 20 deletions(-) create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/NeverToInitiallyAskPermissionUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index fd2967ed70..20808b8396 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -43,6 +43,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase @@ -51,6 +52,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter @@ -188,6 +190,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var cardRepository: CardRepository + @Inject + lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -531,8 +536,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } else { lifecycleScope.launch { val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) - val isEnabled = toggles.isPushNotificationsEnabled && !cardRepository.isTangemTOSAccepted() - val route = if (isEnabled) AppRoute.Disclaimer(isTosAccepted = false) else AppRoute.Home + val isPushPermissionEnabled = toggles.isPushNotificationsEnabled + val shouldShowTos = !cardRepository.isTangemTOSAccepted() && isPushPermissionEnabled + val wasPushInitiallyAsked = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false } + val shouldShowInitialPush = wasPushInitiallyAsked && isPushPermissionEnabled + + val route = when { + shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false) + shouldShowInitialPush -> AppRoute.PushNotification + else -> AppRoute.Home + } store.dispatchNavigationAction { replaceAll(route) } intentProcessor.handleIntent(intentWhichStartedActivity, false) 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 index 90cc3380f0..7ff162a0d1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -176,6 +176,14 @@ internal object SettingsDomainModule { return ShouldInitiallyAskPermissionUseCase(repository = permissionRepository) } + @Provides + @Singleton + fun provideNeverToInitiallyAskPermissionUseCase( + permissionRepository: PermissionRepository, + ): NeverToInitiallyAskPermissionUseCase { + return NeverToInitiallyAskPermissionUseCase(repository = permissionRepository) + } + @Provides @Singleton fun provideIsFirstTimeAskingPermissionUseCase( diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt index b73d5d069f..6497ea59b4 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt @@ -17,10 +17,17 @@ internal class DefaultPermissionRepository( ) : PermissionRepository { override suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean { - val key = getShouldShowInitialPermissionScreen(permission) - val initialPermissionScreen = appPreferencesStore.getSyncOrDefault(key = key, default = true) - if (initialPermissionScreen) appPreferencesStore.store(key = key, value = false) - return initialPermissionScreen + return appPreferencesStore.getSyncOrDefault( + key = getShouldShowInitialPermissionScreen(permission), + default = true, + ) + } + + override suspend fun neverInitiallyShowPermissionScreen(permission: String) { + appPreferencesStore.store( + key = getShouldShowInitialPermissionScreen(permission), + value = false, + ) } override suspend fun isFirstTimeAskingPermission(permission: String): Boolean = diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverToInitiallyAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToInitiallyAskPermissionUseCase.kt new file mode 100644 index 0000000000..02baad3a02 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverToInitiallyAskPermissionUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.PermissionRepository + +class NeverToInitiallyAskPermissionUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: String) { + repository.neverInitiallyShowPermissionScreen(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt index fd136cf651..721bfe90be 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt @@ -8,6 +8,11 @@ interface PermissionRepository { */ suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean + /** + * Sets value indicating that screen for [permission] was shown in initial app launch + */ + suspend fun neverInitiallyShowPermissionScreen(permission: String) + /** * Indicates which time [permission] was asked via platform dialog. * NOTE: Use this method to indicate either reroute to settings or display platform dialog. diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt index 6653043296..20a8932b43 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter @@ -12,11 +13,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class PushNotificationViewModel @Inject constructor( private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val router: DefaultPushNotificationsRouter, private val settingsManager: SettingsManager, ) : ViewModel(), PushNotificationsClickIntents { @@ -25,6 +28,7 @@ internal class PushNotificationViewModel @Inject constructor( viewModelScope.launch { delayPermissionRequestUseCase(PUSH_PERMISSION) setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) } router.openHome() } @@ -38,6 +42,7 @@ internal class PushNotificationViewModel @Inject constructor( override fun onAllowedPermission() { viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) } router.openHome() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 713d704e94..4f4e7c869f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -65,12 +65,19 @@ internal class WalletStateController @Inject constructor() { return with(value) { wallets[selectedWalletIndex].walletCardState.id } } - fun showBottomSheet(content: TangemBottomSheetConfigContent, userWalletId: UserWalletId = getSelectedWalletId()) { + fun showBottomSheet( + content: TangemBottomSheetConfigContent, + userWalletId: UserWalletId = getSelectedWalletId(), + onDismiss: (() -> Unit)? = null, + ) { update( OpenBottomSheetTransformer( userWalletId = userWalletId, content = content, - onDismissBottomSheet = { update(CloseBottomSheetTransformer(userWalletId)) }, + onDismissBottomSheet = { + onDismiss?.invoke() + update(CloseBottomSheetTransformer(userWalletId)) + }, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt index 71638bc6ce..fa0fa3cd6e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt @@ -3,7 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent data class PushNotificationsBottomSheetConfig( - val isFirstTimeAsking: Boolean, + val isFirstTimeRequested: Boolean, + val wasInitiallyAsk: Boolean, val onRequest: () -> Unit, val onAllow: () -> Unit, val onDeny: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index 15035df238..6a37a22f0c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -39,7 +39,7 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( pushPermission = getPushPermissionOrNull(), - isFirstTimeAsking = content.isFirstTimeAsking, + isFirstTimeAsking = content.isFirstTimeRequested, isClicked = isClicked, onAllow = { content.onAllow() @@ -78,7 +78,11 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC ), ) { SecondaryButton( - text = stringResource(R.string.common_later), + text = if (content.wasInitiallyAsk) { + stringResource(R.string.common_later) + } else { + stringResource(R.string.common_cancel) + }, onClick = { content.onDeny() onDismiss() @@ -106,7 +110,8 @@ private fun PushNotificationsSheetContent_Preview() { TangemThemePreview { PushNotificationsSheetContent( PushNotificationsBottomSheetConfig( - isFirstTimeAsking = false, + isFirstTimeRequested = false, + wasInitiallyAsk = false, onRequest = {}, onAllow = {}, onDeny = {}, 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 9c94f8bcdf..585bfc901e 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 @@ -43,7 +43,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @HiltViewModel internal class WalletViewModel @Inject constructor( private val stateHolder: WalletStateController, @@ -157,21 +157,23 @@ internal class WalletViewModel @Inject constructor( delay(timeMillis = 1_800) - val isFirstTimeAsking = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } + val isFirstTimeRequested = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } val onDenyClick: () -> Unit = if (wasInitiallyAsk) { clickIntents::onDelayAskPushPermission } else { - clickIntents::onNeverAskPushPermission + clickIntents::onDenyPushPermission } stateHolder.showBottomSheet( - PushNotificationsBottomSheetConfig( - isFirstTimeAsking = isFirstTimeAsking, + content = PushNotificationsBottomSheetConfig( + isFirstTimeRequested = isFirstTimeRequested, + wasInitiallyAsk = wasInitiallyAsk, onRequest = clickIntents::onRequestPushPermission, - onAllow = clickIntents::onNeverAskPushPermission, + onAllow = clickIntents::onAllowPushPermission, onDeny = onDenyClick, openSettings = settingsManager::openSettings, ), + onDismiss = onDenyClick, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index d2f05b0f2a..398d076811 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -14,7 +14,9 @@ internal interface WalletPushPermissionClickIntents { fun onDelayAskPushPermission() - fun onNeverAskPushPermission() + fun onDenyPushPermission() + + fun onAllowPushPermission() } @ViewModelScoped @@ -36,7 +38,13 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( } } - override fun onNeverAskPushPermission() { + override fun onDenyPushPermission() { + viewModelScope.launch { + neverRequestPermissionUseCase(PUSH_PERMISSION) + } + } + + override fun onAllowPushPermission() { viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) } From 63a8133bd748a430f242cae05b6c5bb77f2b4bdb Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 20:53:39 +0500 Subject: [PATCH 173/223] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 7 +++++++ core/res/src/main/res/values-ru/strings.xml | 3 +++ .../com/tangem/core/ui/components/showcase/ShowcaseItem.kt | 5 ++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aed18d1c66..0740725de0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -147,6 +147,13 @@ android:resource="@xml/provider_paths" /> + + + diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ee19fe0771..701f32fb57 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -657,6 +657,9 @@ Это действие необратимо. У вас не будет доступа к старому кошельку. Приложите twin-карту с номером %s и не убирайте до окончания операции Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку + Будьте в курсе новых функций и новостей + Узнавайте первым о новых акциях + Хотите использовать Push-уведомления? Добавить новый кошелек Вы уверены, что хотите удалить этот кошелек? Произошла ошибка, пожалуйста, отсканируйте свою карту для входа diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt index 824144dbf5..2f098c6720 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.showcase import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -24,7 +25,9 @@ internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) { text = text.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing20), + modifier = Modifier + .fillMaxWidth() + .padding(start = TangemTheme.dimens.spacing20), ) } } \ No newline at end of file From 13bd274bce977a89b3a577a7ce355eafecc69f5c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 20:58:30 +0500 Subject: [PATCH 174/223] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../push-notifications/api/build.gradle.kts | 3 ++ .../PushNotificationAnalyticEvents.kt | 32 +++++++++++++++++++ .../push-notifications/impl/build.gradle.kts | 5 ++- .../viewmodel/PushNotificationViewModel.kt | 10 ++++++ .../WalletPushPermissionClickIntents.kt | 11 +++++++ 6 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index c7c73612f8..093769910b 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -65,6 +65,7 @@ sealed class AnalyticsParam { data object Intro : ScreensSources("Introduction") data object MyWallets : ScreensSources("My Wallets") data object Token : ScreensSources("Token") + data object Stories : ScreensSources("Stories") } sealed class TxSentFrom(val value: String) { diff --git a/features/push-notifications/api/build.gradle.kts b/features/push-notifications/api/build.gradle.kts index 4335917f36..8fd2b5a11b 100644 --- a/features/push-notifications/api/build.gradle.kts +++ b/features/push-notifications/api/build.gradle.kts @@ -12,4 +12,7 @@ dependencies { /** AndroidX */ implementation(deps.androidx.fragment.ktx) + + /** Core */ + implementation(projects.core.analytics.models) } \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt new file mode 100644 index 0000000000..d3c8a94ed5 --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -0,0 +1,32 @@ +package com.tangem.features.pushnotifications.api.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +sealed class PushNotificationAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Push", event = event, params = params) { + + data class ButtonAllow( + val source: AnalyticsParam.ScreensSources, + ) : PushNotificationAnalyticEvents( + event = "Button - Allow", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), + ) + + data class ButtonLater( + val source: AnalyticsParam.ScreensSources, + ) : PushNotificationAnalyticEvents( + event = "Button - Later", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), + ) + + data object ButtonCancel : PushNotificationAnalyticEvents( + event = "Button - Cancel", + ) +} \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index 7cf8d0915d..a9965b5681 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -29,9 +29,8 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) - - /** Common modules */ - implementation(projects.common.routing) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) /** Common modules */ implementation(projects.common.routing) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt index 20a8932b43..fcba5aa01c 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -2,11 +2,14 @@ package com.tangem.features.pushnotifications.impl.presentation.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase +import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter import dagger.hilt.android.lifecycle.HiltViewModel @@ -22,9 +25,13 @@ internal class PushNotificationViewModel @Inject constructor( private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val router: DefaultPushNotificationsRouter, private val settingsManager: SettingsManager, + private val analyticHandler: AnalyticsEventHandler, ) : ViewModel(), PushNotificationsClickIntents { override fun onAskLater() { + analyticHandler.send( + PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories), + ) viewModelScope.launch { delayPermissionRequestUseCase(PUSH_PERMISSION) setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) @@ -40,6 +47,9 @@ internal class PushNotificationViewModel @Inject constructor( } override fun onAllowedPermission() { + analyticHandler.send( + PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), + ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index 398d076811..16994a8a7a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -1,8 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase +import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.launch @@ -24,6 +27,7 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { override fun onRequestPushPermission() { @@ -34,17 +38,24 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( override fun onDelayAskPushPermission() { viewModelScope.launch { + analyticsEventHandler.send( + PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Main), + ) delayPermissionRequestUseCase(PUSH_PERMISSION) } } override fun onDenyPushPermission() { + analyticsEventHandler.send(PushNotificationAnalyticEvents.ButtonCancel) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) } } override fun onAllowPushPermission() { + analyticsEventHandler.send( + PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Main), + ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) } From 082492ee263396076d15dd49c88e2457fb2599ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 16:37:04 +0100 Subject: [PATCH 175/223] Updated on 2026-08-14 --- .../main/res/values-de/strings-blockchain.xml | 14 +- core/res/src/main/res/values-de/strings.xml | 713 ++++++++++++++- .../main/res/values-fr/strings-blockchain.xml | 24 +- core/res/src/main/res/values-fr/strings.xml | 736 +++++++++++++++ .../main/res/values-ja/strings-blockchain.xml | 25 + core/res/src/main/res/values-ja/strings.xml | 864 ++++++++++++++++++ core/res/src/main/res/values-ru/strings.xml | 3 - .../res/values-uk-rUA/strings-blockchain.xml | 25 + .../src/main/res/values-uk-rUA/strings.xml | 827 +++++++++++++++++ core/res/src/main/res/values/strings.xml | 3 - .../TravalaNotificationWithBackground.kt | 216 ----- .../utils/WalletWarningsAnalyticsSender.kt | 4 - .../wallet/state/model/WalletNotification.kt | 29 - .../components/common/WalletNotifications.kt | 7 - gradle/dependencies.toml | 2 +- 15 files changed, 3208 insertions(+), 284 deletions(-) create mode 100644 core/res/src/main/res/values-ja/strings-blockchain.xml create mode 100644 core/res/src/main/res/values-ja/strings.xml create mode 100644 core/res/src/main/res/values-uk-rUA/strings-blockchain.xml create mode 100644 core/res/src/main/res/values-uk-rUA/strings.xml delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml index 82986f8529..adea1fc8ea 100644 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ b/core/res/src/main/res/values-de/strings-blockchain.xml @@ -1,10 +1,18 @@ + Standard + Altbestand Erhalt der Gebühr fehlgeschlagen - Senden Sie Geld an diese Andresse um ein Konto zu erstellen + Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. + Nicht genug Geld für die Transaktion. Bitte lade dein Konto auf. + Es ist ein Fehler aufgetreten. Code: %s. + Um das %1$s -Netzwerk nutzen zu können, must du die Kontoreserve ( %2$s %3$s ) bezahlen, die diesen Betrag auf unbestimmte Zeit sperrt und verbirgt + Das Zielkonto ist nicht aktiv. Sende %s oder mehr, um das Konto zu aktivieren. + Senden Sie Geld an diese Andresse um ein Konto zu erstellen Minimaler Betrag ist %s - Restbestand zu klein + Die Veränderung ist zu gering Falsche Gebühr + Mindestguthaben ist %s Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %s + Gebühr oder mehr sein Unbekannter Fehler Der Betrag geht über die Bilanz hinaus @@ -12,6 +20,6 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Nein, alles senden - Um %s XTZ reduzieren + Um %s XTZ reduziert Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 559c09d33c..3f0832f3fb 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,59 +1,752 @@ + Benutzerdefiniertes Token hinzufügen + Token verwalten + Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. + Hilfe anfordern + Erneut versuchen + Diese Funktion ist im Demomodus deaktiviert + Ursache: %s + Transaktion kann nicht gesendet werden + Das gewählte unterstützt nicht das %1$s Netzwerk + Um die kryptografische Verschlüsselung der %1$s Blockchain zu aktivieren, musst Du die Wallet auf die Werkseinstellungen zurücksetzen. Bitte heb vorher dein Guthaben ab, um sicherzustellen, dass du nichts verlierst, und führe dann den Reset-Vorgang durch. Nach dem Zurücksetzen ist der Zugriff auf die aktuelle Wallet nicht mehr möglich. + Tokens im %1$s -Netzwerk werden von dieser Karte aufgrund einer Firmware-Einschränkung nicht unterstützt. + Hast du Probleme beim Scannen deiner Karte? Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet + Standardgebühr + Aktiviere die Option Standardgebühr, um die Transaktionsgebühren automatisch festzulegen und die Gebührenseite beim Senden von Geldern zu überspringen. Du kannst bei Bedarf jederzeit zu dieser Seite zurückkehren. + Gehe zu den Einstellungen, um die biometrische Authentifizierung in der Tangem-App zu aktivieren + Biometrische Authentifizierung aktivieren + Dadurch werden alle gespeicherten Zugangscodes für die Wallet gelöscht. Jede weitere Operation mit der Wallet erfordert die Übermittlung des Zugangscodes. + Durch das Entfernen der gespeicherten Karte werden alle gespeicherten Wallets und deren Zugangscodes aus der App gelöscht. + Zugangscode speichern + Bei Interaktionen mit deiner Karte wird anstelle des Zugangscodes eine biometrische Authentifizierung abgefragt. + Behalten die Wallet in der App + Aktiviere die Verknüpfung aller Wallets mit der Tangem-App. Die biometrische Authentifizierung ist zum Entsperren der App erforderlich. Das Signieren von Transaktionen erfordert das Antippen deiner Tangem-Karte. + Dunkel + Hell + Systemstandard + Thema + App Einstellungen + Um deine Kontostände ein- oder auszublenden, flippe einfach das Display deines Geräts nach unten, oder schalte es in den Einstellungen aus. + Nicht mehr anzeigen + Verstanden + Guthaben sind ausgeblendet + Bitte Karte scannen + Bitte versuche es in 30 Sekunden erneut oder scanne die Karte + Zu viele Versuche + Du hast die biometrische Authentifizierung auf deinem Telefon deaktiviert und kannst keine Wallets in der App speichern. Um Wallets zu speichern, aktiviere bitte die biometrische Authentifizierung in deinen Telefoneinstellungen. + Backup-Prozess starten + + %d Karte + %d Karten + + Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte zum Zurücksetzen von Zugangscodes auf anderen Karten in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte nicht zurücksetzen kannst. + Ermöglicht die Verwendung dieser Karte zum Zurücksetzen des Zugangscodes auf anderen Karten in dieser Brieftasche + Wiederherstellung des Zugangscodes + Zurücksetzen + Möchtest du das wirklich tun? + Zugangscode ändern + Der Zugangscode wird nur auf dieser Karte geändert + Alle Karten der ausgewählten Wallet wurden auf die Werkseinstellungen zurückgesetzt. Du kannst nun eine neue Wallet aufsetzen. + Komplett zurücksetzen + Möchtest du die nächste Karte in dieser Wallet zurücksetzen? + Karte zurücksetzen + Wir empfehlen dir, den Zurücksetzungsprozess für alle Karten in dieser Wallet abzuschließen + Du hast nicht alle deine Karten zurückgesetzt + Auf Werkseinstellungen zurücksetzen + Sicherheitsmodus + Karteneinstellungen + Zusätzlich zu den Netzwerkgebühren erhebt das Cardano-Netzwerk %1$s ADA bei Transaktionen mit dem Token %2$s + Cardano-Transaktionsanforderungen + Um eine %1$s Transaktion durchzuführen, musst du einen gewissen ADA-Betrag einzahlen, um die Netzwerkgebühr und den ADA-Mindestwert zu decken (5 ADA werden empfohlen). + Nicht genügend ADA für die Tokenübertrag + Du musst einige ADA verwalten, weil du einige Token auf der Cardano-Blockchain besitzt + Nicht genug ADA Akzeptieren + Zugang verweigert + Anwenden + Genehmigung + Genehmigen + Achtung Bilanz: %s - Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an + Saldo + biometrische Authentifizierung + biometrische Daten + Kaufen + Gehe zu %1$s + D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an Abbrechen + Schließen + Weitermachen + Kopieren + Adresse kopieren + Erstellen + Benutzerdefiniert Entfernen + Deaktiviert Erledigt + Aktivieren + Aktiviert Fehler + Erkunden + Transaktionsverlauf einsehen + Explorer Gebühr + Netzwerkgebühren sind Gebühren, die Nutzer für die Verarbeitung und Bestätigung von Transaktionen zahlen. Die Höhe der Gebühren kann von der Überlastung des Netzes, der Größe der Transaktion und der Ausführungspriorität abhängen. %s + Schnell + Markt + Langsam + Geschwindigkeit und Gebühr + Adressen abrufen + Zum Anbieter gehen + Importieren + Später + Gesperrt + Hauptnetz Netzgebühr + Der überwiesene Betrag wird um %1$s (%2$s) gekürzt, um die gewählte Gebührenhöhe zu decken. + Weiter + Nein + Keine Adresse OK + Primär Karte + Passphrase + Einfügen + %1$s-%2$s + Weiterlesen + Empfangen + Ablehnen + Neu laden + Umbenennen + Speichern Änderungen speichern - Absenden - Erfolg + Suchen + Token suchen + Seed phrase + Aktion auswählen + Verkaufen + Senden + Der Server ist nicht verfügbar. Bitte versuche es später erneut. + Teilen + Signieren + Signieren und senden + Staken + Staking + Start + Einreichen + Erfolgt + Unterstützung + Tauschen + Allgemeine Geschäftsbedingungen + Transaktion fehlgeschlagen + Transaktionen + Überweisen + Ich verstehe + Es ist ein Fehler aufgetreten. Bitte versuche es erneut. + Nicht erreichbar + Ja + Vertragsadresse kopiert! + Verfügbare Netzwerke + Token hinzufügen + Vertragsadresse + Vertragsadresse ist ungültig + Bitte wähle das Netzwerk + Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %li + Benutzerdefinierte Ableitung(derivation) + E. g. m/00\'/0000\'/0\'/0/0 + Benutzerdefinierte Ableitung (Derivation) eingeben + Dezimalstellen + Ableitungspfad (Derivation Path) + Standard + BIP44 Coin Type + Der von dir eingegebene Ableitungspfad(derivation path) ist ungültig + Z.B. USD-Coin + Name + Nicht ausgewählt + Netzwerk + Token-Netzwerk + Du kannst einen Token, der von Tangem nicht unterstützt wird, manuell hinzufügen + Z.B. USDC + Symbol + Token-Symbol + Dieses Token/Netzwerk wurde bereits zu deiner Liste hinzugefügt + Beachte, dass Token von jedem erstellt werden können. Achte darauf, keine Betrugstoken hinzuzufügen, diese können kostenlos sein. + Sei vorsichtig beim Hinzufügen von SCAM-Token, sie können nichts kosten + Beachten, dass Token von jedem erstellt werden können + Kaufe eine Tangem Wallet + Chat Zugangscode - Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. + Du musst den richtigen Zugangscode eingeben, bevor du die Karte scannst. Langes Tippen Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert Passcode - Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben. + Bevor du einen Befehl ausführst, der eine Änderung des Kartenstatus zur Folge hat, musst du den Passcode eingeben. + Empfehlungsprogramm + Flippe den Bildschirm deines Geräts nach unten, um Salden schnell ein- und auszublenden %s Hasch KartenID + Kontakt zum Support + Weitere Karten verknüpfen App Währung - Emittent + Flipp um Guthaben auszublenden + Aussteller Signiert + Gib uns eine Rückmeldung Details + Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk Nutzungsbedingungen + Du hast eine Karte aus einer anderen Wallet verwendet. Tippe auf die Karte, die dieser Wallet zugeordnet ist. + Meine Token + Du hast noch keine Token hinzugefügt. Füge Token über den Markt zum Tausch hinzu + Kann nicht gegen %s ausgetauscht werden + Bereitgestellt von + Status + Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an. + Anbieter wählen + Es ist ein Fehler aufgetreten. Code: %s + Huch! Der Tausch des ausgewählten Paares über den gewählten Anbieter ist vorübergehend nicht möglich. Bitte versuche es später erneut. (Code: %s) + Der gewählte Anbieter ist im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s) + Swaps sind im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s) + Geschätzter Betrag + Getauscht von %s + Besuche die Website des Anbieters, um dein Geld zurückzuerhalten + Fehler beim Vorgang durch Anbieter + Besuche die Website des Anbieters zur Überprüfung + KYC-Überprüfung durch den Anbieter erforderlich + Abgebrochen + Bestätigt + Bestätigen + wird bestätigt... + Getauscht + Tauschen läuft + Austauschen... + Fehlgeschlagen + Einzahlung empfangen + Einzahlung wird erwartet + Warten auf Einzahlung... + Rückerstattet + An dich gesendet + wird an dich versendet... + Gesendet + Daten stammen vom Anbieter. Der geschätzte Betrag kann sich aufgrund der Marktbedingungen ändern. + Status des Tausches + Verifizierung erforderlich + Liste aller Token, die deiner Wallet hinzugefügt wurden + Beste Preise werden abgerufen … + Variabler Zinssatz + Durch die Nutzung der Swap-Funktion erklärst du dich mit den folgenden Bedingungen des Anbieters einverstanden %s + Durch die Nutzung der Swap-Funktionalität erklärst du dich mit des Anbieters %1$s und %2$s einverstanden + Weitere Anbieter werden bald folgen.\nBleib dabei! + Datenschutzbestimmungen + Anbieter + Bester Preis + Verfügbar bis zu %s + Erhältlich bei %s + Für dieses Paar nicht verfügbar + Erlaubnis erforderlich + Nutzungsbedingungen + Keine Token gefunden. Bitte versuche eine andere Anfrage + ID: %s + Transaktions-ID kopiert + Die folgenden Angaben sind freiwillig. Du kannst diese löschen, wenn du sie nicht weitergeben möchtest. + Teile uns mit, welche Funktionen du vermisst, und wir werden versuchen, dir zu helfen. + Bitte sag uns, welche Karte du hast + Hallo Support-Team, + Bitte erzähle uns mehr über dein Problem. Jedes kleine Detail kann helfen. + Meine Vorschläge + Kann eine Karte nicht scannen + Rückmeldung + Feedback zu Tangem + Eine Transaktion kann nicht gesendet werden + Aktuelle Transaktion + Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. + Gib das Genehmigungslimit für das ausgewählte Token an + Betrag %s + Um fortzufahren, erteile %1s Smart Contracts die Berechtigung, dein zu %2s verwenden. + Erlaubnis erteilen + Unbegrenzt + Karte bestellen Karte scannen - Tippen Sie um den Zugangscode zu ändern - Tippen Sie um den Passcode zu ändern + Um den Zugangscode zu ändern, halte die Karte wie oben gezeigt an das Gerät und entferne sie erst am Ende des Vorgangs. + Um den Passcode zu ändern, halte die Karte wie oben gezeigt an das Gerät und entferne sie erst am Ende des Vorgangs. + Um die Wallet zu erstellen, halte die Karte wie oben gezeigt an das Gerät und entferne sie erst am Ende des Vorgangs. + Halte deine Karte Nr. %s des Sets an dein Smartphone Legen Sie die Karte zum Scannen an Tippen um zu signieren - Legen Sie die Karte an - Ein wallet erstellen + Lege die Karte an + Du hast deine biometrischen Daten aktualisiert, scanne deine Karte, um einzutreten + Dein Guthaben sollte höher sein als der Gebührenwert, um eine Überweisung zu tätigen + Unzureichende Mittel + Du hast nicht genug Mana für diese Transaktion. Bitte warte, bis das Mana wieder aufgefüllt ist. Dein Mana-Guthaben beträgt %1$s/%2$s + Nicht genug Mana + Du kannst nur %s übertragen, da das Koinos-Netzwerk ein Mana-Limit vorgibt. + Mana-Limit + Das Koinos-Netzwerk benötigt Mana als Netzwerkgebühr. Du hast %1$s/%2$s Mana + Mana-Level + Um mit der Verfolgung deiner Krypto-Assets und -Transaktionen zu beginnen, füge einen Token hinzu + Token verwalten + Um auf alle Netzwerke zugreifen zu können, musst du die Karte scannen + Scanne deine Karte + Genießen %1$s Servicegebühren auf Swaps über Changelly von Februar %2$s-%3$s + Swap mit Changelly, %s Gebühren + Token + Hinzufügen + Bearbeiten + \nCoinMarketCap + Blockchain, auf der die Kryptowährung ursprünglich erstellt wurde + Natives Netzwerk + Die Verwendung von nicht nativen Netzwerken für Token ermöglicht eine blockchainübergreifende Interoperabilität, sodass Vermögenswerte in verschiedenen dezentralen Anwendungen und Smart Contracts plattformübergreifend genutzt werden können. Dies erfordert jedoch häufig eine Verwahrstelle oder einen intelligenten Vertrag, um den ursprünglichen Vermögenswert sicher zu verwahren, was zu einer Zentralisierung und einem Kontrahentenrisiko führt. + Nicht die ursprüngliche oder primäre Blockchain, in der der Token gehostet wird + Nicht-native Netzwerke + Netzwerke auswählen + Wallet + Dieses Token konnte nicht gefunden werden. Du kannst ihn manuell hinzufügen. + + %1$d von %2$d Wallet + %1$d von %2$d Wallets + + z.B. BTC vertraue ich, hodl muss ich + Der ausgewählte Token ist derzeit nicht für Aktionen innerhalb der Krypto-Wallet verfügbar. Aber keine Sorge, du kannst dein Interesse bekunden, indem du den Token hochstufen. + Hochstimmen + Wallet auswählen + Das Wallet unterstützt nicht mehr als ein Netzwerk + Du musst einen einzigen Zugangscode einrichten, um alle deine Karten zu schützen + Schützen + Du kannst später auf jeder Karte einen individuellen Zugangscode einrichten + Personalisieren + Der Zugangscode kann mit einer verbundenen Karte wiederhergestellt werden. Bewahre nicht alle Karten an einem Ort auf. + Wiederherstellen + Wähle ein beliebiges Wort, eine Phrase oder eine Zahl als Zugangscode + Zugangscode erstellen + Gib deinen Zugangscode ein weiteres Mal ein, um einen Fehler zu vermeiden + Gebe deinen Zugangscode erneut ein + Der Zugangscode muss mindestens 4 Zeichen lang sein + Der eingegebene Zugangscode stimmt nicht mit dem ursprünglichen Zugangscode überein + Bitte wiederhole den Vorgang. Die Karte wird auf Werkseinstellungen zurückgesetzt. + Aktivierungsfehler + Token hinzufügen + Du hast eine Backup-Karte hinzugefügt. Wenn der Backup-Prozess abgeschlossen ist, kannst du keine weiteren Backup-Karten hinzufügen. Wenn du noch eine Karte hast, füge diese zum Backup hinzu. Möchtest du den Backup-Prozess fortsetzen? + Der Sicherungsvorgang ist teilweise abgeschlossen. Du kannst ihn jetzt nicht beenden. + Die Passphrase ist eine fortschrittliche Sicherheitsfunktion, die von Krypto-Wallets verwendet wird. Sie fügt ein zusätzliches Wort oder eine Phrase deiner Wahl zu der bereits bestehenden Wiederherstellungsphrase hinzu, um einen brandneuen Satz von Adressen zu erzeugen. + Hinzufügen einer Sicherungskarte + Scanne die Karte Nr. %d + Jetzt sichern + Scannen der Hauptkarte + Weiter zu meiner Wallet + Abschließen der Sicherung + Krypto empfangen + Primärkarte scannen + Für später überspringen + Wie funktioniert es? + Lass uns alle Schlüssel auf deiner Karte generieren und eine sichere Wallet erstellen + Ein Wallet erstellen + Erstelle eine Wallet + Andere Optionen + Deine Schlüssel(private-keys) werden sicher im Inneren der Karte generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen. + Schlüssel privat generieren + Deine Karte ist aktiviert und einsatzbereit + Erfolgreich! + In diesem Fall musst du ganz von vorne anfangen. + Möchtest du den Aktivierungsprozess abbrechen? + Erste Schritte + Für die Karte, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte zurück und füge sie als Backup hinzu. + Erstellen eines Backups + Lese mehr über die Seed-Phrase + + leer + Schreibe diese %d-Wörter in der unten angegebenen Reihenfolge auf und bewahre sie an einem sicheren und geheimen Ort auf. + + Deine Seed-Phrase + + leer + %d Wörter + + Um deine Wallets zu importieren, gib bitte deineSeed-Phrase in das folgende Feld ein + Seed-Phrase generieren + Wallet importieren + Eine Seed-Phrase ist eine Reihe von Wörtern, mit denen du deine Wallet wiederherstellen kannst. Im Gegensatz zu den von der Karte generierten Schlüsseln sind Seed-Phrasen ungeschützt und können kopiert und gestohlen werden. Die Verwendung dieser Option erfolgt auf eigene Gefahr. + Seed-Phrase verwenden + Ungültige Seed-Phrase. Bitte überprüfe die Wortreihenfolge. + Ungültige Seed-Phrase. Bitte überprüfe die Rechtschreibung. + Altbestand + Um zu überprüfen, ob du deine Seed-Phrase richtig aufgeschrieben hast, gib bitte das 2., 7. und 11 Wort ein. + Also, lass uns das überprüfen + Um den Sicherungsvorgang zu starten, füge bis zu zwei Sicherungskarten hinzu. + Du kannst eine weitere Karte hinzufügen oder den Sicherungsvorgang abschließen + Bereite die Sicherungskarte mit der Nummer %s vor. + Scanne die primär-Karte, um den Sicherungsvorgang zu starten. + Bereite die primäre Karte mit der Nummer %s vor. + Deine Wallet-Karte ist konfiguriert und einsatzbereit. + Maximale Anzahl an Karten hinzugefügt. Schließe den Sicherungsvorgang ab. + Karte aktivieren + Sicherungskarte Nr. %d + Keine Sicherungskarten + Eine Backup-Karte hinzugefügt + Bereite deine Karte vor + Zwei Backup-Karten hinzugefügt + Um zu beginnen, lade einfach die wallet mit einem beliebigen Betrag auf + Um zu beginnen, lade die Wallet einfach mit mehr als %1$s %2$s auf. + Krypto kaufen + Adresse der Wallet anzeigen + Aktivieren einer Wallet + Der Twinning-Prozess ist teilweise abgeschlossen. Du kannst ihn jetzt nicht mehr beenden. + Wenn der Prozess der Erstellung der Brieftasche in irgendeiner Weise unterbrochen wird, musst du diesen von vorne beginnen + Du kannst deine Schlüssel mit bis zu zwei weiteren leeren Tangem Wallet-Karten sichern. + Der Zugangscode kann mit einer der Ersatzkarten wiederhergestellt werden. + Alle Backup-Karten können als voll funktionsfähig mit den gleichen Schlüsseln verwendet werden. + Du kannst einen Zugangscode zum Schutz deiner wallet festlegen. + Wiederherstellungs-Wallet + Zugangscode wiederherstellen + Identische Karten + Zugangscode + Gruppe erstellen + Nach Guthaben + Token organisieren + Gruppierung aufheben + Wählen aus der Galerie aus + Einstellungen + Du hast keinen Zugriff auf deine Kamera gewährt + Kamerazugriff verweigert + %1$s ( %2$s ) im %3$s Netzwerk + Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. + Teilnehmen + Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. + Die Informationen über das Empfehlungsprogramm konnten nicht geladen werden. Fehlercode: %s. Bitte versuche es später noch einmal. + Anstehende Zahlungen + Deine Freunde kauften + Weniger + Mehr + Keine anstehenden Zahlungen + + für %d Wallet + für %d Wallets + + Du bekommst ^^%1$s^^ für jede von einem Freund gekaufte Wallet auf deine %2$s Netzwerkadresse %3$s ^^30 Tage nach^^ dem + Du + Bekommt eine + beim Kauf einer Wallet auf tangem.com + %s Rabatt + Dein Freund + Persönlicher Code kopiert! + Dein persönlicher Code + Tangem Wallet mit Rabatt kaufen!\n%s + Empfehle Tangem deinen Freunden + Du hast akzeptiert + Durch Tippen auf diese Schaltfläche akzeptierst du + des Empfehlungsprogramms + + %d Wallet + %d Wallets + + Zurücksetzen der Karte + Mir ist bewusst, dass ich nach der Durchführung dieser Aktion keinen Zugriff mehr auf die aktuelle Wallet habe. + Mir ist klar, dass ich diese Karte nicht verwenden kann, um meinen Zugangscode auf den anderen Karten der aktuellen Wallet wiederherzustellen + Durch das Zurücksetzen auf Werkseinstellungen wird die Wallet vollständig von der ausgewählten Karte gelöscht. Du kannst die aktuelle Wallet nicht wiederherstellen oder die Karte verwenden, um den Zugangscode wiederherzustellen. + Beim Zurücksetzen auf die Werkseinstellungen wird die Wallet der ausgewählten Karte vollständig gelöscht und aus der App entfernt. Es ist nicht möglich, die aktuelle Wallet wiederherzustellen. + Besitzt du eine Bankkarte aus einem anderen Land und eine Aufenthaltserlaubnis oder Registrierung außerhalb der Russischen Föderation? + Russische Bankkarten werden derzeit nicht akzeptiert + Melde dich bei der App an und überprüfe dein Guthaben, ohne die Karte zu scannen + Zugriff auf die App + Nutzung biometrischer Daten zulassen + Bei Interaktionen mit deiner Wallet werden anstelle des Zugangscodes biometrische Daten abgefragt + Zugangscode + Es sieht so aus, als ob du die biometrische Authentifizierung deaktiviert hast. Diese ist notwendig, um Wallets zu speichern + Biometrische Autorisierung aktivieren + Möchtest du Biometrie nutzen? + Beachte, dass für Transaktionen mit deinem Guthaben weiterhin deine Karte erforderlich ist + Karte scannen + Scanne die Karte, um ihre Einstellungen zu ändern. Die Änderungen wirken sich nur auf die von dir gescannte Karte aus und haben keine Auswirkungen auf andere mit deiner Wallet verknüpften Karten. + Halte deine Karte bereit! + Bereits in der eingegebenen Adresse enthalten + Der Provisionsbetrag ist %s mal der empfohlene Betrag. Stelle sicher, dass die benutzerdefinierten Einstellungen korrekt sind. + Du hast eine Kommission angegeben, die unter dem empfohlenen Betrag liegt, was zu einer Verzögerung Ihrer Transaktion führen könnte. Weiter? + Grund: %1$s\nCode: %2$s + Die Transaktion ist nicht abgeschlossen Betrag + Du kannst deine Transaktionsgebühr festlegen, indem du den Wert im Feld Satoshi pro vByte anpasst. + Die Gebühr, die für deine Transaktion erhoben wird. Du kannst einen eigenen Wert festlegen. + Maximale Gebühr + Dies sind die Kosten, die du für jede Gaseinheit zu zahlen bereit bist. Je höher der Gaspreis ist, desto schneller wird deine Transaktion bearbeitet. (Vorzugsgebühr inbegriffen) + Prioritätsgebühr + Die Gebühr, die ein Nutzer an Miner oder Validierer zahlen kann, um die Aufnahme seiner Transaktion in einen Block zu beschleunigen. + %1$s, %2$s Adresse + Ziel-Tag + Adresse eingeben Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein + Ungültiges Tag. Es wird der Transaktion nicht hinzugefügt. + Ungültiges Memo. Sie wird der Transaktion nicht hinzugefügt. Tag Memo inkl. Gebühr Niedrig Normal Priorität + Überprüfe deine Netzwerkverbindung + Informationen zur Netzwerkgebühr nicht erreichbar + Von + Grenzwert Gasgebühr + Dies ist die maximale Gasgebühr, die für den Abschluss einer Transaktion oder eines Vertrags ausgegeben wird. Ein Gaslimit verhindert unerwartete oder unbegrenzte Gebühren bei der Ausführung einer Transaktion. + Gaspreis + Dies sind die Kosten, die du für jede Gaseinheit zu zahlen bereit sind. Je höher der Gaspreis ist, desto schneller wird Ihre Transaktion abgewickelt. + Max Höchstbetrag + Gebühr bis zu + Ungültiges Memo + Abdeckung der Netzgebühren + Unzureichende Mittel für die Überweisung, da die Summe aus Gebühr und Überweisungsbetrag das bestehende Guthaben übersteigt + Gesamtbetrag übersteigt den Saldo + Das Konto wird von der Blockchain gelöscht, wenn der Kontostand unter die Mindesteinlage fällt. Bitte belasse %s auf deinem Konto. + Mindesteinlage + Der Kommissionsbetrag ist %s mal der empfohlene Betrag. Stelle sicher, dass die benutzerdefinierten Einstellungen korrekt sind. + Die individuelle Gebühr ist hoch + Aufgrund der Besonderheiten des Netzes %1$s ist die Gebühr für die Überweisung des gesamten Guthabens höher. Um die Kommission zu reduzieren, Kannst du %2$s verlassen. + Die Gebühr ist höher + Die enthaltene Kommission übersteigt den Überweisungsbetrag, was zu einem negativen Wert führt + ungültige Menge + Der Mindestbetrag für den Versand beträgt %1$s. Bitte stell sicher, dass der Restbetrag nach dem Versand nicht unter %2$s liegt. + Das Zielkonto wurde nicht erstellt. Bitte änder den zu sendenden Betrag. + Der zu sendende Betrag muss mindestens %s betragen + Verlasse %s + Verringern um %s + Reduziere auf %s + Bitte beachte, dass es bei bestimmten Gebühreneinstellungen zu Verzögerungen bei deiner Transaktion kommen kann + Transaktionsverzögerungen sind möglich + Aufgrund der Beschränkungen von %1$s können nur %2$s UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. + Begrenzung der Transaktionen + Optional + Bitte richte den QR-Code auf das Quadrat aus, um ihn zu scannen. Stell sicher, dass du die Netzwerkadresse %s scannen. + Jüngste + Empfänger + Keine gültige Adresse + Vergewisser dich, dass die empfangende Wallet-Adresse im %s Netzwerk ist, damit du deine Token nicht verlierst. + Senden an + Ein Memo/Destination Tag ist eine eindeutige ID zur Unterscheidung von Transaktionen, die an denselben Empfänger im selben Netzwerk gesendet werden. Vorsicht! Das Weglassen eines Vermerks kann dazu führen, dass Gelder für immer verloren sind! + Meine Wallet + Eine Möglichkeit, Bitcoin-Transaktionsgebühren zu messen. Du gibst die Anzahl der kleinsten Bitcoin-Einheiten (Satoshi) für jedes virtuelle Byte in einer Transaktion an. Je höher die Zahl, desto schneller wird die Transaktion von den Minern verarbeitet. + Satoshi / vByte + Senden... + Berühre an beliebiger Stelle für Änderungen + Versende %s + Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s + Du sendest ** %1$s ** und %2$s + Senden %s Gesamt %1$s und %2$s werden gesendet ≈ %1$s (inkl. Gebühr: %2$s) %s wird gesendet Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert Ungültige Adresse + %1$s (%2$s) + Transaktion gesendet + Entferne diese Wallet + Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. + Name + Verfügbar + Durchschnittliche Belohnungsquote + %s geschätzter Profit + Mindestanforderungen + Keine Belohnungen zu beanspruchen. + Gestaked + Belohnungen beanspruchen + Belohnungszeitplan + Staking %s + Entbindungsdauer + Aufwärmphase + Belohnungen + Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben + Revolutionäre Hardware-Wallet + Bis zu 3 physische Karten pro Wallet + Ultrasicheres Backup + Eine Hardware-Wallet für deine Bitcoins, Ethereum und viele weitere Währungen gleichzeitig – alles auf einer Karte + Tausende von Währungen + Verwende es unterwegs, überall und jederzeit. Keine Kabel oder Batterien. Tippe einfach mit der Karte auf dein Telefon, wenn du Kryptowährung benötigst. + Die Wallet für jeden + Lerne Tangem kennen + Tausche, kaufen Sie NFTs, vergebe Kredite und tätige Einlagen bei mehr als 100 verschiedenen dezentralen Diensten + Web 3.0-kompatibel + Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. + Der Betrag enthält die Gebühren des Dienstleisters. + Gebühren + Alle dezentralen Börsen benötigen Genehmigungen, um zu verhindern, dass intelligente Verträge ohne Ihre Erlaubnis auf Ihre Geldbörse zugreifen. Smart Contracts können nicht auf Ihre Token zugreifen, wenn Sie nicht zustimmen. Indem Sie Ihre Token \"freischalten\", ermächtigen Sie den 1-Zoll-Smart-Contract, sie auszugeben. Die Miner des Netzwerks erhalten eine (von Ihnen bezahlte) Gasgebühr, um diese Aktion in der Blockchain aufzuzeichnen. Sie können Ihre Token tauschen, nachdem Sie Ihre Zustimmung gegeben haben. + Genehmigen + Du wechselst + Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. + Unzureichende Mittel + Erlaubnis erteilen + In Arbeit + Tauschen + Du erhältst + Token auswählen + Nicht verfügbar + Guthaben versteckt + Angezeigte Salden + Rückgängig machen + Dieser Vorgang ist derzeit nicht verfügbar. Bitte versuche es später noch einmal. + Der Kauf von %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. + Du hast kein Guthaben um dies zu Verkaufen. Lade dein Konto auf, um Geld von dort zu verkaufen. + Du hast kein Guthaben zum Versenden. Lade dein Konto auf, um Geld von dort aus senden zu können. + Swappen %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. + Das Geld für den Verkauf steht zur Verfügung, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. + Die Geldsendung wird verfügbar, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. + Der Verkauf von %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. + Staking %s ist aktuell nicht verfügbar. Prüfe bitte ob es ein neues Update gibt. + Generiere XPUB + Ausblenden + Du bist dabei, dieses Token vom Hauptbildschirm auszublenden. Du kannst es jederzeit über die Seite „Token verwalten“ wieder hinzufügen. + Blende %s aus + Token ausblenden + Durch das Staking kannst du alle %2$s Tage %1$s verdienen und Belohnungen erhalten + Verdiene bis zu %s Stakingprämie pro Jahr + %1$s Token in %%image%% %2$s Netzwerk + Token in %%image%% %1$s Netzwerk + Der %1$s (%2$s) Token ist die Hauptwährung im %3$s Netzwerk und kann nicht versteckt werden, solange du andere Token dieses Netzwerks in der Liste aktiv hast. + %s kann nicht ausgeblendet werden + Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. + Tausche mit Changelly, %s Gebühren + Jetzt tauschen + Vertrag: %s + Du hast noch keine Transaktionen + Der Transaktionsverlauf konnte nicht geladen werden.\nKlicke auf die Schaltfläche Neu laden, um die Informationen zu aktualisieren. + Mehrere Adressen + Die Transaktionshistorie wird für diese Blockchain derzeit nicht verfügbar. Aber keine Sorge, wir arbeiten daran! In der Zwischenzeit kannst du es im Explorer überprüfen. + Operation + von: %s + zu: %s + Du hast dieselbe Karte gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte mit der Nummer %d scannen. + Du hast die falsche Doppelkarte gescannt. Bitte versuche eine andere Karte + Die, die du in den Hand hältst, und die andere mit der Nummer %s.\n\nBeide Karten können verwendet werden, um Geld aus dieser Wallet zu versenden. + Eine Wallet. Zwei Karten. + Scanne die Karte Nr. %s + Wallet erstellen + Scanne die Nr. %s Zwillingskarte + Karte vorbereiten Tangem Twin + Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. + Tippe auf die Doppelkarte mit der Nummer %s und entferne sie erst am Ende des Vorgangs. + Verwende %s oder scanne eine Karte, um Zugriff auf deine Wallet zu erhalten. + Neues Wallet hinzufügen + Möchtest du diese Wallet wirklich löschen? + Es ist ein Fehler aufgetreten, bitte scanne deine Karte, um sich anzumelden + Dieses Wallet wurde bereits gespeichert, du kannst ein weiteres hinzufügen + Die Wallet mit dem Namen %s existiert bereits + Wallet-Name + Wallet umbenennen + Alle freischalten + Alle mit %s freischalten + Blockchain ist nicht erreichbar. Versuche es später nochmal + Karte scannen + Aufforderung zum Signieren einer Nachricht. \n\n %s + Dapp %1$s, mit der Bitte um\nBNB-Transaktion zu unterzeichnen.\n\n%2$s + Handelsauftrag für %1$s\n Preis: %2$s\n Zu erhaltender Betrag: %3$s\n Zu zahlender Betrag: %4$s + Details zur Transaktion:\nVon: %1$s\nZu: %2$s\nBetrag: %3$s + Zwischenablage enthält WalletConnect-Code. Kopierten Wert verwenden oder QR-Code scannen + Antrag auf Erstellung einer Transaktion für %1$s\n%2$s\n\nBetrag: %3$s\nGebühr: %4$s\nGesamt: %5$s\nSaldo: %6$s + Die Transaktion kann nicht gesendet werden. Nicht genügend Guthaben. + WalletConnect Sitzung konnte nicht aufgebaut werden. Bitte versuche es später noch einmal. + Nachricht konnte nicht signiert werden.\nBitte, versuche es erneut + WalletConnect-Sitzung konnte nicht aufgebaut werden: Timeout-Fehler. Bitte versuche es später noch einmal. + Sitzungsanfrage enthält nicht unterstützte Blockchains für WalletConnect-Verbindung. Nicht unterstützte Blockchains:\n + Aufgrund der technischen Implementierung kann keine Verbindung zu dieser Dapp hergestellt werden. + Es ist ein unbekannter Fehler aufgetreten. Fehlermeldung: %s. Wenn das Problem weiterhin besteht, wende dich bitte an unseren Support + Falsche Karte in der Tangem App ausgewählt + Transaktion aus Dapp-Daten konnte nicht erstellt werden. Code: %s + Es ist ein unbekannter Fehler aufgetreten. Fehlercode: %d. Wenn das Problem weiterhin besteht, wende dich bitte an unseren Support + Keine geöffneten WalletConnect-Sitzungen + Hoppla. Keine Sitzungen. + WalletConnect-Sitzung konnte nicht gekoppelt werden: %1$s + Einfügen aus der Zwischenablage + Nachricht für %1$s:\n%2$s + Anfrage zum Starten einer Sitzung für\n %1$s\n\n NETZWERK: %2$s \n\n URL: %3$s + Der Vorgang konnte nicht abgeschlossen werden. \n\nDu hast bereits eine WalletConnect-Sitzung mit diesen Parametern hergestellt. + Neuen Code scannen + Mit dieser Karte kann keine WalletConnect-Sitzung hergestellt werden + Dieses Netzwerk wird nicht unterstützt. Bitte wähle ein anderes Netzwerk. + Netzwerk auswählen + WalletConnect-Sitzungen + Mit dApps verbinden WalletConnect + %s Marktpreis + letzte 24h + %s Netzwerk Die Adresse wurde erfolgreich kopiert Keine Internetverbindung + Wallet-Einstellungen Tangem + Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. + Aktivierungsfehler + Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. + BNB Beacon Chain wird abgeschaltet + Könnte besser sein + Gefällt mir OK, ich hab\'s! + Echt toll! + Aktualisieren + Du befindest sich derzeit im Demo-Modus + Demo-Modus aktiv + Die Karte, die du gescannt hast, ist eine Entwicklerkarte. Verwenden diese nicht zur Erstellung Ihrer Wallet. + Nicht für Benutzer! + %1$s Netzwerk erfordert eine Mindesteinlage. Wenn dein Konto unter %2$s fällt, wird es deaktiviert und alle verbleibenden Guthaben werden vernichtet. + Netzwerk erfordert eine Mindesteinzahlung + Der Swap wird nach Abschluss der Transaktion %s verfügbar sein. + Du hast aktive Transaktion + Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. + Genehmigung in Arbeit + Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt. + Du hast keine %s Coins in deiner Liste + Keine Token zum Tauschen verfügbar + Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s + Die Gebühr %s kann nicht gedeckt werden + Der zu erhaltende Betrag muss mindestens %s betragen + Service vorübergehend nicht verfügbar + Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s + Der zu tauschende Betrag muss mindestens %s betragen + Bitte änder den zu tauschenden Betrag + Bei dieser Karte kann es sich um ein Produktionsmuster oder eine Fälschung handeln + Echtheitsprüfung fehlgeschlagen + zuordnen + Dieser Token muss mit Ihrem Hedera-Konto verbunden sein, bevor du diesen erhalten kannst. Zuordnungsgebühr ~%1$s %2$s + Dieses Token muss mit deinem Hedera-Konto verknüpft sein, bevor du ihn erhalten kannst. + Verknüpfe deinen Token + Nicht genug %s. Lade dein Hedera-Konto auf, um dieses Token zuzuordnen + Auf dieser Karte sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben. + Geringe Anzahl von Unterschriften + Token in verschiedenen Netzwerken können unterschiedliche Adressen haben. Überprüfe bei der Überweisung noch einmal, ob deine Adresse mit der des Netzwerks übereinstimmt. + + Verwende deine Karte, um eine Adresse für das %d-Netz zu erhalten + Verwende deine Karte, um mehrere Adressen für die %d-Netzwerke zu erhalten + + Einige Adressen fehlen + Das Netzwerk ist derzeit nicht erreichbar. Bitte versuchen Sie es später erneut. + Netzwerk ist nicht erreichbar + Lade dein Guthaben auf + Deine Wallet wurde nicht gesichert. Führe diesen Vorgang durch, um dein Vermögen jetzt zu schützen. + Fehlende Sicherung + Diese Karte wurde bereits für Transaktionen verwendet. Wenn die Karte aus einer nicht vertrauenswürdigen Quelle stammt, solltest du den gesamten Betrag abheben. Wenn es sich um deine Karte handelt, sind keine Maßnahmen erforderlich. + Karte hat bereits Transaktionen unterzeichnet + Deine Bewertung motiviert uns, die Tangem Wallet noch besser zu machen. + Gefällt dir Tangem? + Du musst deinen Token zuordnen, bevor du Token erhalten kannst + Netzmietgebühr erforderlich + %1$s ist ein Vermögenswert im %2$s Netzwerk. Um eine %3$s Transaktion durchzuführen, musst du etwas %4$s (%5$s) einzahlen, um die Netzwerkgebühr zu decken. + Unzureichende %1$s zur Deckung der Netzgebühr + Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion. + Solana Netzwerkalarm + Das Solana-Netzwerk erhebt alle 2 Tage eine Miete von %1$s. Konten, die sich die Miete nicht leisten können, werden aus dem Netzwerk gelöscht. Hinterlege deinem Konto mit mehr als %2$s, um es kostenlos zu nutzen. + Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut. + Einige Netzwerke sind nicht erreichbar + Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden. + Nur für Testzwecke + Verwerfen + Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? + Ja, fortsetzen + Verwerfen + Wenn du das Backup jetzt abbrichst, musst du die Karten auf die Werkseinstellungen zurücksetzen, um von vorne zu beginnen + Sicherung fortsetzen + Dies ist eine unwiderrufliche Aktion + Anmelden mit %s + Karte scannen + Verwenden %s oder scanne eine Karte, um auf die App zuzugreifen + Willkommen zurück! diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml index 97b9e7c7c7..8269ca16c6 100644 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ b/core/res/src/main/res/values-fr/strings-blockchain.xml @@ -1,17 +1,25 @@ - Échec de réception des commissions - Pour créer un compte, envoyez des fonds monétaires à cette adresse + Défaut + Héritage + Échec de réception des frais de commissions + En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant. + Pas assez de fonds pour la transaction. Veuillez recharger votre compte. + Une erreur s\'est produite. Code:%s + Pour utiliser le réseau %1$s, vous devez payer la réserve de compte (%2$s%3$s), qui bloque et cache ce montant indéfiniment + Le compte de destination est inactif. Envoyez %s ou plus pour activer le compte. + Pour créer un compte, envoyez des fonds à cette adresse Le montant minimal est de %s - Le reste est trop petit - Commission non valide - Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s + commissions ou plus + Le montant est trop faible + Frais de commissions non valides + Le solde minimal est de %s + Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s + frais de commissions ou plus Erreur inconnue Le montant dépasse le solde - Somme incorrecte - Les commissions dépassent le solde + Montant invalide + Les frais de commissions dépassent le solde Le total dépasse le solde Non, envoyer toute la somme Réduire de %s XTZ - Pour ne pas payer une commission élevée la prochaine fois que vous rechargez votre portefeuille, veuillez réduire le montant de %s XTZ + Pour ne pas payer un fraid de commissions élevé la prochaine fois que vous rechargez votre portefeuille, veuillez réduire le montant de %s XTZ diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index e7454edeb7..a07a86a3ab 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1,59 +1,795 @@ + Choisissez le réseau + Ajouter un jeton personnalisé + Gérer les jetons + Envoyez uniquement %1$s (%2$s) depuis les réseaux %3$s à cette adresse. L\'utilisation d\'autres jetons et réseaux peut entraîner une perte de fonds. + Comment scanner + Demander de l\'aide + Réessayer + Cette fonctionnalité est désactivée en mode démo + Raison : %s + Impossible d\'envoyer une transaction + Le sélectionné ne prend pas en charge le réseau %1$s + Pour activer le cryptage de la blockchain %1$s, vous devrez réinitialiser le portefeuille aux paramètres d\'usine. Veuillez retirer vos fonds avant de le faire pour vous assurer de ne pas les perdre, puis terminez le processus de réinitialisation. L\'accès au portefeuille actuel ne sera pas possible après la réinitialisation. + Les jetons du réseau %1$s ne sont pas pris en charge par cette carte en raison d\'une limitation du micrologiciel. + Avez-vous des difficultés à scanner votre carte ? Cette carte n\'est pas conçue pour fonctionner avec Tangem + Frais par défaut + Activez les frais par défaut pour définir automatiquement les frais de transaction et ignorer la page Frais lors de l\'envoi de fonds. Vous pouvez toujours revenir sur cette page si nécessaire. + Allez dans les paramètres pour activer l\'authentification biométrique dans le Tangem App + Activer l\'authentification biométrique + Cela supprimera tous les codes d\'accès des portefeuilles enregistrés. Toute opération ultérieure avec le portefeuille nécessitera la soumission du code d\'accès. + La suppression de la carte enregistrée supprime de l\'application tous les portefeuilles enregistrés et leurs codes d\'accès. + Enregistrer le code d\'accès + L\'authentification biométrique sera demandée à la place du code d\'accès pour les interactions avec votre carte. + Conserver le portefeuille dans l\'application + Activez cette option pour lier tous les portefeuilles à l\'application Tangem. L\'authentification biométrique sera requise pour déverrouiller l\'application. La signature des transactions nécessite de taper sur votre carte Tangem. + Sombre + Clair + Par défaut du système + Thème + Paramètres de l\'application + Pour masquer ou afficher vos soldes, il suffit de retourner l\'écran de votre appareil vers le bas ou de le désactiver dans les paramètres + Ne plus afficher + Compris + Les soldes sont masqués + Veuillez scanner la carte + Veuillez réessayer dans 30 secondes ou scanner la carte + Trop de tentatives + Vous avez désactivé l\'authentification biométrique sur votre téléphone et ne pourrez pas enregistrer de portefeuilles dans l\'application. Pour enregistrer des portefeuilles, veuillez activer la fonction d\'authentification biométrique dans les paramètres de votre téléphone. + Démarrer le processus de sauvegarde + + %d carte + %d cartes + + Désactivez cette option si vous ne voulez pas que cette carte soit utilisée pour réinitialiser les codes d\'accès sur d\'autres cartes de ce portefeuille. Veuillez noter que cela vous empêchera également de réinitialiser le code d\'accès sur cette carte. + Vous permet d\'utiliser cette carte pour réinitialiser le code d\'accès sur d\'autres cartes de ce portefeuille + Récupération du code d\'accès + Réinitialiser + Êtes-vous sûr de vouloir faire cela ? + Modifier le code d\'accès + Le code d\'accès ne sera modifié que sur cette carte + Toutes les cartes du portefeuille sélectionné ont été réinitialisées aux paramètres d\'usine. Vous pouvez maintenant créer un nouveau portefeuille. + Réinitialisation terminée + Voulez-vous réinitialiser la prochaine carte de ce portefeuille ? + Carte réinitialisée + Nous vous recommandons de terminer le processus de réinitialisation pour toutes les cartes de ce portefeuille + Vous n\'avez pas réinitialisé toutes vos cartes + Réinitialiser aux paramètres d\'usine + Mode de sécurité + Paramètres de la carte + En plus des frais de réseau, le réseau Cardano facture %1$s de l\'ADA lors des transactions avec le jeton %2$s + Exigences pour les transactions Cardano + Pour effectuer une transaction %1$s, vous devez déposer de l\'ADA pour couvrir les frais de réseau et la valeur minimale en ADA (5 ADA recommandés) + ADA insuffisant pour le transfert de jetons + Vous devez conserver un peu d\'ADA car vous avez des jetons sur la blockchain Cardano + ADA insuffisant J\'accepte + Accès refusé + Tout + Permettre + Appliquer + Approbation + Approuver + Attention Solde : %s + Solde + authentification biométrique + biométrie + Acheter + Aller à %1$s Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité Annuler + Fermer + Continuer + Copier + Copier l\'adresse + Créer + Personnalisé Supprimer + Désactivé Exécuté + Activer + Activé Erreur + Explorer + Explorez l\'historique des transactions + Explorateur Commissions + Les frais de réseau sont des charges que les utilisateurs paient pour traiter et confirmer les transactions. Le montant des frais peut être affecté par la congestion du réseau, la taille de la transaction et la priorité d\'exécution. %s + Rapide + Marché + Lent + Vitesse et frais + Obtenir des adresses + Aller au fournisseur + Importez + Plus tard + Verrouillé + Réseau principal Commissions du réseau + Le montant envoyé sera réduit de %1$s(%2$s) pour couvrir le niveau de frais sélectionné + Suivant + Non + Aucune adresse + Maintenant OK + Carte principale + Passphrase + Coller + %1$s-%2$s + En savoir plus + Recevoir + Rejeter + Recharger + Renommer + Enregistrer Sauvegarder les modifications + Rechercher + Rechercher des jetons + Seed phrase + Sélectionner une action + Vendre Envoyer + Le serveur n\'est pas disponible, veuillez réessayer plus tard + Partager + Signer + Signer et envoyer + Enjeu + Staking + Démarrer + Soumettre Avec succès + Support + Échange + termes et conditions + Aujourd\'hui + La transaction a échoué + Transactions + Transfert + Je comprends + Il y avait une erreur. Veuillez réessayer. + Inaccessible + Oui + Adresse du contrat copiée ! + Réseaux disponibles + Ajouter un jeton + Adresse du contrat + L\'adresse du contrat n\'est pas valide + Veuillez sélectionner le réseau + Les décimales doivent être un entier valide, jusqu\'à %li + Dérivation personnalisée + Par exemple m/00\'/0000\'/0\'/0/0 + Entrez une dérivation personnalisée + Décimales + Chemin de dérivation + Par défaut + Coin de type BIP44 + Le chemin de dérivation entré est invalide + Par exemple USD coin + Nom + Non sélectionné + Réseau + Réseau de jetons + Vous pouvez ajouter manuellement un jeton qui n\'est pas nativement pris en charge par Tangem + Par exemple USDC + Symbole + Symbole du jeton + Ce jeton/réseau a déjà été ajouté à votre liste + Notez que les jetons peuvent être créés par n\'importe qui. Méfiez-vous de l\'ajout de jetons frauduleux, ils peuvent ne rien coûter. + Prenez garde de l\'ajout de jetons frauduleux, ils peuvent ne rien coûter + Notez que les jetons peuvent être créés par n\'importe qui + Acheter un Portefeuille Tangem + Chat Code d\'accès Vous devrez entrer le mot de passe correct avant de scanner la carte Tenez la carte fermement Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. Mot de passe Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. + Programme de parrainage + Retournez l\'écran de votre appareil vers le bas pour masquer et afficher rapidement les soldes %s hashes ID de la carte + Contactez l\'équipe de support + Lier plus de cartes Monnaie de l\'application + Retourner pour masquer les soldes Emetteur Signé + Envoyer un commentaire Référénces + Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation + Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille + Mes jetons + Vous n\'avez pas encore ajouté de jetons. Ajoutez des jetons via Market pour effectuer une échange + Ne peut pas être échangé contre %s + Fourni par + Statut + Tangem propose des échanges de jetons via des fournisseurs tiers selon les conditions de chaque fournisseur + Choisissez un fournisseur + Une erreur s\'est produite. Code : %s + Oups! L\'échange de la paire sélectionnée via le fournisseur choisi est temporairement indisponible. Veuillez réessayer plus tard. (Code : %s) + Le fournisseur sélectionné n\'est pas disponible pour le moment. Veuillez réessayer plus tard. (Code : %s) + Les échanges ne sont pas disponibles pour le moment. Veuillez réessayer plus tard. (Code : %s) + Montant estimé + Échange par %s + Visitez le site Web du fournisseur pour obtenir un remboursement + Opération échouée par le fournisseur + Visitez le site Web du fournisseur pour la vérification + Vérification KYC requise par le fournisseur + Annulé + Confirmé + En attente de confirmation + En cours de confirmation + Échangé + En cours d\'échange + En cours d\'échange + Échoué + Dépôt reçu + En attente du dépôt + En attente du dépôt + Remboursé + Je vous envoie + En cours d\'envoi + Envoyé + Données fournies par le fournisseur. Le montant estimé est sujet à modification en raison des conditions du marché. + Statut de l\'échange + Verification requise + Liste de tous les jetons ajoutés à votre portefeuille + En cherche des meilleurs taux + Taux flottant + En utilisant la fonctionnalité d\'échange, vous acceptez les %s + En utilisant la fonctionnalité d\'échange, vous acceptez les conditions %1$s et %2$s du fournisseur. + D\'autres fournisseurs arriveront bientôt.\nRestez branchés ! + Politique de confidentialité + Fournisseur + Meilleur taux + Disponible jusqu\'à %s + Disponible à partir de %s + Indisponible pour cette paire + Permission requise + Conditions d\'utilisation + Aucun jeton trouvé. Veuillez essayer une autre demande + ID : %s + ID de transaction copié + Les informations suivantes sont facultatives. Vous pouvez les effacer si vous ne souhaitez pas les partager. + Dites-nous quelles fonctions vous manquent, et nous essaierons de vous aider. + Veuillez nous dire quelle carte vous avez + Chère équipe de support, + Veuillez nous en dire plus sur votre problème. Chaque petit détail peut nous aider. + Mes suggestions + Impossible de scanner une carte + Commentaires + Commentaires sur Tangem + Impossible d\'envoyer une transaction + Transaction en cours + Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. + Spécifiez la limite approuvée pour le jeton sélectionné + Montant %s + Pour continuer, accordez aux smart contracts de %1s l\'autorisation d\'utiliser votre %2s + Donner l\'autorisation + Illimité + Commander une carte Scannez la carte Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe + Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération + Appuyez sur la carte n°%s du portefeuille Posez pour scanner Touchez pour signer Posez la carte + Vous avez mis à jour vos données biométriques, scannez votre carte pour entrer + Votre solde doit être supérieur à la valeur des frais pour effectuer un transfert + Solde insuffisant + Vous n\'avez pas assez de Mana pour cette transaction. Veuillez attendre que le Mana soit reconstitué. Votre solde de Mana est de %1$s/%2$s + Mana insuffisant + Vous ne pouvez transférer que %s en raison de la limite de Mana imposée par le réseau Koinos + Limite de Mana + Le réseau Koinos nécessite du Mana pour les frais de réseau. Vous avez %1$s/%2$s Mana + Quantité de Mana + Pour commencer à suivre vos actifs et transactions crypto, ajoutez des jetons + Gérer les jetons + Pour accéder à tous les réseaux, vous devez scanner la carte + Scannez votre carte + Bénéficiez de %1$s frais de service sur les échanges via Changelly du %2$s au %3$s février + Echangez avec Changelly, %s frais + Jetons + Ajouter + Modifier + Capitalisation boursière des crypto + Blockchain sur laquelle la crypto-monnaie a été initialement créée + Réseau natif + L\'utilisation de réseaux non natifs pour les jetons permet l\'interopérabilité entre les blockchains permettant aux actifs d\'être utilisés dans diverses applications décentralisées et smart contracts sur différentes plateformes. Cependant, cela implique souvent un custodial ou un smart contract pour détenir l\'actif original en toute sécurité introduisant une centralisation et un risque de contrepartie. + Blockchain non originale ou principale sur laquelle le jeton est hébergé + Réseaux non natifs + Choisissez les réseaux + Portefeuille + Impossible de trouver ce jeton, vous pouvez l\'ajouter manuellement + + %1$d du %2$d portefeuille + %1$d des %2$d portefeuilles + + Enlever + par exemple, BTC I trust, hodl I must + Votre portfolio a été mis à jour + Le jeton sélectionné n\'est actuellement pas disponible pour des actions dans le portefeuille crypto. Mais ne vous inquiétez pas, vous pouvez exprimer votre intérêt en votant positivement. + Vote positif + Choisissez un portefeuille + Le portefeuille ne prend pas en charge plus d\'un réseau + Pour commencer à acheter, échanger ou recevoir cet actif, ajoutez ce jeton à au moins 1 réseau + Cet actif n\'est pas disponible + Ajouter au portfolio + Mon portfolio + Marché + Sélectionnez un portefeuille + Trier par + Idées + Liens + Métriques + Performance des prix + Score de sécurité + Vous devez définir un seul code d\'accès pour protéger toutes vos cartes + Protéger + Vous pourrez définir un code d\'accès individuel sur chaque carte plus tard + Personnaliser + Le code d\'accès peut être restauré avec une carte liée. Ne gardez pas toutes les cartes au même endroit. + Restaurer + Choisissez n\'importe quel mot, phrase ou nombre que vous souhaitez comme code d\'accès + Créer un code d\'accès + Entrez à nouveau votre code d\'accès pour éviter une erreur + Saisissez à nouveau votre code d\'accès + Le code d\'accès doit comporter au moins 4 caractères + Le code d\'accès saisi ne correspond pas au code d\'accès initial + Veuillez répéter l\'opération. La carte sera réinitialisée aux paramètres d\'usine. + Erreur d\'activation + Ajouter des jetons + Vous avez ajouté une carte de sauvegarde. Une fois le processus de sauvegarde terminé, vous ne pourrez plus ajouter de cartes de sauvegarde. Si vous avez une autre carte, ajoutez-la à la sauvegarde. Souhaitez-vous poursuivre le processus de sauvegarde ? + Le processus de sauvegarde est partiellement terminé. Vous ne pouvez pas le quitter maintenant. + La phrase secrète est une fonctionnalité de sécurité avancée utilisée par les portefeuilles cryptographiques. Il ajoute un mot ou une phrase supplémentaire de votre choix à votre phrase de récupération déjà existante pour débloquer un tout nouvel ensemble d\'adresses. + Ajouter une carte de sauvegarde + Scannez la carte n°%d + Sauvegarder maintenant + Scannez la carte principale + Continuer vers mon portefeuille + Finaliser la sauvegarde + Recevoir des crypto-monnaies + Scannez la carte principale + Ignorer pour plus tard + Comment ça marche ? + Générons toutes les clés sur votre carte et créons un portefeuille sécurisé Créer un portefeuille + Créer un portefeuille + Autres options + Vos clés seront générées de manière sécurisée à l\'intérieur de la carte. Il n\'y a pas de seed phrase, ce qui signifie que personne ne peut l\'exporter ou la voler. + Générer des clés de manière privée + Votre carte est activée et prête à être utilisée + Succès ! + Dans ce cas, vous devrez recommencer depuis le début. + Voulez-vous quitter le processus d\'activation ? + Initialiser + Un autre portefeuille a déjà été créé sur la carte que vous essayez d\'ajouter. Si vous avez des fonds dans ce portefeuille, veuillez les retirer, puis réinitialiser cette carte et l\'ajouter comme sauvegarde. + Création d\'une sauvegarde + En savoir plus sur les seed phrases + + Empty + Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. + + Votre seed phrase + + Empty + %d mots + + Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous + Générer une seed phrase + Importer un portefeuille + Une seed phrase est une série de mots qui vous permet de récupérer votre portefeuille. Contrairement aux clés générées par la carte, les seed phrases ne sont pas protégées et peuvent être copiées et volées. Utilisez cette option à vos propres risques. + Utiliser une seed phrase + Seed phrase invalide. Veuillez vérifier l\'ordre des mots. + Seed phrase invalide. Veuillez vérifier votre orthographe. + Héritage + Pour vérifier si vous avez correctement noté votre seed phrase, veuillez entrer les 2ème, 7ème et 11ème mots + Alors, vérifions + Pour démarrer le processus de sauvegarde, ajoutez jusqu\'à deux cartes de sauvegarde. + Vous pouvez ajouter une carte supplémentaire ou finaliser le processus de sauvegarde + Préparez la carte de sauvegarde avec le numéro %s + Scannez la carte principale pour démarrer le processus de sauvegarde. + Préparez la carte principale avec le numéro %s + Votre carte de portefeuille est configurée et prête à l\'emploi. + Nombre maximum de cartes ajoutées. Finalisez le processus de sauvegarde. + Activation de la carte + Carte de sauvegarde n°%d + Aucune carte de sauvegarde + Une carte de sauvegarde ajoutée + Préparez votre carte + Deux cartes de sauvegarde ajoutées + Pour commencer, rechargez simplement le portefeuille avec n\'importe quel montant + Pour commencer, rechargez simplement le portefeuille avec plus de %1$s %2$s + Acheter des crypto-monnaies + Afficher l\'adresse du portefeuille + Activer un portefeuille + Le processus de jumelage est partiellement terminé. Vous ne pouvez pas le quitter maintenant. + Si le processus de création du portefeuille est interrompu de quelque manière que ce soit, vous devrez recommencer + Vous pouvez sauvegarder vos clés sur jusqu\'à deux autres cartes vierges du Portefeuille Tangem. + Le code d\'accès peut être restauré avec l\'une des cartes de sauvegarde. + Toutes les cartes de sauvegarde peuvent être utilisées comme des cartes entièrement fonctionnelles avec des clés identiques. + Vous pourrez définir un code d\'accès pour protéger vos portefeuilles. + Portefeuille de sauvegarde + Restauration du code d\'accès + Cartes identiques + Code d\'accès + Grouper + Par solde + Organiser les jetons + Dégrouper + Sélectionnez dans la galerie + Paramètres + Vous n\'avez pas donné accès à votre caméra + Accès à la caméra refusé + %1$s (%2$s) sur le réseau %3$s + Envoyez uniquement %s à cette adresse. L\'envoi de toute autre devise entraînera sa perte irréversible. + Participer + Échec du chargement des informations sur le programme de parrainage. Veuillez réessayer plus tard. + Échec du chargement des informations sur le programme de parrainage. Code d\'erreur : %s. Veuillez réessayer plus tard. + Paiements à venir + Vos amis ont acheté + Moins + Plus + Aucun paiement à venir + + pour le portefeuille %d + pour les portefeuilles %d + + Obtiendras ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^ + Vous + Obtiendrez + lors de l\'achat d\'un portefeuille sur tangem.com + %s réduction + Votre ami + Code personnel copié ! + Votre code personnel + Achetez le Portefeuille Tangem avec une remise !\n%s + Référez Tangem à vos amis + Vous avez accepté + En appuyant sur ce bouton, vous acceptez + du programme de parrainage + + %d portefeuille + %d portefeuilles + + Réinitialiser la carte + Je comprends qu\'après avoir effectué cette action, je n\'aurai plus accès au portefeuille actuel + Je comprends que je ne peux pas utiliser cette carte pour récupérer mon code d\'accès sur les autres cartes du portefeuille actuel + La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée. Vous ne pourrez pas restaurer le portefeuille actuel ni utiliser la carte pour récupérer le code d\'accès. + La réinitialisation aux paramètres d\'usine supprimera complètement le portefeuille de la carte sélectionnée et le supprimera de l\'application. Vous ne pourrez pas restaurer le portefeuille actuel. + Avez-vous une carte bancaire d\'un autre pays et un permis de séjour ou une inscription en dehors de la Fédération de Russie ? + Les cartes bancaires russes ne sont actuellement pas acceptées + Connectez-vous à l\'application et vérifiez votre solde sans scanner la carte + Accéder à l\'application + Autoriser l\'utilisation de la biométrie + La biométrie sera demandée à la place du code d\'accès pour les interactions avec votre portefeuille + Code d\'accès + Il semble que vous ayez désactivé l\'authentification biométrique, elle est nécessaire pour sauvegarder les portefeuilles + Activer l\'autorisation biométrique + Souhaitez-vous utiliser la biométrie ? + Notez qu\'effectuer une transaction avec vos fonds nécessitera toujours votre carte + Scannez votre carte + Scannez la carte pour modifier ses paramètres. Les modifications n\'affecteront que la carte que vous avez scannée et n\'affecteront pas les autres cartes liées à votre portefeuille. + Préparez votre carte ! + Déjà inclus dans l\'adresse saisie + Le montant de la commission est %s fois le montant recommandé. Assurez-vous que les paramètres personnalisés sont corrects. + Vous avez spécifié une commission inférieure au montant recommandé, ce qui pourrait entraîner un retard dans votre transaction. Continuer? + Raison : %1$s\nCode : %2$s + La transaction est incomplète Somme + Vous pouvez définir vos frais de transaction en ajustant la valeur dans le champ Satoshi par vByte. + Les frais qui seront facturés pour votre transaction. Vous pouvez définir votre propre valeur. + Frais maximum + Il s\'agit du coût que vous êtes prêt à payer pour chaque unité de gaz. Plus le prix du gaz est élevé, plus votre transaction sera traitée rapidement. (Frais de priorité inclus) + Frais de priorité + Les frais qu\'un utilisateur peut payer aux mineurs ou aux validateurs pour accélérer l\'inclusion de leur transaction dans un bloc. + %1$s, %2$s Adresse + Destination Tag + Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille + Tag invalide. Il ne sera pas ajouté à la transaction. + Mémo invalide. Il ne sera pas ajouté à la transaction. Tag Memo Inclure les commissions Bas Normal Priorité + Vérifiez votre connexion réseau + Informations sur les frais de réseau inaccessibles + De + Limite de gaz + Il s\'agit du montant maximal de gaz qui sera dépensé pour effectuer une transaction ou un contrat. Une limite de gaz empêche des frais imprévus ou illimités lors de l\'exécution d\'une transaction. + Prix ​​du gaz + Il s\'agit du coût que vous êtes prêt à payer pour chaque unité de gaz. Plus le prix du gaz est élevé, plus votre transaction sera traitée rapidement. + Max Somme maximale + Frais jusqu\'à + Memo invalide + Couverture des frais de réseau + Fonds insuffisants pour le transfert, car le total des frais et du montant du transfert dépasse le solde existant + Le total dépasse le solde + Le compte sera effacé de la blockchain si un solde descend en dessous du dépôt existentiel. Veuillez en laisser %s sur votre solde. + Dépôt existentiel + Le montant de la commission est %s fois le montant recommandé. Assurez-vous que les paramètres personnalisés sont corrects. + Les frais de douane sont élevés + En raison des particularités du réseau %1$s, les frais de transfert de la totalité du solde sont plus élevés. Pour réduire la commission, vous pouvez en laisser %2$s. + Les frais sont plus élevés + La commission incluse dépasse le montant du transfert, ce qui entraîne une valeur négative + Montant invalide + Le montant minimum d\'envoi est de %1$s. Veuillez vous assurer que le solde restant après l\'envoi ne sera pas inférieur à %2$s. + Le compte cible n\'est pas créé. Veuillez modifier le montant à envoyer. + Le montant à envoyer doit être d\'au moins %s + Quitter %s + Réduire de %s + Réduire à %s + Veuillez noter que votre transaction peut subir des retards dans certains paramètres de frais + Des retards de transaction sont possibles + En raison de %1$s limitations, seuls %2$s UTXO peuvent s\'intégrer dans une seule transaction. Cela signifie que vous ne pouvez en envoyer que %3$s ou moins. Vous devez réduire le montant. + Limitation des transactions + Facultatif + Veuillez aligner votre code QR avec le carré pour le scanner. Assurez-vous de scanner l\'adresse du réseau %s. + Récent + Destinataire + Adresse non valide + Assurez-vous que l\'adresse du portefeuille de réception est sur le réseau %s pour éviter de perdre vos jetons + Envoyer à + Un Memo/Destination Tag est un identifiant unique permettant de différencier les transactions envoyées au même destinataire sur le même réseau. Attention : L\'omission d\'un mémo peut entraîner des fonds mal placés + Mes portefeuilles + Un moyen de mesurer les frais de transaction Bitcoin. Il indique le nombre de la plus petite unité de Bitcoin (Satoshi) pour chaque octet virtuel dans une transaction. Plus le nombre est élevé, plus la transaction sera traitée rapidement par les mineurs. + Satoshi / vByte + En cours d\'envoi + Appuyez sur n\'importe quel champ pour le modifier + Envoyer %s + Vous envoyez **%1$s** incluant des frais de réseau de %2$s + Vous envoyez **%1$s** and %2$s + Envoi de %s Total Sera envoyé %1$s et %2$s ≈ %1$s (incl. les commissions : %2$s) Sera envoyé %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps Adresse incorrecte + %1$s (%2$s) + Transaction envoyée + Préparez-vous à numériser la carte que vous souhaitez configurer. + Oublier le portefeuille + Cela supprimera le portefeuille de l\'application. Le portefeuille lui-même peut être ajouté à nouveau. + Nom + Actif + Afin d\'unstaker vos actifs, cliquez ici. + APR + APY + Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. + Disponible + Taux de récompense moyen + %s profit estimatif + Cote du marché + Métriques + Minimum requis + Aucune récompense à réclamer + En jeu + Réclamation de récompense + Un moyen de recevoir des récompenses de staking. Il peut être réclamé automatiquement ou manuellement. + Calendrier de récompenses + Il s\'agit d\'un calendrier qui détermine le moment où les participants au staking reçoivent leurs récompenses. + Récompenses à réclamer : %s + Staking %s + Période de détachement + La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles. + Période d\'échauffement + Le temps imparti pour activer la participation au staking. + Native staking + Le staking vous permet d\'en gagner %1s. Vos récompenses de staking arrivent tous les ~%2s jours. + Gagnez des récompenses de staking + Récompenses + Staker plus + Non-staké + Vérifiez non-stakés pour réclamer vos actifs + Validateur + Stockez vos actifs crypto en toute sécurité tout en conservant les clés privées contenues dans votre carte + Portefeuille matériel révolutionnaire + Jusqu\'à 3 cartes physiques pour un portefeuille + Sauvegarde ultra sécurisée + Un portefeuille matériel pour vos Bitcoin, Ethereum et de nombreuses autres devises simultanément, le tout dans une seule carte + Des milliers de devises + Utilisez-la en déplacement, n\'importe où, n\'importe quand. Pas de fils ni de piles. Il suffit de taper la carte sur votre téléphone lorsque vous avez besoin de votre crypto. + Le portefeuille pour tous + Découvrez Tangem + Échangez, achetez des NFT, faites des prêts et des dépôts dans plus de 100 services décentralisés différents + Compatible Web 3.0 + Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. + Le montant comprend les frais du fournisseur de services. + Frais + Tous les échanges décentralisés nécessitent des approbations pour empêcher les smart contracts d\'accéder à votre portefeuille sans votre permission. Par conception, les smart contracts ne peuvent pas accéder à vos jetons sans votre approbation. En « déverrouillant » vos jetons, vous autorisez le smart contract 1-inch à les dépenser. Les mineurs du réseau reçoivent des frais de gaz (payés par vous) pour enregistrer cette action sur la blockchain. Vous pouvez échanger votre jeton après avoir donné votre approbation. + Approuver + Vous échangez + Échanger ce montant de jetons sélectionnés aura un impact significatif sur les prix et réduira votre résultat. + Fonds insuffisants + Donner l\'autorisation + En cours + Échanger + Vous recevez + Choisir le jeton + non disponible + Soldes masqués + Soldes affichés + Annuler + Cette opération est actuellement indisponible. Veuillez réessayer plus tard. + L\'achat de %s n\'est pas disponible pour le moment. Veuillez vérifier vos mises à jour. + Vous n\'avez pas de fonds à vendre. Renflouez votre compte pour pouvoir vendre des fonds à partir de celui-ci. + Vous n\'avez pas de fonds à envoyer. Renflouez votre compte pour pouvoir envoyer des fonds à partir de celui-ci. + Le service d\'échange %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour. + La vente de fonds sera disponible une fois que la ou les transactions en attente dans le réseau %s seront terminées + L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées. + La vente de %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour. + Le staking %s n’est pas disponible pour le moment. Veuillez consulter nos mises à jour. + Générer XPUB + Masquer + Vous êtes sur le point de masquer ce jeton de l\'écran principal. Vous pouvez le rajouter à tout moment via la page de gestion des jetons. + Masquer %s + Masquer le jeton + Le Staking vous permet d\'en gagner %1$s et d\'obtenir des récompenses tous les %2$s jours + Gagnez jusqu\'à %s récompense de mise par an + %1$s jeton dans %%image%% le réseau %2$s + Jeton dans le %%image%% %1$s réseau + Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste + Impossible de masquer %s + Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février. + Échangez avec Changelly, %s frais + Échangez maintenant + contrat : %s + Vous n\'avez pas encore de transactions + Échec du chargement de l\'historique des transactions.\nCliquez sur le bouton de rechargement pour mettre à jour les informations. + Adresses multiples + L\'historique des transactions n\'est actuellement pas pris en charge pour cette blockchain. Mais ne vous inquiétez pas, nous y travaillons ! En attendant, vous pouvez le vérifier dans l\'explorateur. + Opération + de : %s + à : %s + Vous avez scanné la même carte. Pour créer un portefeuille jumeau, vous devez scanner la carte portant le numéro %d + Vous avez scanné une mauvaise carte jumelle. S\'il vous plaît, essayez-en un autre + Celle que vous tenez dans vos mains et l\'autre portant le numéro %s\n\nLes deux cartes peuvent être utilisées pour extraire des fonds de ce portefeuille. + Un portefeuille. Deux cartes. + Scannez la carte n°%s + Création du portefeuille + Scannez la carte jumelle n°%s + Préparation de la carte Tangem Twin + Cette action est irréversible. Vous n\'aurez plus accès à l\'ancien portefeuille. + Appuyez sur la carte jumelle avec le numéro %s et ne la retirez pas jusqu\'à la fin de l\'opération + Utilisez %s ou scannez une carte pour avoir accès à votre portefeuille + Restez à jour avec les dernières fonctionnalités et actualités + Souhaitez-vous utiliser les notifications push? + Ajouter un nouveau portefeuille + Êtes-vous sûr de vouloir supprimer ce portefeuille ? + Une erreur s\'est produite, veuillez scanner votre carte pour vous connecter + Ce portefeuille a déjà été enregistré, vous pouvez en ajouter un autre + Le portefeuille portant le nom %s existe déjà + Nom du portefeuille + Renommer le portefeuille + Tout déverrouiller + Tout déverrouiller avec %s + La blockchain n\'est pas accessible. Réessayez plus tard + Scanner la carte + Demande de signature d\'un message.\n\n%s + Dapp %1$s, demandant de\nsigner la transaction BNB.\n\n%2$s + Ordre de trade pour %1$s\nPrix : %2$s\nMontant à recevoir : %3$s\nMontant à payer : %4$s + Détails de la transaction :\nDe : %1$s\nÀ : %2$s\nMontant : %3$s + Le presse-papiers contient un code WalletConnect. Utilisez la valeur copiée ou scannez le QR-code + Demande de création d\'une transaction pour %1$s\n%2$s\n\nMontant : %3$s\nFrais : %4$s\nTotal : %5$s\nSolde : %6$s + Impossible d\'envoyer la transaction. Fonds insuffisants. + Échec de l\'établissement de la session WalletConnect. Veuillez réessayer plus tard. + Échec de la signature du message. Veuillez réessayer + Échec de l\'établissement de la session WalletConnect : erreur de délai d\'attente. Veuillez réessayer plus tard. + La demande de session contient des blockchains non prises en charge pour la connexion WalletConnect. Blockchains non prises en charge :\n + La connexion avec cette dApp ne peut pas être établie en raison de son implémentation technique. + Nous avons rencontré une erreur inconnue. Message d\'erreur : %s. Si le problème persiste, n\'hésitez pas à contacter notre support + Mauvaise carte sélectionnée dans le Tangem App + Échec de la création de la transaction à partir des données de la Dapp. Code: %s + Nous avons rencontré une erreur inconnue. Code d\'erreur : %d. Si le problème persiste, n\'hésitez pas à contacter notre support + Aucune session WalletConnect ouverte + Oups. Aucune session. + Échec de l\'appairage de la session WalletConnect : %1$s + Coller depuis le presse-papiers + Message pour %1$s :\n%2$s + Demande de démarrage d\'une session pour\n%1$s\nRÉSEAU : %2$s\n\nURL : %3$s + L\'opération n\'a pas pu être effectuée.\n\nVous avez déjà établi une session WalletConnect avec ces paramètres. + Scanner un nouveau code + Cette carte ne peut pas être utilisée pour établir une session WalletConnect + Ce réseau n\'est pas pris en charge. Veuillez sélectionner un autre réseau. + Sélectionnez un réseau + Sessions WalletConnect + Se connecter aux dApps WalletConnect + La connexion peut prendre quelques secondes + %s Prix du marché + dernières 24h + %s réseau L\'adresse a été copiée avec succès Pas de connexion internet + Paramètres du portefeuille Tangem + Utilisez %s ou scannez une carte pour déverrouiller l\'accès à votre portefeuille + Erreur d\'activation + Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. + BNB Beacon Chain va s\'arrêter de fonctionner + Pourrait être mieux + J\'aime Ok, je l\'ai! + Vraiment cool ! + Rafraîchir + Vous êtes actuellement en mode démo + Mode démo actif + La carte que vous avez scannée est une carte de développeur. Ne l\'utilisez pas pour créer votre portefeuille. + Pas pour les utilisateurs ! + Le réseau %1$s nécessite un dépôt existentiel. Si votre compte descend en dessous de %2$s, il sera désactivé et les fonds restants seront détruits. + Le réseau nécessite un dépôt existentiel + L\'échange sera disponible une fois la %s transaction terminée. + Vous avez une transaction active + L\'approbation de l\'échange est en cours et sera achevée sous peu + Approbation en cours + Le montant minimum d\'échange est de %1$s. Veuillez vous assurer que le solde restant après l\'échange ne sera pas inférieur à %2$s. + Vous n\'avez pas de pièces échangeables %s dans votre liste + Aucun jeton disponible à échanger + Pour effectuer une transaction, vous devez déposer %1$s %2$s + Impossible de couvrir %s frais + Le montant à recevoir doit être d\'au moins %s + Service temporairement indisponible + Le nombre de jetons à échanger ne doit pas dépasser %s + Le montant à échanger doit être d\'au moins %s + Veuillez modifier le montant à échanger + Cette carte pourrait être un échantillon de production ou une contrefaçon + Échec de la vérification d\'authenticité + Associer + Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir. Frais d\'association ~%1$s %2$s + Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir + Associez votre jeton + %s insuffisant. Renflouez votre compte Hedera pour associer ce jeton + Il ne reste que %s signatures sur cette carte. Vous devez retirer tous vos fonds. + Faible nombre de signatures + Les jetons sur différents réseaux peuvent avoir des adresses différentes. Vérifiez bien que votre adresse correspond au réseau lorsque vous transférez des fonds. + + Utilisez votre carte pour obtenir une adresse pour le réseau %d + Utilisez votre carte pour obtenir des adresses pour les réseaux %d + + Certaines adresses sont manquantes + Le réseau est actuellement inaccessible. Veuillez réessayer plus tard. + Le réseau est inaccessible + Renflouez votre portefeuille + Votre portefeuille n\'a pas été sauvegardé. Effectuez cette procédure pour protéger vos actifs dès maintenant. + Sauvegarde manquante + Cette carte a déjà été utilisée pour des transactions. Si elle provient d\'une source non fiable, envisagez de retirer tous les fonds. S\'il s\'agit de votre carte, aucune action n\'est requise. + La carte a déjà signé des transactions + Votre avis nous motive à améliorer encore le Portefeuille Tangem + Vous appréciez Tangem ? + Vous devez associer votre jeton avant de recevoir des jetons + Frais de location de réseau requis + %1$s s\'agit d\'un actif du réseau %2$s. Pour effectuer une transaction %3$s, vous devez déposer une certaine quantité de %4$s (%5$s) pour couvrir les frais de réseau. + %1$s insuffisant pour couvrir les frais de réseau + Le réseau Solana est encombré. Si votre transaction n\'est pas traitée dans les 2 minutes, veuillez répéter la transaction. + Alerte réseau Solana + Le réseau Solana facture un loyer de %1$s tous les 2 jours. Les comptes qui ne peuvent pas se permettre le loyer sont purgés du réseau. Déposez sur votre compte plus de %2$s pour l\'utiliser gratuitement. + Certains réseaux sont actuellement inaccessibles. Veuillez réessayer plus tard. + Certains réseaux sont inaccessibles + Il s\'agit d\'une carte Testnet. Elle ne peut pas traiter les transactions et ne doit être utilisée qu\'à des fins de test et de développement. + À des fins de test uniquement + Ignorer + Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? + Oui, reprendre + Ignorer + Si vous ignorez la sauvegarde maintenant, vous devrez réinitialiser les cartes aux paramètres d\'usine pour recommencer + Reprendre la sauvegarde + C\'est une action irréversible + Se connecter avec %s + Scanner la carte + Utiliser %s ou scanner une carte pour accéder à l\'application + Bon retour ! diff --git a/core/res/src/main/res/values-ja/strings-blockchain.xml b/core/res/src/main/res/values-ja/strings-blockchain.xml new file mode 100644 index 0000000000..5c5a94491d --- /dev/null +++ b/core/res/src/main/res/values-ja/strings-blockchain.xml @@ -0,0 +1,25 @@ + + + デフォルト + レガシー + 手数料を取得できませんでした + %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 + 取引に必要な資金が不足しています。アカウントに入金してください。 + エラーが発生しました。コード: %s 。 + %1$sネットワークを使用するには、アカウント準備金 ( %2$s %3$s ) を支払う必要があります。これにより、その金額はロックされ、無期限に非表示になります。 + 送信先アカウントが有効ではありません。%s 以上を送信してアカウントを有効にしてください。 + アカウントを作成するには、このアドレスに資金を送金してください + 最低額は%sです + 値が小さすぎます + 無効な手数料 + 最低残高は%sです + 対象アカウントは作成されていません。送信金額は、%s + 手数料またはそれ以上である必要があります。 + 不明なエラー + 金額が残高を超えています + 無効な金額 + 手数料が残高を超えています + 合計金額が残高を超えています + いいえ、すべて送信します + %s XTZを減らす + 次回ウォレットにチャージするときに手数料の増加を避けるには、金額を%s XTZ減らしてください。 + diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..e004bcbd28 --- /dev/null +++ b/core/res/src/main/res/values-ja/strings.xml @@ -0,0 +1,864 @@ + + + ネットワークを選択 + カスタムトークンの追加 + トークンの管理 + このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 + スキャン方法 + サポートをリクエストする + もう一度やり直してください + この機能はデモモードでは無効です + 理由: %s + 取引を送信できません + 選択したものは%1$sネットワークをサポートしていません。 + %1$sブロックチェーンの暗号化を有効にするには、ウォレットを工場出荷時の設定にリセットする必要があります。リセットする前に出金して、資金が失われないようにしてから、リセット処理を完了してください。リセット後は、現在のウォレットにアクセスできなくなります。 + %1$s ネットワークのトークンは、ファームウェアの制限により、このカードではサポートされていません。 + カードのスキャンに問題がありますか? + このカードはこのアプリでは使用できません。 + デフォルト手数料 + デフォルト手数料を有効にすると、取引手数料が自動的に設定され、送金時に手数料ページを表示する必要がなくなります。必要に応じて、いつでもこのページに戻ることができます。 + 設定に移動して、Tangemアプリで生体認証を有効にします。 + 生体認証を有効にする + これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 + 保存したカードを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 + アクセスコードを保存 + カードとのやり取りには、アクセスコードの代わりに生体認証が要求されます。 + ウォレットをアプリに保存する + すべてのウォレットをTangemアプリにリンクできるようにします。アプリのロックを解除するには、生体認証が必要になります。取引の署名には、Tangemカードをタップする必要があります。 + ダーク + ライト + システムのデフォルト + テーマ + アプリ設定 + 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 + 今後表示しない + わかりました + 残高は非表示 + カードをスキャンしてください + 30秒後に再試行するか、カードをスキャンしてください。 + 試行回数が多すぎます + お使いの携帯電話で生体認証が無効になっているため、アプリにウォレットを保存できません。ウォレットを保存するには、携帯電話の設定で生体認証機能を有効にしてください。 + バックアップ処理を開始する + 法定通貨カードまたは銀行口座から + + %d カード + + このカードを使用して、このウォレット内の他のカードのアクセスコードをリセットしたくない場合は、このオプションを無効にしてください。これにより、このカードのアクセスコードもリセットできなくなりますのでご注意ください。 + このカードを使用して、このウォレット内の他のカードのアクセスコードをリセットできます。 + アクセスコードの復元 + リセット + 本当に実行しますか? + アクセスコードの変更 + アクセスコードは、このカードのみで変更されます + 選択したウォレットのすべてのカードが、工場出荷時の設定にリセットされました。これで、新しいウォレットを作成できます。 + リセット完了 + このウォレットの次のカードをリセットしますか? + カードのリセット + このウォレットのすべてのカードをリセットすることをお勧めします。 + すべてのカードのリセットが完了していません。 + 工場出荷時の設定にリセット + セキュリティ・モード + カード設定 + ネットワーク手数料に加えて、Cardanoネットワークは%2$sトークンで取引する際に%1$s ADAを請求します。 + Cardanoの取引要件 + %1$s 取引を行うには、いくらかのADAを入金し、ネットワーク手数料と最小ADA (5ADAを推奨) をカバーする必要があります。 + トークンを送るために必要なADAが不足しています。 + カルダノブロックチェーン上にトークンを持っているため、いくらかのADA残高を維持する必要があります。 + ADAが不足しています。 + 受け入れる + アクセスが拒否されました + すべて + 許可する + 適用する + 承認 + 承認 + 注意 + 残高: %s + 残高 + 生体認証 + 生体認証 + 買う + %1$sへ移動 + カメラへのアクセスを許可していません。プライバシー設定を調整してください。 + キャンセル + 報酬を受け取る + 閉じる + 続ける + コピー + アドレスをコピー + 作成 + 設定 + + + + 削除 + 無効 + 完了 + 有効にする + 有効 + エラー + 移動する + 取引履歴を調べる + エクスプローラー + 手数料 + ネットワーク手数料は、取引の処理と確認のためにユーザーが支払う料金です。手数料の額は、ネットワークの混雑さ、取引のサイズ、実行の優先度によって左右されます。 %s + 速い + マーケット + 遅い + 速度と料金 + アドレスを取得する + プロバイダーへ移動 + インポート + 後で + ロックされています + メインネットワーク + ネットワーク手数料 + 送金額は、選択された手数料レベルをカバーするため、%1$s (%2$s) 減額されます。 + + いいえ + アドレスがありません + + わかりました + プライマリーカード + パスフレーズ + ペースト + %1$s-%2$s + 続きを読む + 受け取る + 拒否 + リロード + 名前を変更 + 保存 + 変更内容を保存 + 検索 + トークンを検索 + シードフレーズ + アクションを選択 + 売る + 送る + サーバーが利用できません。しばらくしてからもう一度お試しください。 + 共有 + 署名 + 署名して送信 + ステーキング + ステーキング + 始める + 提出する + 成功 + サポート + スワップ + 利用規約 + 今日 + 取引が失敗しました + 取引 + 送金 + わかりました + エラーが発生しました。もう一度お試しください。 + アクセスできません + ステーキング解除 + はい + コントラクトアドレスをコピーしました! + 利用可能なネットワーク + トークンを追加 + コントラクトアドレス + コントラクトアドレスが無効です + ネットワークを選択してください + 小数は%liまでの有効な整数である必要があります + カスタム派生パス + 例:m/00\'/0000\'/0\'/0/0 + カスタム派生パスを入力 + 小数 + 派生パス + デフォルト + BIP44コインタイプ + 入力した派生パスは無効です + USD Coin + 名前 + 未選択 + ネットワーク + トークン・ネットワーク + Tangemでネイティブにサポートされていないトークンを手動で追加できます。 + 例: USDC + シンボル + トークン・シンボル + このトークン/ネットワークはすでにリストに追加されています + トークンは誰でも作成できることに注意してください。詐欺トークンの追加には注意が必要ですが、費用はかかりません。 + 詐欺トークンを追加することには注意してください。費用がかからない場合があります。 + トークンは誰でも作成できることに注意してください。 + Tangemウォレットを購入 + チャット + アクセスコード + カードをスキャンする前に、正しいアクセスコードを送信する必要があります。 + 長くタップ + このメカニズムは、カードに対する近接攻撃から保護します。コマンドの受信と実行の間に遅延を強制します。 + パスコード + カードの状態の変更を伴うコマンドを実行する前に、パスコードを入力する必要があります。 + 紹介プログラム + デバイスの画面を下に向けると、残高をすばやく非表示にしたり表示したりできます。 + %sハッシュ + カードID + サポートへのお問い合わせ + 他のカードをリンクする + アプリ通貨 + フリップして残高を非表示にする + 発行者 + 署名済み + フィードバックを送信 + 詳細 + インターネット接続を確認するか、別のネットワークに切り替えてください。 + 利用規約 + 別のウォレットのカードを使用しました。このウォレットに関連付けられているカードをタップしてください。 + マイトークン + まだトークンを追加していません。マーケットからトークンを追加して交換してください。 + %sとの交換はできません + 提供元 + ステータス + Tangemは、各プロバイダーの条件に従って、サードパーティプロバイダーを介してトークンスワップを提供します。 + プロバイダーを選択 + エラーが発生しました。コード: %s + おっと!このプロバイダーで選択したペアを交換することは一時的にできません。後でもう一度お試しください。(Code:%s) + 選択したプロバイダーは現在利用できません。しばらくしてからもう一度お試しください。(コード: %s ) + 現在、交換はご利用いただけません。しばらくしてからもう一度お試しください。(コード: %s ) + 推定金額 + %sによる交換 + 返金を受けるには、プロバイダーのウェブサイトにアクセスしてください。 + プロバイダーによる操作が失敗しました。 + 確認するには、プロバイダーのウェブサイトにアクセスしてください。 + プロバイダーによる本人確認手続きが必要です。 + キャンセルされました + 確認済み + 確認中 + 確認中... + 交換済み + 交換中 + 交換中... + 失敗しました + 入金済み + 入金待ち + 入金待ち... + 返金済み + あなたへ送金しています + 送金中... + 送金済み + プロバイダー提供のデータ。推定額は市場動向により変更される場合があります。 + 交換ステータス + 確認が必要です + 取引ハッシュを待機中 + ウォレットに追加されたすべてのトークンのリスト + 最良のレートを取得しています... + 変動レート + スワップ機能を使用すると、プロバイダーの%sに同意したことになります。 + スワップ機能を使用すると、プロバイダーの%1$sおよび%2$sに同意したことになります。 + さらに多くのプロバイダーを追加予定です。 \nお楽しみに。 + プライバシーポリシー + プロバイダー + ベストレート + 最大 %s まで使用可能 + %s 以上で利用可能 + このペアは利用できません + 許可が必要です + 推奨 + 利用規約 + トークンが見つかりません。別のリクエストをお試しください。 + ID: %s + 取引IDをコピーしました + ウォレット内の別の通貨から + 以下の情報はオプションです。共有したくない場合は消去できます。 + どのような機能が不足しているかを教えください。解決できるよう尽力致します。 + お持ちのカードについて教えてください + サポートチームの皆さん、こんにちは。 + 問題について詳しく教えてください。どんな些細なことでも役に立ちます。 + 私の提案 + カードをスキャンできません + フィードバック + Tangemへのフィードバック + 取引を送信できません + 現在の取引 + ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 + 選択したトークンの承認制限を指定します + 金額%s + 承認機能は、別のアドレスに特定の量のトークンを使用する許可を与えるために必要です。設計上スマートコントラクトは、承認しない限りトークンにアクセスできません。トークンを「ロック解除」すると、StakeKitスマート コントラクトがトークンを使用する権限が与えられます。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためにガス料金(あなたが支払う)を受け取ります。承認後、トークンをステーキングできます。 + 続行するには、StakeKitスマートコントラクトが%sを使用することを許可する必要があります + 続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください + 許可を与える + 無制限 + カードを注文 + カードをスキャン + アクセスコードを変更するには、上図のようにカードをタップし、操作が終了するまで取り外さないでください。 + パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 + ウォレットを作成するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 + ウォレットの%s番目のカードをタップします + タップしてスキャン + タップして署名 + カードをタップ + 生体認証を更新しました。カードをスキャンして入ってください。 + 送金するには、残高が手数料額より高くなければなりません。 + 残高不足 + この取引に必要なManaが不足しています。Manaが補充されるまでお待ちください。あなたのMana残高は%1$s/%2$sです。 + Manaが不足しています + KoinosネットワークによるMana制限のため、%s のみ送金できます。 + Mana上限 + Koinosネットワークでは、ネットワーク手数料としてManaが必要です。あなたは%1$s / %2$sManaを持っています。 + Manaレベル + 暗号資産および取引の追跡を開始するには、トークンを追加してください + トークンの管理 + すべてのネットワークにアクセスするには、カードをスキャンする必要があります。 + カードをスキャンする + 2月%2$s - %3$sの期間、Changelly経由のスワップは%1$sのサービス手数料となります。 + Changellyでスワップ、手数料%s + トークン + 追加 + 編集 + コインの時価総額 + この暗号資産が最初に生成されたブロックチェーン + ネイティブ・ネットワーク + 非ネイティブネットワークを使用すると、ブロックチェーン間の相互運用性が実現し、トークンをさまざまな分散型アプリケーションやプラットフォーム間のスマートコントラクトで利用できるようになります。ただし、これには多くの場合、元の資産を安全に保管するためのカストディアンまたはスマートコントラクトが必要となり、中央集権化とカウンターパーティリスクが生じます。 + トークンがホストされているオリジナルまたは主要なブロックチェーンではありません + 非ネイティブ・ネットワーク + ネットワークを選択 + ウォレット + トークンが見つかりませんでした。手動で追加できます。 + + %1$d の %2$d ウォレット\n%1$d の %2$d ウォレット + + 削除 + 例:私はBTCを信じる、HODLしなければならない + ポートフォリオが更新されました + 選択したトークンは現在、暗号資産ウォレット内でのアクションには利用できません。しかし、心配しないでください。賛成票を投じることで関心を表明できます。 + 賛成票を投じる + ウォレットを選択 + ウォレットは複数のネットワークをサポートしていません。 + このアセットの購入、交換、受け取りを開始するには、このトークンを少なくとも1つのネットワークに追加してください。 + このアセットは利用できません + ポートフォリオに追加 + トークンを追加 + 利用可能なネットワーク + 私のポートフォリオ + マーケット + 選択したネットワークのアドレスを生成するには、Tangemカードをタップする必要があります + データを読み込めません… + クイックアクション + 結果 + 時価総額10万ドル以下のトークンを見る + トークンを表示 + 検索結果はありません + ネットワークを選択 + ウォレットを選択 + 1ヶ月 + 1年 + 24時間 + 3ヶ月 + 6ヶ月 + 7日 + すべて + 経験豊富な買い手 + 格付け + 並べ替え + 上昇率上位 + 下落率上位 + トレンド + %sについて + + %d のレーティングに基づいて + + ブロックチェーンサイト + 買い圧力 + 買い手と売り手の取引量の差 + 循環供給量 + 取引可能で市場に流通しているコインの総数 + 経験豊富な買い手 + 少なくとも100の発信取引を持つネット・バイヤー + 完全希薄化後評価額 + 現在流通していないものも含め、存在する可能性のあるすべてのコインが流通している場合の暗号資産の理論上の合計価値 + ジェネシスの日付 + 高い + ホルダー + 特定の期間内のトークンホルダー数の変化 + インサイト + リンク + 流動性 + 指定された期間中のトークンに利用可能な流動性の変化 + 流動性指数 + 低い + 時価総額 + 暗号資産の市場価値の合計。コインの現在の価格と、流通しているコインの総数を掛けて計算されます。 + 市場格付け + 時価総額に基づくすべてのコイン間の暗号資産評価における位置 + 最大供給量 + 指標 + 公式リンク + 値動き + リポジトリ + セキュリティ・スコア + ソーシャル + 総供給量 + 特定の暗号資産に存在しうるコインまたはトークンの最大数 + 取引量(24時間) + 過去24時間以内に取引された暗号資産の合計額。市場の活発さと流動性を示します。 + すべてのカードを保護するには、単一のアクセスコードを設定する必要があります + 保護する + 後で各カードに個別のアクセスコードを設定できます + パーソナライズする + アクセスコードはリンクされたカード1枚で復元できます。すべてのカードを1か所に保管しないでください。 + 復元する + アクセスコードとして任意の単語、フレーズ、または数字を選択してください + アクセスコードの作成 + 間違いを避けるため、アクセスコードをもう一度入力してください。 + アクセスコードの再入力 + アクセスコードは4文字以上である必要があります + 入力したアクセスコードが、初期アクセスコードと一致しませんでした + 操作を繰り返してください。カードは工場出荷時の設定にリセットされます。 + アクティベーションに失敗しました + トークンを追加する + バックアップカードを1枚追加しました。バックアップ処理が終了すると、これ以上バックアップカードを追加することはできません。もう一枚カードがあれば、バックアップに追加してください。バックアップ処理を続けますか? + バックアップ手続きは部分的に完了しています。現在終了することはできません。 + パスフレーズは、暗号資産ウォレットが使用する高度なセキュリティ機能です。既存のリカバリフレーズに、自分で選択した単語またはフレーズを追加することで、まったく新しいアドレスのロック解除が可能になります。 + バックアップカードを追加する + カード #%d をスキャン + 今すぐバックアップ + プライマリカードをスキャン + ウォレットへ進む + バックアップを完了する + 暗号資産を受け取る + プライマリカードをスキャン + スキップする + どのように機能しますか? + すべての秘密鍵をカード内で生成し、安全なウォレットを作成しましょう + ウォレットを作成する + ウォレットを作成する + その他のオプション + 秘密鍵はカード内で安全に生成されます。シードフレーズは存在しないので、誰もエクスポートしたり盗んだりすることはできません。 + 秘密鍵を非公開で生成する + カードは有効化され、使用可能になりました + 成功! + この場合は、最初からやり直す必要があります。 + アクティベーション処理を途中で終了しますか? + スタート + 追加しようとしているカードには、すでに別のウォレットが作成されています。このウォレットに資金がある場合は、それを引き出してからこのカードをリセットし、バックアップとして追加してください。 + バックアップの作成 + シードフレーズについてもっと読む + + これらの %d 単語を以下の順番通りに書き留め、安全かつ秘密の場所に保管してください。 + + あなたのシードフレーズ + + %d 単語 + + ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 + シードフレーズを生成する + ウォレットをインポートする + シードフレーズは、ウォレットを復元できる一連の単語です。カードによって生成される秘密鍵とは異なり、シードフレーズは保護されていないため、コピーされて盗まれる可能性があります。このオプションは自己責任で使用してください。 + シードフレーズを使用する + 無効なシードフレーズです。語順を確認してください。 + 無効なシードフレーズです。スペルを確認してください。 + レガシー + シードフレーズを正しく書き込んだかどうかを確認するために、2番目、7番目、11番目の単語を入力してください。 + では、確認してみましょう + バックアップ処理を開始するには、最大2枚のバックアップカードを追加します。 + カードをもう1枚追加するか、バックアップ処理を完了します + 番号%sのバックアップカードを準備してください + プライマリカードをスキャンして、バックアップ処理を開始します。 + 番号%sのプライマリカードを準備してください + ウォレットカードが設定され、使用できるようになりました。 + 最大枚数のカードが追加されました。バックアップ処理を完了します。 + カードの有効化 + バックアップカード # %d + バックアップカードなし + 通知 + バックアップカードが1枚追加されました + カードを準備してください + バックアップカード2枚が追加されました + 始めるには、ウォレットに任意の金額を入金するだけです + 始めるには、ウォレットに%1$s %2$s以上入金するだけです + 暗号資産を購入する + ウォレットのアドレスを表示する + ウォレットを有効化する + ツイン化プロセスは部分的に完了しています。現在終了することはできません。 + ウォレットの作成プロセスが何らかの理由で中断された場合は、最初からやり直す必要があります。 + 秘密鍵のバックアップは、未使用のTangemウォレットカード2枚まで可能です。 + アクセスコードは、バックアップカード1枚を使用して復元できます。 + すべてのバックアップカードは同一の秘密鍵を使用しており、すべての機能を使用できます。 + ウォレットを保護するためにアクセスコードを設定できます。 + バックアップウォレット + アクセスコードの復元 + 同一のカード + アクセスコード + グループ + 残高順 + トークンを整理する + グループ解除 + ギャラリーから選択 + 設定 + カメラへのアクセスを許可していません。 + カメラへのアクセスが拒否されました + %3$sネットワーク上の%1$s ( %2$s ) + このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。 + QRコードを表示するか、アドレスを共有します + 参加する + 紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。 + 紹介プログラムに関する情報を読み込めませんでした。エラー コード: %s 。しばらくしてからもう一度お試しください。 + 今後のお支払い + あなたの友達が購入した + 少ない + 多い + お支払いの予定はありません + + %dのウォレット + + あなたの%2$sネットワークアドレス%3$sで友達が購入したウォレットごとに、^^30日後 ^^^^ %1$s ^^ をゲットできます + あなた + 以下を取得します。 + tangem.comでウォレットを購入するとき + %s割引 + あなたの友達 + パーソナルコードをコピーしました! + パーソナルコード + Tangemウォレットを割引価格で購入!\n%s + 友達にTangemを紹介 + 承諾しました + このボタンをタップすると、同意したことになります + 紹介プログラム + + %d ウォレット + + カードをリセットする + この操作を実行すると、現在のウォレットにアクセスできなくなることを理解しています。 + このカードを使用して、現在のウォレットの他のカードのアクセスコードを回復させられないことを認識しています。 + 工場出荷時設定にリセットすると、選択したカードからウォレットが完全に削除されます。現在のウォレットを復元したり、カードを使用してアクセスコードを回復したりすることはできません。 + 工場出荷時の状態にリセットすると、選択したカードからウォレットが完全に削除され、アプリから削除されます。現在のウォレットを復元することはできません。 + 他の国の銀行カードと、ロシア連邦外での居住許可証または登録をお持ちですか? + ロシアの銀行カードは現在ご利用いただけません + アプリにログインして、カードをスキャンせずに残高を確認できます + アプリにアクセスする + 生体認証の使用を許可する + ウォレットの操作には、アクセスコードの代わりに生体認証が要求されます。 + アクセスコード + 生体認証が無効になっているようです。ウォレットを保存する必要があります。 + 生体認証を有効にする + 生体認証を使用しますか? + 資金を使って取引を行うには、カードが必要になりますのでご注意ください。 + カードをスキャン + カードをスキャンして設定を変更します。変更はスキャンしたカードにのみ影響し、ウォレットに関連付けられている他のカードには影響しません。 + カードを準備してください! + 入力したアドレスにすでに含まれています + 手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。 + 推奨手数料を下回る手数料が指定されたため、取引に遅れが生じる可能性があります。続行しますか? + 理由: %1$s \nコード: %2$s + 取引は完了していません + 金額 + 取引手数料は、vByteフィールドのSatoshiの値を調整して設定できます。 + 取引にかかる手数料です。自由に設定できます。 + 最大手数料 + 各ガスに支払う最大コストです。ガス代が高いほど、トランザクションは早く処理されます。(優先手数料を含みます) + 優先手数料 + ブロックに自分の取引が含まれるのを早めるために、ユーザーがマイナーやバリデーターに支払う手数料です。 + Kaspaネットワークで未使用の取引出力(UTXO)を使用するために必要な手数料です。取引で使用するUTXOが多ければ多いほど、手数料は高くなります。 + UTXOあたりのKAS + %1$s 、 %2$s + アドレス + 宛先タグ + アドレスを入力 + アドレスはウォレットアドレスと同じです + 無効なタグです。取引には追加されません。 + 無効なメモです。取引には追加されません。 + タグ + メモ + 手数料込み + 低い + 普通 + 優先 + ネットワーク接続を確認してください + ネットワーク手数料についての情報にアクセスできません + より + ガス上限 + これは、取引または契約を完了するために使用されるガスの最大額です。ガス上限を設定することで、取引実行時に予期せぬ請求や無制限の請求を防ぐことができます。 + ガス代 + 各ガスに支払う最大コストです。ガス代が高いほど、取引は早く処理されます。 + 最大 + 最大金額 + 最大手数料 + 無効なメモ + ネットワーク手数料のカバー + 手数料と送金額の合計が残高を超えているため、送金に必要な資金が不足しています。 + 合計が残高を超えています + 残高が最低量を下回ると、当アカウントはブロックチェーンから消去されます。残高に%sを残しておいてください。 + アカウント維持に必要な最低残高 + 手数料額が推奨額の%s倍となっています。カスタム設定が正しいことを再度確認してください。 + カスタム手数料が高くなっています + %1$sネットワークの特殊性により、残高全体を転送する場合の手数料は高くなります。手数料を削減するには、 %2$sを残します。 + 手数料が高くなっています + 手数料が送金金額を超えており、マイナスの値になってしまいます。 + 無効な金額 + 最低送金金額は%1$sです。送金後の残高が%2$s未満にならないようにしてください。 + 対象口座が作成されていません。送金額を変更してください。 + 送金額は%s以上である必要があります。 + %s を残します + %sを減らす + %s に減らします。 + 手数料の設定によっては、取引に遅延が発生する場合があります。 + 取引遅延の可能性があります + %1$sの制限により、1つの取引に収まるUTXOは%2$s個のみです。つまり、 %3$s個以下しか送信できません。量を減らす必要があります。 + 取引制限 + オプション + QRコードを正方形に合わせてスキャンしてください。%s ネットワークアドレスをスキャンしてください。 + 最近の取引 + 受取人 + 有効なアドレスではありません + トークンを失わないように、受信ウォレットアドレスが%sネットワーク上にあることを確認してください。 + 送金先 + メモ/宛先タグは、同じネットワーク上の同じ受信者に送信された取引を区別するためのユニークなIDです。注意:メモを省略すると、資金の行き違いにつながる可能性があります。 + マイウォレット + Bitcoinの取引手数料を測定する方法。取引における各仮想バイトの最小Bitcoin単位(Satoshi)の数を示します。数値が高いほど、マイナーによる取引の処理速度が速くなります。 + Satoshi / vByte + 送金中... + 変更するには任意の箇所をタップしてください + %sを送金する + ** %1$s ** を送金する (ネットワーク手数料%2$sを含む) + **%1$s** と %2$s を送金しています。 + %sを送信しています + 合計 + %1$sと%2$sが送信されます + ≈ %1$s (%2$s: 手数料を含む) + %sが送信されます + 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 + 無効なアドレス + %1$s ( %2$s ) + 取引が送信されました + セットアップしたいカードをスキャンするために準備してください。 + ウォレット削除 + これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 + 名前 + アクティブ + 資産のステーキングを解除するには、ここをクリックしてください。 + ステーキング金額は %s 以上である必要があります + APR + APY + ステーキングに参加することで得られる年間収益率。 + 利用可能 + 平均報酬率 + %s 推定利益 + 市場評価 + 指標 + 最低要件 + 請求できる報酬はありません + ステーキング中 + 請求中の報酬 + ステーキング報酬を受け取る方法。自動または手動で請求できます。 + 報酬スケジュール + これは、ステーキングの参加者がいつ報酬を受け取るかを決定するスケジュールです。 + 受け取る報酬: %s + ステーキング%s + 解約完了までの期間 + ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 + ウォームアップ期間 + ステーキングへの参加を有効にするために割り当てられた時間。 + ネイティブステーキング + ステーキングにより%1sを獲得できます。ステーキング報酬は ~ %2s日ごとに届きます。 + ステーキング報酬を獲得 + 報酬 + もっとステーキングする + スタックされていない + 資産を請求するために、unstakedを確認してください + バリデーター + カード内に秘密鍵を保管しながら暗号資産を安全に保管します + 革新的なハードウェアウォレット + 1つのウォレットに最大3枚のカード + 安全なバックアップ + ビットコイン、イーサリアム、その他多くの暗号資産を同時に管理できるハードウェアウォレット – オールインワンカード + 数千の通貨 + 外出先でも、いつでもどこでも使用できます。コードや電池は不要です。暗号資産が必要なときに、カードをスマートフォンにタップするだけです。 + すべての人のためのウォレット + Tangemのご紹介 + 100種類以上の分散型サービスで、NFTの交換・購入や、借入・預金を行うことができます。 + Web3.0対応 + より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 + 新しいスワッププロバイダーが利用可能になりました! + この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 + この金額には、サービスプロバイダーの手数料が含まれています。 + 手数料 + すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金(あなたが支払う)を受け取ります。承認後、トークンを交換することができます。 + 承認 + 手数料見積りエラーです。サポートにフィードバックをお送りください。 + スワップする + 選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。 + 残高不足 + 許可を与える + 進行中 + スワップ + 受け取る + トークンを選択 + 利用不可 + 残高非表示 + 残高表示 + 元に戻す + この操作は現在利用できません。しばらくしてからもう一度お試しください。 + 現在、 %sの買付はご利用いただけません。アップデート情報をご確認ください。 + 売却できる資金がありません。アカウントに入金して、売却できるようにしてください。 + 送金する資金がありません。アカウントに入金して、送金できるようにしてください。 + 現在、 %sの交換はご利用いただけません。アップデート情報を確認してください。 + ネットワーク%s内の保留中の取引が完了すると、資金の売却が可能になります。 + ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 + 現在、 %sの売却はご利用いただけません。アップデート情報をご確認ください。 + %s のステーキングは現在ご利用いただけません。最新情報をご確認ください。 + XPUBを生成する + 非表示 + このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 + %sを非表示 + トークンを非表示 + ステーキングにより、 %1$sを獲得し、 %2$s日ごとに報酬を受け取ることができます。 + 年間最大%sのステーキング報酬を獲得 + %%image%% %2$s ネットワークの %1$s トークン + %%image%% %1$sネットワーク上のトークン + %1$s ( %2$s ) トークンは%3$sネットワークの主要通貨であり、このネットワーク上の他のトークンがリストにある限り、非表示にすることはできません。 + %sを非表示にできません + このトークンは、2月%2$s-%3$s の間、%1$s のサービス手数料で別のトークンと交換できます。 + Changellyでスワップ、手数料%s + 今すぐスワップ + コントラクト: %s + まだ取引はありません + 取引履歴の読み込みに失敗しました。\n情報を更新するには、リロードボタンをクリックしてください。 + 複数のアドレス + 現在、このブロックチェーンでは取引履歴はサポートされていません。しかしご心配なく!弊社で対応中です。その間、エクスプローラーで確認することができます。 + オペレーション + 送金元: %s + 送金先: %s + もう一度やり直してください + 同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。 + 間違ったツインカードをスキャンしました。別のカードをお試しください。 + あなたが今手にしているカード、そしてもうひとつが番号%s のカードです。\n\nどちらのカードも、このウォレットから資金を引き出すために使用できます。 + 1つのウォレット。2枚のカード。 + カード #%s をスキャン + ウォレットの作成 + # %sツインカードをスキャン + カードの準備 + Tangemツイン + この操作は元に戻せません。古いウォレットにはアクセスできなくなります。 + 番号%sのツインカードをタップし、操作が終了するまで取り外さないでください。 + %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + 最新の機能とニュースをお届けします + 新しいプロモーション情報をいち早く入手しましょう + プッシュ通知を使用しますか? + 新しいウォレットを追加 + 本当にこのウォレットを削除してもよろしいですか? + エラーが発生しました。カードをスキャンしてログインしてください。 + このウォレットはすでに保存されています。別のウォレットを追加できます。 + %sという名前のウォレットはすでに存在します。 + ウォレット名 + ウォレット名の変更 + すべてのロックを解除 + %sですべてをロック解除 + ブロックチェーンにアクセスできません。後でもう一度お試しください。 + カードをスキャン + メッセージに署名することを要求しています。 \n\n %s + Dapp%1$s 、BNB取引の署名を要求しています\n\n%2$s + %1$sの取引注文\n価格: %2$s\n受取金額: %3$s\n支払金額: %4$s + 取引の詳細:\n送信元: %1$s\n受取先: %2$s\n量: %3$s + クリップボードには WalletConnect コードが含まれています。コピーした値を使用するか、QRコードをスキャンしてください。 + %1$sの取引を作成するリクエスト\n%2$s\n\n金額: %3$s\n手数料: %4$s\n合計: %5$s\n残高: %6$s + 取引を送信できません。資金が足りません。 + WalletConnect セッションを確立できませんでした。しばらくしてからもう一度お試しください。 + メッセージの署名に失敗しました。\nもう一度お試しください。 + WalletConnectセッションの確立に失敗しました:タイムアウトエラー。しばらくしてもう一度お試しください。 + セッションリクエストには、WalletConnect接続でサポートされていないブロックチェーンが含まれています。サポートされていないブロックチェーン: \n + 技術的な実装のため、このDappとの接続は確立できません。 + 不明なエラーが発生しました。エラーメッセージ: %s 。問題が解決しない場合は、お気軽にサポートにお問い合わせください。 + Tangemアプリで間違ったカードが選択されました + Dappデータから取引を作成できませんでした。コード: %s + 不明なエラーが発生しました。エラーコード: %d 。問題が解決しない場合は、お気軽にサポートにお問い合わせください。 + WalletConnectセッションが開かれていません + おっと。セッションがありません。 + WalletConnectセッションのペアリングに失敗しました: %1$s + クリップボードから貼り付け + %1$sへのメッセージ:\n %2$s + セッション開始のリクエスト\n%1$s\n\nネットワーク: %2$s \n\n URL: %3$s + 操作を完了できませんでした。\n\nこのパラメータでWalletConnectセッションをすでに確立しています。 + 新しいコードをスキャンする + このカードは、WalletConnectセッションの確立には使用できません。 + このネットワークはサポートされていません。別のネットワークを選択してください。 + ネットワークを選択 + WalletConnectセッション + dAppsに接続する + WalletConnect + 接続には数秒かかる場合があります + %s市場価格 + 直近24時間 + %sネットワーク + アドレスがクリップボードにコピーされました + インターネット接続がありません + ウォレット設定 + Tangem + %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + アクティベーションに失敗しました + BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 + BNBビーコンチェーンは閉鎖されます。 + もっと良くなるはず + 気に入った + はい、わかりました! + すごくクールだ! + リフレッシュ + 現在デモモードです + デモモードが有効になっています + スキャンしたカードは開発者カードです。ウォレットの作成には使用しないでください。 + ユーザー向けではありません! + %1$sネットワークには、最低預金残高が必要です。残高が%2$sを下回ると、アカウントは無効になり、残りの資金は破棄されます。 + ネットワークには最低残高が必要です + スワップは、%s の取引完了後に利用可能となります。 + アクティブな取引があります + スワップの承認は現在進行中で、まもなく完了する予定です。 + 承認が進行中 + 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 + あなたのリストには、交換可能な %s トークンがありません。 + スワップ可能なトークンがありません + 取引を行うには、 %1$s %2$sを入金する必要があります。 + %s 手数料を支払えません + 受け取る金額は、 %s 以上である必要があります。 + サービスは一時的に利用できません + スワップするトークンの量は %s を超えないでください + スワップ金額は %s 以上である必要があります + スワップの金額を変更してください + このカードは、サンプル品または偽造品である可能性があります + 真正性チェックに失敗しました + 関連付ける + このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。関連付け手数料 ~ %1$s %2$s + このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。 + トークンを関連付ける + %sが不足しています。このトークンを関連付けるには、Hederaアカウントに資金を追加してください。 + このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。 + 署名数が少ないです + 異なるネットワーク上のトークンは、異なるアドレスを持つ場合があります。資金を送金する際には、アドレスがネットワークと一致していることを再確認してください。 + + %d ネットワークのアドレスを取得するために、カードを利用してください + + 一部のアドレスが見つかりません + 現在、ネットワークにアクセスできません。しばらくしてからもう一度お試しください。 + ネットワークにアクセスできません + ウォレットに入金 + ウォレットがバックアップされていません。アセットを保護するために今すぐこの手順を実行してください。 + バックアップがありません + このカードは以前取引に使用されたことがあります。信頼できない出所から受け取った場合は、全資金を引き出すことを検討してください。あなたのカードであれば、何もする必要はありません。 + カードはすでに取引に署名済みです + あなたのレビューは、Tangemウォレットをさらに良くするためのモチベーションになります + Tangemを楽しんでいますか? + トークンを受け取る前に、トークンを関連付ける必要があります。 + ネットワーク使用料が必要です + %1$s は %2$s ネットワーク内のアセットです。%3$s 取引を行うには、ネットワーク手数料のために %4$s (%5$s) を入金する必要があります。 + ネットワーク料金をカバーするための%1$sが足りません + Solanaネットワークが混雑しています。2分以内に取引が完了しない場合は、再度取引を繰り返してください。 + Solanaネットワークアラート + Solana ネットワークは 2 日ごとに%1$sのレンタル料を請求します。レンタル料を支払えないアカウントはネットワークから削除されます。アカウントに%2$s以上入金すると、無料で使用できます。 + 現在、一部のネットワークにアクセスできません。しばらくしてからもう一度お試しください。 + 一部のネットワークにアクセスできません + これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 + テスト目的のみ + 破棄 + バックアップが中断されました。再開しますか? + はい、再開します + 破棄 + バックアップを破棄する場合は、カードを工場出荷時の設定にリセットしてやり直す必要があります。 + バックアップを再開する + これはやり直しができないアクションです + %sでログイン + カードをスキャン + アプリにアクセスするには、 %sを使用するか、カードをスキャンしてください + お帰りなさい! + diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 701f32fb57..14c45c2681 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -303,9 +303,6 @@ Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии Токены - Забронировать - Оплатите его криптой и сэкономьте **50 долларов** через нашего партнера Travala: **%1s - %2s** - Забронируйте отпуск с Tangem Добавить Изменить Рыночная капитализация diff --git a/core/res/src/main/res/values-uk-rUA/strings-blockchain.xml b/core/res/src/main/res/values-uk-rUA/strings-blockchain.xml new file mode 100644 index 0000000000..d2c3ed6cdf --- /dev/null +++ b/core/res/src/main/res/values-uk-rUA/strings-blockchain.xml @@ -0,0 +1,25 @@ + + + За замовчуванням + Застарілий + Не вдалося отримати комісію + Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. + Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. + Виникла помилка. Код: %s. + Щоб користуватися мережею %1$s, ви маєте оплатити резерв акаунта (%2$s %3$s), який блокується і не використовується у вашому балансі + Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис. + Для створення акаунту надішліть кошти на цю адресу + Мінімальна сума: %s + Решта занадто мала + Невірна комісія + Мінімальний баланс: %s + Обліковий запис одержувача не створено. Сума для переказу повинна бути %s + комісія або більше + Невідома помилка + Сума перевищує залишок + Недопустима сума + Комісія перевищує залишок + Сума, що відправляється, перевищує залишок + Ні, відправити все + Зменшити на %s XTZ + Щоб не платити підвищену комісію при наступному поповненні гаманця, зменште суму на %s XTZ + diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml new file mode 100644 index 0000000000..c6e93b561a --- /dev/null +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -0,0 +1,827 @@ + + + Оберіть мережу + Додати токен + Керування токенами + Надсилайте тільки %1$s (%2$s) в мережі %3$s на цю адресу. Використання іншої мережі може призвести до втрати коштів. + Як сканувати + Звернутися в підтримку + Спробуйте ще раз + Ця функція недоступна в демонстраційному режимі + Причина: %s + Не вдається відправити транзакцію + Обраний гаманець не підтримує мережу %1$s + Щоб активувати криптографічне шифрування блокчейну %1$s, вам потрібно скинути налаштування гаманця до заводських. Зніміть свої кошти перед цим, щоб переконатися, що ви їх не втратите, а потім завершіть процес скидання. Вхід до поточного гаманця буде неможливий після скидання. + Токени в мережі %1$s не підтримуються цією карткою через обмеження прошивки. + У вас виникли труднощі зі скануванням картки? + Ця картка не призначена для роботи з цим додатком + Комісія за замовчуванням + Увімкніть комісію за замовчуванням, щоб автоматично встановлювати комісію за транзакцію і пропускати сторінку комісій під час відправлення коштів. Ви завжди можете повернутися до цієї сторінки за необхідності. + Перейдіть до налаштувань, щоб увімкнути біометричну автентифікацію в додатку Tangem + Увімкнути біометричну автентифікацію + Усі збережені коди доступу будуть видалені. Вам доведеться вводити код доступу при роботі з гаманцем. + При відключенні функції збереження гаманця всі збережені до цього гаманці будуть видалені зі застосунку. + Зберегти код доступу + Підключіть функцію збереження кодів доступу від карток на телефоні у зашифрованому виді, і при роботі з карткою замість коду доступу буде запитуватися біометрична автентифікація. + Зберігати гаманець у додатку + Увімкніть функцію привʼязки карток у застосунку, а також можливість біометричної автентифікації. Підпис транзакції й надалі буде потребувати картку. + Темна + Світла + Системна + Тема + Налаштування застосунку + Щоб приховати або показати свій баланс, просто переверніть екран пристрою вниз або вимкніть його в налаштуваннях + Більше не показувати + Зрозуміло + Баланси приховані + Будь ласка, відскануйте картку + Будь ласка, повторіть спробу через 30 секунд або відскануйте картку + Забагато спроб + Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. + Почніть процес резервного копіювання + + %d картка + %d картки + %d карток + %d карток + + Вимкніть цю опцію, якщо ви не хочете, щоб ця картка використовувалася для скидання кодів доступу до інших карток у цьому гаманці. Зверніть увагу, що це також унеможливить скидання коду доступу на цій картці. + Дозволяє використовувати цю картку для скидання коду доступу на інших картках у цьому гаманці + Відновлення коду доступу + Скинути + Ви впевнені, що хочете це зробити? + Зміна коду доступу + Код доступу буде змінено лише на цій картці + Усі картки у вибраному гаманці було скинуто до заводських налаштувань. Тепер ви можете створити новий гаманець. + Скидання завершено + Ви хочете скинути наступну картку в цьому гаманці? + Скидання картки + Рекомендуємо завершити процес скидання для всіх карток у цьому гаманці + Ви скинули не всі картки + Скинути до заводських налаштувань + Тип безпеки + Налаштування картки + Крім мережевих комісій, мережа Cardano стягує %1$s ADA при транзакції з токеном %2$s + Вимоги до транзакції Cardano + Щоб здійснити транзакцію %1$s, внесіть певну суму ADA для покриття мережевої комісії та мінімальне значення ADA (рекомендовано 5 ADA) + Недостатньо ADA для транзакції + Ви повинні утримувати деяку кількість ADA, оскільки у вас на балансі є токени в мережі Cardano + Недостатньо ADA + Прийняти + Доступ заборонено + Усе + Дозволити + Застосовувати + Затвердження + Підтвердити + Увага + Баланс: %s + Баланс + біометрична автентифікація + біометрією + Купити + Перейдіть до %1$s + Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності + Скасувати + Закрити + Продовжити + Копіювати + Скопіювати адресу + Створити + Власна + + %d день + %d днів + %d днів + %d днів + + Видалити + Вимкнуто + Готово + Увімкнути + Увімкнено + Помилка + Оглядач + Переглянути історію транзакцій + Оглядач + Комісія + Мережеві комісії – це збори, які користувачі сплачують за обробку та підтвердження транзакцій. На розмір комісії може впливати перевантаження мережі, розмір транзакції та пріоритет виконання. %s + Швидко + За ринком + Повільно + Швидкість та комісія + Отримати адреси + Перейти до провайдера + Імпортувати + Пізніше + Заблокований + Основна мережа + Комісія мережі + Сума відправлення буде зменшена на %1$s (%2$s) для покриття обраного рівня комісії + Далі + Ні + Немає адреси + Зараз + ОК + Основна картка + Парольна фраза + Вставити + %1$s-%2$s + Детальніше + Отримати + Відхилити + Перезавантажити + Перейменувати + Зберегти + Зберегти зміни + Шукати + Пошук токенів + Seed-фраза + Оберіть дію + Продати + Надіслати + Сервер недоступний, спробуйте пізніше + Поширити + Підписати + Підписати та надіслати + Застейкати + Стейкінг + Почати + Надіслати + Успіх + Підтримка + Обмін + умови участі + Сьогодні + Помилка транзакції + Транзакції + Переказ + Я зрозумів + Виникла помилка. Будь ласка, спробуйте ще раз. + Недоступно + Так + Адреса контракту скопійована! + Доступні мережі + Додати токен + Адреса контракту + Адреса контракту недійсна + Будь ласка, оберіть мережу + Десяткове число повинно бути дійсним цілим числом, до %li + Власна деривація + Наприклад, m/00\'/0000\'/0\'/0/0 + Введіть власну деривацію + Знаків після коми + Шлях деривації + За замовчуванням + Тип монети BIP44 + Введений шлях деривації недійсний + Наприклад, USD Coin + Назва токену + Не обрано + Мережа + Мережа + Ви можете вручну додати токен, який не підтримується Tangem + Наприклад, USDC + Символ + Символ токену + Цей токен/мережа вже знаходяться у вашому списку + Зауважте, що токени можуть бути створені ким завгодно. Остерігайтеся шахрайських токенів, вони можуть нічого не коштувати. + Остерігайтеся шахрайських токенів, вони можуть нічого не коштувати + Зауважте, що токени можуть бути створені ким завгодно + Купити гаманець Tangem + Чат + Код доступу + Перед скануванням картки вам потрібно буде ввести правильний код доступу + Затримка сканування + Цей механізм захищає картку від безконтактних атак. Між скануванням картки та виконанням команди буде додана затримка. + Пароль + Перед виконанням будь-якої команди, що тягне за собою зміну стану картки, вам необхідно буде ввести пароль. + Реферальна програма + Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси + %s хешів + Номер картки + Звернутися у підтримку + Додати більше карток + Валюта застосунку + Приховувати баланси жестом перевороту + Емітент + Підписано + Надіслати відгук + Деталі + Перевірте підключення до інтернету або змініть мережу + Умови використання + Ви використали картку від іншого гаманця. Прикладіть картку, пов\'язану з цим гаманцем. + Мої токени + У вас немає доданих токенів. Додайте токени для обміну + Недоступний для обміну з %s + Послугу надає + Статус + Tangem пропонує обмін токенів через сторонніх провайдерів відповідно до умов кожного провайдера + Оберіть провайдера + Виникла помилка. Код: %s + На жаль, обмін вказаної пари через обраного провайдера тимчасово неможливий. Спробуйте здійснити обмін пізніше. (Код: %s) + Наразі обраний провайдер недоступний для обміну. Спробуй пізніше. (Код: %s) + Наразі обмін неможливий. Спробуй пізніше. (Код: %s) + Курс обміну + Обмін через %s + Щоб повернути ваші кошти, відвідайте сайт провайдера + Операція не виконана провайдером + Відвідайте сайт провайдера для перевірки + Провайдер вимагає проходження KYC верифікації + Скасовано + Підтверджено + Підтвердження + Підтвердження... + Обміняно + Обмін + Обмін... + Не вдалося + Депозит отримано + Очікуємо на депозит + Очікуємо на депозит... + Повернено + Надсилаємо вам + Надсилаємо вам... + Надіслано + Дані провайдера. Орієнтовна сума може бути змінена через ринкові умови. + Статус обміну + Потрібна верифікація + Список токенів у вашому гаманцю + Шукаємо найвигідніший курс... + Плаваюча ставка + Використовуючи сервіс обміну, ви погоджуєтеся з його %s + Використовуючи сервіс обміну, ви погоджуєтеся з його %1$s та %2$s + Незабаром з\'являться нові провайдери.\nСлідкуйте за новинами! + Політикою конфіденційності + Провайдер + Найкращий курс + Доступно до %s + Доступно від %s + Недоступно для цієї пари + Потрібен дозвіл + Умовами використання + Токенів не знайдено. Будь ласка, спробуйте інший запит + ID: %s + ID транзакції скопійовано + Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. + Розкажіть, яких функцій вам не вистачає, і ми спробуємо вам допомогти. + Розкажіть, будь ласка, яку картку ви маєте? + Привіт, команда підтримки, + Будь ласка, розкажіть нам більше про вашу проблему. Кожна дрібниця може допомогти. + Мої пропозиції + Не вдається відсканувати картку + Звернення в підтримку + Звернення в підтримку Tangem + Не вдається відправити транзакцію + Поточна транзакція + Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. + Вкажіть ліміт доступу для обраного токена + Кількість %s + Щоб продовжити, вам потрібно надати дозвіл смарт-контракту %1s використовувати ваш %2s + Надати дозвіл + Необмежено + Купити + Сканувати + Щоб змінити код доступу, прикладіть картку, як показано вище, і не прибирайте її до закінчення операції + Щоб змінити пароль, прикладіть картку, як показано вище, і не прибирайте її до закінчення операції + Щоб створити гаманець, прикладіть картку, як показано вище, і не прибирайте її до завершення операції + Прикладіть картку #%s для скидання + Прикладіть, щоб відсканувати + Прикладіть, щоб підписати + Прикладіть картку + Ви оновили біометричні дані, відскануйте свою картку для входу + Ваш баланс повинен перевищувати суму комісії для здійснення переказу + Недостатньо балансу + У вас недостатньо Mana для цієї транзакції. Будь ласка, зачекайте, поки Mana буде поповнена. Ваш баланс Mana становить %1$s/%2$s + Недостатньо Mana + Ви можете переказати тільки %s через обмеження Mana, встановлене мережею Koinos + Ліміт Mana + Мережа Koinos вимагає Mana для мережевої комісії. У вас є %1$s/%2$s Mana + Рівень Mana + Щоб почати відстежувати свої криптоактиви та транзакції, додайте токени + Керування токенами + Для доступу до всіх мереж необхідно відсканувати картку + Відскануйте картку + Насолоджуйтесь %1$s комісією за обслуговування на свопах через Changelly з %2$s по %3$s лютого. + Обмін із Changelly, %s комісії + Токени + Додати + Редагувати + Ринкова капіталізація + Спочатку був створений блокчейн криптовалюти + Основна мережа + Використання неосновних мереж для токенів дозволяє забезпечити крос-блокчейн інтероперабельність, що дає змогу використовувати активи в різноманітних децентралізованих додатках та смарт-контрактах на різних платформах. Однак це часто вимагає наявності кастодіана або смарт-контракту для безпечного зберігання оригінального активу, що вводить централізацію та ризик контрагента. + Не оригінальний або первинний блокчейн, на якому розміщено токен + Неосновні мережі + Оберіть мережі + Гаманець + Не вдалося знайти цей токен, ви можете додати його вручну + + %1$d із %2$d гаманця + %1$d із %2$d гаманців + %1$d із %2$d гаманців + %1$d із %2$d гаманців + + Видалити + наприклад Bitcoin + Ваше портфоліо оновлено + Обраний токен наразі недоступний для дій у криптогаманці. Але не хвилюйтеся, ви можете висловити свою зацікавленість, проголосувавши за його інтеграцію. + Проголосувати + Оберіть гаманець + Гаманець не підтримує більше однієї мережі + Щоб почати купувати, обмінювати або отримувати цей актив, додайте цей токен принаймні в 1 мережу + Цей актив недоступний + Додати в портфоліо + Моє портфоліо + Маркет + Не вдалося завантажити дані... + Результат + Переглянути токени до 100к ринкової капіталізації + Показати токени + Жодного результату + Оберіть гаманець + Сортувати за + Інсайти + Посилання + Метрики + Цінова ефективність + Оцінка безпеки + Вам потрібно встановити єдиний код доступу для захисту всіх ваших карток + Захист + Пізніше ви зможете налаштувати індивідуальний код доступу до кожної картки + Персоналізація + Код доступу можна відновити за допомогою прив\'язаної картки. Не зберігайте всі картки в одному місці. + Відновлення + Оберіть будь-яке слово, фразу або число в якості коду доступу + Створити код доступу + Введіть код доступу ще раз, щоб уникнути помилки + Повторно введіть код доступу + Код доступу повинен містити не менше 4 символів + Введені коди доступу не збігаються + Необхідно повторити операцію. Карту буде скинуто до заводських налаштувань. + Помилка активації + Додати токени + Ви додали одну резервну картку. Після завершення процесу резервного копіювання ви не зможете додати більше резервних карток. Якщо у вас є ще одна картка, додайте її до резервної копії. Ви бажаєте продовжити? + Процес резервного копіювання частково завершено. Ви не можете вийти з нього зараз. + Парольна фраза – це розширена функція безпеки, яку використовують криптогаманці. Вона додає додаткове слово або фразу на ваш вибір до вже існуючої seed - фрази, щоб розблокувати абсолютно новий набір адрес. + Додати резервну картку + Відсканувати картку #%d + Створити резервну копію + Відсканувати основну картку + Перейти до мого гаманця + Завершення бекапу + Отримати криптовалюту + Сканувати основну картку + Пропустити + Як це працює? + Давайте згенеруємо всі ключі на вашій картці та створимо безпечний гаманець + Створити гаманець + Створити гаманець + Інші опції + Ваші ключі будуть надійно згенеровані всередині картки. Ніякої seed-фрази, а це означає, що ніхто не зможе її експортувати або вкрасти. + Генеруйте ключі приватно + Ваша картка активована та готова до використання + Успішно! + У цьому випадку вам доведеться почати наново. + Ви хочете вийти з процесу активації? + Підготовка + На картці, яку ви намагаєтеся додати, вже створено інший гаманець. Якщо у вас є кошти на цьому гаманці, будь ласка, виведіть їх, а потім скиньте цю картку до заводських налаштувань і додайте її як резервну. + Резервна копія + Дізнатися більше про seed-фразу + + Запишіть це %d слово у порядку, вказаному нижче, та збережіть його у надійному місці. + Запишіть ці %d слова у порядку, вказаному нижче, та збережіть їх у надійному місці. + Запишіть ці %d слів у порядку, вказаному нижче, та збережіть їх у надійному місці. + Запишіть ці %d слів у порядку, вказаному нижче, та збережіть їх у надійному місці. + + Ваша seed-фраза + + %d слово + %d слова + %d слів + %d слів + + Щоб імпортувати гаманець, введіть seed-фразу в поле нижче + Згенерувати seed-фразу + Імпортувати гаманець + Seed-фраза — це набір слів, який дозволяє відновити ваш гаманець. На відміну від ключів, що генеруються карткою, seed-фраза не захищена і може бути скопійована та викрадена. Використовуйте цю опцію на свій власний ризик. + Використовувати seed-фразу + Невірна seed-фраза. Будь ласка, перевірте порядок слів. + Невірна seed-фраза. Будь ласка, перевірте правопис. + Застарілий + Щоб перевірити чи правильно ви записали seed-фразу, введіть 2-е, 7-е та 11-те слова + Отже, давайте перевіримо + Щоб почати процес резервного копіювання, додайте одну або дві резервні картки. + Ви можете додати ще одну картку або завершити процес резервного копіювання + Підготуйте резервну картку з номером %s + Відскануйте основну картку, щоб почати процес резервного копіювання. + Підготуйте основну картку з номером %s + Ваша картка налаштована та готова до використання. + Додано максимальну кількість карток. Завершіть процес резервного копіювання. + Активація картки + Резервна картка #%d + Немає резервних карток + Сповіщення + Додано одну резервну картку + Підготуйте свою картку + Додано дві резервні картки + Поповніть гаманець на будь-яку суму, щоб почати користуватися карткою + Щоб почати, просто поповніть гаманець більше, ніж на %1$s %2$s + Купити криптовалюту + Показати адресу гаманця + Активація гаманця + Процес поєднання карток частково завершено. Ви не можете вийти з нього зараз. + Якщо процес створення гаманця буде перервано будь-яким чином, вам доведеться почати все спочатку + Ви можете створити резервну копію ключів на одній або двох інших порожніх картках Tangem Wallet. + Код доступу можна відновити за допомогою однієї з резервних карток. + Всі резервні карти можна використовувати як повнофункціональні з ідентичними ключами. + Ви зможете встановити код доступу для захисту своїх гаманців. + Резервна копія картки + Відновлення коду доступу + Ідентичні картки + Код доступу + Групами + За балансом + Сортування токенів + Список + Виберіть з галереї + Налаштування + Ви не надали доступ до своєї камери + Доступ до камери заборонено + %1$s (%2$s) у мережі %3$s + Надсилайте лише %s на цю адресу. Надсилання будь-якої іншої валюти призведе до її незворотної втрати. + Взяти участь + Не вдалося завантажити інформацію по реферальній програмі. Будь ласка, спробуйте пізніше. + Не вдалося завантажити інформацію по реферальній програмі. Код помилки: %s. Будь ласка, спробуйте пізніше. + Майбутні виплати + Ваші друзі купили + Менше + Більше + Немає майбутніх виплат + + за %d гаманець + за %d гаманця + за %d гаманців + за %d гаманців + + Отримайте ^^%1$s^^ на вашу адресу в мережі %2$s%3$s ^^через 30 днів^^ за кожен гаманець, який придбає ваш друг + Ви + Отримає + при купівлі гаманця на сайті tangem.com + %s знижку + Ваш друг + Персональний код скопійовано! + Ваш персональний код + Купити Tangem Wallet зі знижкою!\n%s + Приведи друга в Tangem + Ви прийняли + Натискаючи цю кнопку, ви приймаєте + в реферальній програмі + + %d гаманець + %d гаманця + %d гаманців + %d гаманців + + Скинути картку + Я розумію, що після виконання цієї дії у мене більше не буде доступу до поточного гаманця + Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця + Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки. Ви не зможете відновити поточний гаманець або використати картку для відновлення коду доступу. + Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки. Ви не зможете відновити поточний гаманець. + У вас є банківська картка іншої країни та посвідка на проживання або реєстрація за межами Російської Федерації? + Російські банківські картки наразі не приймаються + Увійдіть у додаток та слідкуйте за своїм балансом без сканування картки + Доступ до додатку + Використовувати біометрію + Для взаємодії з гаманцем будуть запитуватися біометричні дані замість коду доступу + Код доступу + Схоже, що у вас відключена біометрична автентифікація, вона необхідна для збереження гаманців + Увімкніть біометричну автентифікацію + Бажаєте використовувати біометрію? + Зверніть увагу, що для здійснення транзакції з вашими коштами вам все одно знадобиться ваша картка + Сканувати + Відскануйте картку, щоб змінити її налаштування. Зміни торкнуться лише відсканованої картки і не вплинуть на інші картки, прив\'язані до вашого гаманця. + Підготуйте свою картку + Вже включено до введеної адреси + Сума комісії у %s разів перевищує рекомендовану. Переконайтеся, що користувацькі налаштування вірні. + Ви вказали комісію нижче рекомендованої, це може спричинити затримку вашої транзакції. Продовжити? + Причина: %1$s\nКод: %2$s + Транзакція не завершена + Сума + Ви можете встановити розмір комісії за транзакцію, налаштувавши значення в полі Satoshi per vByte. + Комісія, яка буде стягнута за вашу транзакцію. Ви можете встановити власне значення. + Максимальна комісія + Це вартість, яку ви готові платити за кожну одиницю газу. Чим вища ціна газу, тим швидше буде оброблено вашу транзакцію. (Пріоритетна плата входить у вартість) + Пріоритетна плата + Комісія, яку користувач може заплатити майнерам або валідаторам, щоб прискорити включення їхньої транзакції в блок. + Комісія, необхідна за використання кожної невитраченої транзакції (UTXO) у мережі Kaspa. Чим більше UTXO ви використовуєте в транзакції, тим вищою буде комісія. + KAS за UTXO + %1$s, %2$s + Адреса + Тег призначення + Введіть адресу + Адреса збігається з адресою гаманця + Недопустимий Tag. Він не буде доданий у транзакцію. + Недопустимий Memo. Він не буде доданий до транзакції. + Tag + Memo + Включаючи комісію + Низька + Нормальна + Пріоритетна + Перевірте підключення до мережі + Інформація щодо комісії в мережі недоступна + Із + Ліміт газу + Це максимальна кількість газу, яка буде витрачена на здійснення транзакції чи контракту. Ліміт газу запобігає несподіваним або необмеженим стягненням під час виконання транзакції. + Вартість газу + Це вартість, яку ви готові платити за кожну одиницю газу. Чим вища ціна газу, тим швидше буде оброблено вашу транзакцію. + Все + Максимальна сума + Комісія може сягати до + Недопустимий Memo + Покриття мережевої комісії + Недостатньо коштів для здійснення переказу, оскільки загальна сума комісії та переказу перевищує наявний баланс + Сума перевищує баланс + Рахунок буде видалено з блокчейну, якщо баланс стане нижчим за екзистенційний депозит. Будь ласка, залиште %s на своєму балансі. + Екзистенційний депозит + Сума комісії в %s разів перевищує рекомендовану. Переконайтеся, що користувацькі налаштування вірні. + Встановлена комісія завелика + Через особливості мережі %1$s комісія за переказ всього балансу вища. Щоб зменшити комісію, Ви можете залишити %2$s. + Підвищена комісія + Включена комісія перевищує суму переказу, що призводить до від’ємного значення + Недопустима сума + Мінімальна сума переказу - %1$s. Будь ласка, переконайтеся, що залишок після переказу не буде меншим ніж %2$s. + Цільовий рахунок не активований. Будь ласка, змініть суму переказу, щоб продовжити. + Сума переказу повинна бути не менше %s + Залишити %s + Зменшити на %s + Зменшити до %s + Зверніть увагу, що при певних налаштуваннях комісії можливі затримки по вашій транзакції + Можливі затримки транзакцій + Через обмеження %1$s одна транзакція може вмістити лише %2$s UTXO. Це означає, що ви можете надіслати лише %3$s або менше. Вам потрібно зменшити суму. + Ліміт транзакції + Опціонально + Будь ласка, сумістіть свій QR-код з квадратом, щоб відсканувати його. Переконайтеся, що ви скануєте адресу в мережі %s. + Останні + Одержувач + Недійсна адреса + Переконайтеся, що адреса гаманця одержувача знаходиться в мережі %s, щоб уникнути втрати ваших токенів + Надіслати до + Memo/ Тег призначення — це унікальний ідентифікатор для розрізнення транзакцій, надісланих тому самому одержувачу в тій самій мережі. Застереження: відсутність тегу може призвести до втрати коштів. + Мої гаманці + Спосіб виміру комісії за біткоїн-транзакцію. Він свідчить про кількість найменшої одиниці біткоїна (сатоші) за кожен віртуальний байт у транзакції. Чим вище число, тим швидше буде оброблено транзакцію майнерами. + Сатоші / вбайт + Надсилання... + Торкніться будь-якого поля, щоб змінити його + Надіслати %s + Ви надсилаєте ** %1$s **, включно з комісію мережі %2$s + Ви надсилаєте ** %1$s ** і %2$s + Надсилання %s + Всього + %1$s та %2$s буде надіслано + ≈ %1$s (вкл. комісію: %2$s ) + %s буде надіслано + Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час + Недійсна адреса + %1$s (%2$s) + Трансакцію надіслано + Підготуйтеся до сканування картку, яку потрібно налаштувати. + Забути гаманець + Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. + Ім\'я + Активний + Щоб вивести активи зі стейкінгу, натисніть тут. + Сума для стейкінгу має бути не менше %s + APR + APY + Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. + Доступно + Середня ставка винагороди + ~ прибуток за %s + Рейтинг ринку + Метрики + Мінімальні вимоги + Немає винагород, щоб отримати + В стейкінгу + Отримати винагороду + Спосіб отримання винагороди за стейкінг. Його можна отримати автоматично або вручну. + Розклад винагород + Це графік, який визначає коли учасники стейкінгу отримують свої винагороди. + Нагороди для отримання: %s + Стейкінг %s + Період розблокування + Період, який ви повинні чекати після запиту на виведення коштів зі стейкінгу, перш ніж токени стануть доступними. + Період блокування + Відведений час для активації участі в стейкінгу. + Нативний стейкінг + Стейкінг дозволяє заробляти %1s. Ваші винагороди за стейкінг надходять кожні ~%2s днів. + Отримуйте винагороду за стейкінг + Винагороди + Застейкати більше + Не застейканий + Перевірити незастейкані, щоб отримати свої активи + Валідатор + Тримайте свою криптовалюту в безпеці. Приватні ключі надійно зберігаються на картці. + Революційний апаратний гаманець + До трьох карток з одним гаманцем + Всі ключі у безпеці + Апаратний гаманець для ваших біткоїнів, ефіріуму та багатьох інших валют одночасно — і все це на одній картці + Тисячі криптовалют + Використовуйте його в дорозі, будь-де і будь-коли. Ніяких дротів чи батарейок. Просто прикладіть картку до телефону, коли вам потрібна криптовалюта. + Гаманець для кожного + Зустрічайте Tangem + Обмінюйте, купуйте NFT, отримуйте позики та робіть депозити у понад 100 різних децентралізованих сервісах + Web 3.0 сумісність + Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. + З\'явився новий провайдер обмінів! + Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. + Сума включає комісію постачальника послуг. + Комісії + Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл. + Підтвердити + Ви обмінюєте + Обмін цієї кількості обраних токенів призведе до значного впливу на ціну і зменшить вашу кінцеву суму. + Недостатньо коштів + Надати дозвіл + В процесі + Обміняти + Ви отримаєте + Оберіть токен + недоступно + Баланси приховано + Баланси показано + Скасувати + Обрана операція наразі недоступна. Спробуйте пізніше. + Наразі купівля монети %s недоступна. Слідкуйте за нашими оновленнями. + У вас немає коштів для продажу. Поповніть рахунок, щоб мати змогу продати з нього кошти. + У вас немає коштів для відправлення. Поповніть рахунок, щоб мати змогу надіслати з нього кошти. + Наразі обмін монети %s недоступний. Слідкуйте за нашими оновленнями. + Продаж коштів стане доступним після завершення транзакції(-ій) в мережі %s + Надсилання коштів стане доступним після завершення транзакції(-ій) в мережі %s + Наразі продаж %s недоступний. Слідкуйте за нашими оновленнями. + Стейкінг %s зараз недоступний. Будь ласка, слідкуйте за нашими оновленнями. + Згенерувати XPUB + Приховати + Ви збираєтеся приховати цей токен з головного екрану. Ви можете додати його назад будь-коли на сторінці керування токенами. + Приховати %s + Приховати токен + Стейкінг дозволяє заробляти %1$s і отримувати винагороду кожні %2$s днів + Заробляйте до %s винагород за стейкінг щороку + %1$s токен в мережі %%image%% %2$s + Токен у мережі %%image%% %1$s + Токен %1$s (%2$s) є основною валютою в мережі %3$s і не може бути прихований до тих пір, поки у вас в списку є інші токени цієї мережі + Неможливо приховати %s + Обмінюйте цей токен на інші з %1$s комісією за обслуговування з %2$s по %3$s лютого. + Обмін із Changelly, %s комісії + Обміняти + контракт: %s + У вас ще немає транзакцій + Не вдалося завантажити історію транзакцій.\nНатисніть кнопку перезавантаження, щоб оновити інформацію. + Кілька адрес + Історія транзакцій наразі не підтримується для цього блокчейну. Але не хвилюйтеся, ми працюємо над цим! А поки можете перевірити це в оглядачі. + Операція + від: %s + до: %s + Спробуйте знову + Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d + Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу + Це картка, яку ви тримаєте в руках. У парної картки номер %s.\n\nОбидві картки можна використовувати для зняття коштів з цього гаманця. + Один гаманець. Дві картки. + Відсканувати картку #%s + Створення гаманця + Відскануйте twin-картку # %s + Підготовка картки + Tangem Twin + Ця дія є незворотною. Ви не матимете доступу до старого гаманця. + Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції + Використовуйте %s або відскануйте картку, щоб отримати доступ до свого гаманця + Будьте в курсі останніх функцій та новин + Бажаєте використовувати Push-повідомлення? + Додати новий гаманець + Ви впевнені, що хочете видалити цей гаманець? + Сталася помилка, будь ласка, відскануйте свою картку, щоб увійти в систему + Цей гаманець вже збережено, ви можете додати інший + Гаманець з назвою %s вже існує + Назва гаманця + Перейменувати гаманець + Розблокувати все + Розблокувати все з %s + Блокчейн недоступний. Спробуйте пізніше + Відскануйте картку + Запит на підпис повідомлення.\n\n%s + Dapp %1$s, запит на\nпідпис транзакції BNB.\n\n%2$s + Торговий ордер на %1$s\nЦіна: %2$s\nСума до отримання: %3$s\nСума до сплати: %4$s + Деталі транзакції:\nВід: %1$s\nДo: %2$s\nСума: %3$s + У буфері обміну міститься код WalletConnect. Використайте скопійоване значення або відскануйте QR-код + Запит на створення транзакції для %1$s\n%2$s\n\nСума: %3$s\nКомісія: %4$s\nВсього: %5$s\nБаланс: %6$s + Неможливо відправити транзакцію. Недостатньо коштів. + Не вдалося встановити сеанс WalletConnect. Будь ласка, спробуйте пізніше. + Не вдалося підписати повідомлення.\nБудь ласка, спробуйте ще раз + Не вдалося створити сеанс WalletConnect за відведений час. Будь ласка, спробуйте пізніше. + Запит на підключення сеансу WalletConnect містить непідтримувані блокчейни. Непідтримувані блокчейни:\n + Зв\'язок з цим Dapp сервісом не може бути встановлений через його технічну реалізацію. + Ми зіткнулися з невідомою помилкою. Повідомлення про помилку: %s. Якщо проблема не зникає — зверніться до нашої служби підтримки. + Невірна картка обрана в додатку Tangem + Не вдалося створити транзакцію з даних Dapp. Код: %s + Ми зіткнулися з невідомою помилкою. Код помилки: %d. Якщо проблема не зникає — зверніться до нашої служби підтримки. + Немає відкритих сеансів WalletConnect + Упс. Немає сеансів. + Не вдалося створити пару WalletConnect: %1$s + Вставити з буфера обміну + Повідомлення для %1$s:\n%2$s + Запит на відкриття сесії для\n%1$s\n\nМЕРЕЖА: %2$s\n\nURL: %3$s + Операція не може бути завершена.\n\nВи вже створили сеанс WalletConnect з цими параметрами. + Відсканувати новий код + Цю картку не можна використовувати з WalletConnect. + Ця мережа не підтримується. Будь ласка, виберіть іншу мережу. + Оберіть мережу + Сеанси WalletConnect + Підключення до dApps + WalletConnect + Підключення може зайняти кілька секунд + Ринкова ціна %s + за 24 години + Мережа %s + Адреса скопійована в буфер обміну + Немає підключення до інтернету + Налаштування гаманця + Tangem + Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Помилка активації + За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. + Відключення мережі BNB Beacon Chain + Могло б бути краще + Вподобати + Зрозуміло! + Дуже круто! + Оновити + Ви перебуваєте в демонстраційному режимі + Демонстраційний режим активовано + Відсканована вами картка є карткою розробника. Не використовуйте її для створення гаманця. + Не для користувачів! + Мережа %1$s використовує концепцію екзистенціального депозиту. Якщо баланс вашого рахунку буде нижче %2$s, його буде деактивовано, а всі залишкові кошти знищено. + Для роботи з мережею вимагається депозит + Обмін буде доступний після завершення %s транзакції + У вас є активна транзакція + Затвердження обміну триває і незабаром буде завершено + Затвердження в процесі + Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. + У вашому списку немає доступних монет для обміну %s + Немає доступних токенів для обміну + Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s + Неможливо покрити комісію %s + Сума отримання не може бути меншою за %s + Сервіс тимчасово недоступний + Сума до обміну не повинна перевищувати %s + Сума для обміну має бути не менше %s + Будь ласка, змініть суму для обміну + Ця картка може бути виробничим зразком або підробкою + Перевірка автентичності не вдалася + Асоціювати + Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти. Вартість асоціації ~ %1$s %2$s + Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти + Асоціюйте свій токен + Недостатньо %s. Поповніть ваш обліковий запис Hedera для асоціації цього токена + На цій картці залишається лише %s підписів. Ви повинні вивести всі свої кошти. + Низька кількість підписів + Токени в різних мережах можуть мати різні адреси. Переконайтеся, що ваша адреса відповідає мережі, коли переказуєте кошти. + + Використовуйте вашу картку, щоб отримати адресу для %d мережі + Використовуйте вашу картку, щоб отримати адреси для %d мереж + Використовуйте вашу картку, щоб отримати адреси для %d мереж + Використовуйте вашу картку, щоб отримати адреси для %d мереж + + Деякі адреси відсутні + Мережа наразі недоступна. Будь ласка, спробуйте пізніше. + Мережа недоступна + Поповніть свій гаманець + Ваш гаманець не має резервної копії. Проведіть цю процедуру зараз, щоб захистити свої активи. + Резервна копія відсутня + Ця картка вже використовувалася для здійснення транзакцій. Якщо вона отримана з ненадійного джерела, подумайте про те, щоб зняти всі кошти. Якщо це ваша картка, ніяких додаткових дій не потрібно. + Картка вже підписувала транзакції + Ваш відгук мотивує нас робити гаманець Tangem Wallet ще кращим + Подобається Tangem? + Вам необхідно провести асоціацію токена, щоб мати можливість приймати його + Необхідна плата за оренду мережі + %1$s є активом у мережі %2$s. Щоб здійснити транзакцію %3$s, ви повинні внести певну суму %4$s (%5$s), щоб покрити комісію мережі. + Недостатньо %1$s для покриття комісії мережі + Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію. + Оповіщення мережі Солана + Мережа Solana стягує орендну плату у розмірі %1$s кожні 2 дні. Акаунти, які не можуть дозволити собі орендну плату, видаляються з мережі. Поповніть свій рахунок на суму понад %2$s, щоб не платити орендну плату. + Деякі мережі наразі недоступні. Будь ласка, спробуйте пізніше. + Деякі мережі недоступні + Це картка Testnet. Вона не може обробляти транзакції і повинна використовуватися лише для тестування та розробки. + Лише для цілей тестування + Відмовитися + Ви не завершили резервне копіювання. Бажаєте продовжити? + Так, поновити + Відмовитися + Якщо зараз відмовитися, то доведеться скинути картки до заводських налаштувань, щоб почати спочатку + Відновити резервне копіювання + Це незворотна дія + Увійдіть за допомогою %s + Сканувати картку + Використовуйте %s або відскануйте картку для доступу в додаток + З поверненням! + diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 002bbbf669..c1a5d5ad43 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -307,9 +307,6 @@ Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s Swap with Changelly, %s fees Tokens - Book now - Save **$50** while booking via our partner Travala: **%1s - %2s** - Book your holidays with Tangem and pay in crypto Add Edit Coin market cap diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt deleted file mode 100644 index 6fc2f27ebd..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt +++ /dev/null @@ -1,216 +0,0 @@ -package com.tangem.core.ui.components.notifications - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.ScaleFactor -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.LineBreak -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonColors -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.res.TangemColorPalette.White -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -/** - * Travala notification with image background - * @see Travala Promo - */ -@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") -@Composable -fun TravalaNotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) { - val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig - - Box( - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size62) - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(Color.Black) - .clickable( - enabled = config.onClick != null, - onClick = config.onClick ?: {}, - ), - propagateMinConstraints = true, - contentAlignment = Alignment.TopStart, - ) { - val density = LocalDensity.current - Image( - painter = painterResource(R.drawable.img_travala_banner_promo_background), - contentDescription = null, - contentScale = TravalaBackgroundScale(density), - alignment = Alignment.TopStart, - modifier = Modifier - .matchParentSize() - .wrapContentSize(unbounded = true, align = Alignment.TopStart) - .align(Alignment.TopStart), - ) - Image( - painter = painterResource(R.drawable.img_travala_banner_promo_background_2), - contentDescription = null, - contentScale = TravalaBackgroundScale(density), - alignment = Alignment.TopStart, - modifier = Modifier - .matchParentSize() - .wrapContentSize(unbounded = true, align = Alignment.TopEnd) - .align(Alignment.TopEnd), - ) - Column { - Row { - Box(modifier = Modifier.size(87.dp)) - Column( - Modifier - .weight(1f) - .padding(top = TangemTheme.dimens.spacing12), - ) { - Text( - text = config.title.resolveReference(), - style = TangemTheme.typography.button.copy( - lineBreak = LineBreak.Heading, - ), - color = TangemTheme.colors.text.constantWhite, - ) - SpacerH8() - Text( - text = formatSubtitle(config.subtitle.resolveReference()), - style = TangemTheme.typography.caption2.copy( - lineBreak = LineBreak.Heading, - ), - color = TangemTheme.colors.text.constantWhite, - ) - } - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors.text.constantWhite, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - start = TangemTheme.dimens.spacing2, - ) - .size(TangemTheme.dimens.size16) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false), - ) { - config.onCloseClick?.invoke() - }, - ) - } - - TangemButton( - text = button?.text?.resolveReference().orEmpty(), - icon = TangemButtonIconPosition.None, - onClick = button?.onClick ?: {}, - colors = TangemButtonColors( - backgroundColor = White.copy(alpha = 0.3f), - contentColor = White, - disabledBackgroundColor = TangemTheme.colors.button.disabled, - disabledContentColor = TangemTheme.colors.text.disabled, - ), - enabled = true, - showProgress = false, - modifier = Modifier - .padding(TangemTheme.dimens.spacing12) - .fillMaxWidth(), - ) - } - } -} - -private const val TRAVALA_BACKGROUND_SRC_IMG_SCALE = 4 - -private class TravalaBackgroundScale( - val density: Density, -) : ContentScale { - override fun computeScaleFactor(srcSize: Size, dstSize: Size): ScaleFactor { - with(density) { - val originalWidth = (srcSize.width / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx() - val widthScale = originalWidth / srcSize.width - val originalHeight = (srcSize.height / TRAVALA_BACKGROUND_SRC_IMG_SCALE).dp.toPx() - val heightScale = originalHeight / srcSize.height - return ScaleFactor(widthScale, heightScale) - } - } -} - -@Composable -private fun formatSubtitle(subtitle: String): AnnotatedString { - val pattern = Regex("\\*\\*(.*?)\\*\\*") - var startIndex = 0 - val annotatedString = buildAnnotatedString { - pattern.findAll(subtitle).forEach { matchResult -> - val index = matchResult.range.first - val matchedValue = matchResult.groups[1]?.value ?: "" - - // appends unformatted part - append(subtitle.substring(startIndex, index)) - - // applies style on ^^-wrapped parts - withStyle(SpanStyle(fontWeight = TangemTheme.typography.caption1.fontWeight)) { - append(matchedValue) - } - - // goes to next part - startIndex = matchResult.range.last + 1 - } - - // appends remaining ending if exists - append(subtitle.substring(startIndex)) - } - - return annotatedString -} - -//region preview -@Preview -@Composable -private fun TravalaNotificationWithBackgroundPreview() { - TangemThemePreview { - TravalaNotificationWithBackground( - config = NotificationConfig( - title = resourceReference( - id = R.string.main_travala_promotion_title, - ), - subtitle = resourceReference( - id = R.string.main_travala_promotion_description, - formatArgs = wrappedList("May 13", "June 12"), - ), - iconResId = R.drawable.img_swap_promo, - backgroundResId = R.drawable.img_travala_banner_promo_background, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(id = R.string.token_swap_promotion_button), - onClick = {}, - ), - ), - ) - } -} -//endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 398372e208..1a15d9ee54 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -46,10 +46,6 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem is WalletNotification.Critical.BackupError -> MainScreen.BackupError - is WalletNotification.TravalaPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( - source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Travala, - ) is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 4a3247b5e1..0ed8bda419 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.pluralReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.feature.wallet.impl.R import org.joda.time.DateTime @@ -174,34 +173,6 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) - data class TravalaPromo( - val startDateTime: DateTime, - val endDateTime: DateTime, - val bannerLink: String?, - val onBookNowButtonClick: (String?) -> Unit, - val onCloseClick: () -> Unit, - ) : WalletNotification( - config = NotificationConfig( - title = resourceReference(id = R.string.main_travala_promotion_title), - subtitle = resourceReference( - id = R.string.main_travala_promotion_description, - wrappedList( - DateTimeFormatters.formatDate(startDateTime, DateTimeFormatters.dateMMMMd), - DateTimeFormatters.formatDate(endDateTime, DateTimeFormatters.dateMMMMd), - ), - ), - // Stub. Travala has its own Composable implementation with correct img - iconResId = R.drawable.ic_star_24, - // Stub. Travala has its own Composable implementation with correct img - backgroundResId = R.drawable.ic_star_24, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - onClick = { onBookNowButtonClick(bannerLink) }, - text = resourceReference(R.string.main_travala_promotion_button), - ), - onCloseClick = onCloseClick, - ), - ) - data class SwapPromo( val startDateTime: DateTime, val endDateTime: DateTime, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 08a341ebb7..b27ba3f5e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.OkxPromoNotification -import com.tangem.core.ui.components.notifications.TravalaNotificationWithBackground import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -34,12 +33,6 @@ internal fun LazyListScope.notifications(configs: ImmutableList { - TravalaNotificationWithBackground( - config = it.config, - modifier = modifier.animateItemPlacement(), - ) - } else -> { Notification( config = it.config, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 498657aa3e..cc82331baf 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -90,7 +90,7 @@ markdown = "0.7.2" # region Tangem tangemBlockchainSdk = "develop-700" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-374" +tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From ef36fbf13ecf9d68b26158bb54da9c77cf6167ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jul 2024 19:15:57 +0300 Subject: [PATCH 176/223] Updated on 2026-08-14 --- .../details/ui/cardsettings/CardSettingsViewModel.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 94be2ca2fd..4715114d15 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -130,14 +130,20 @@ internal class CardSettingsViewModel @Inject constructor( } is CardInfo.ResetToFactorySettings -> { Analytics.send(Settings.CardSettings.ButtonFactoryReset()) - store.dispatchNavigationAction { AppRoute.ResetToFactory(userWalletId) } + store.dispatchNavigationAction { + push(route = AppRoute.ResetToFactory(userWalletId)) + } } is CardInfo.SecurityMode -> { Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode()) - store.dispatchNavigationAction { AppRoute.DetailsSecurity(userWalletId) } + store.dispatchNavigationAction { + push(route = AppRoute.DetailsSecurity(userWalletId)) + } } is CardInfo.AccessCodeRecovery -> { - store.dispatchNavigationAction { AppRoute.AccessCodeRecovery(userWalletId) } + store.dispatchNavigationAction { + push(route = AppRoute.AccessCodeRecovery(userWalletId)) + } } else -> {} } From 5f482a669850fd0e224137dfdf7a137ed3bcd455 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jul 2024 22:01:02 +0300 Subject: [PATCH 177/223] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 +++ .../tokens/GetCryptoCurrencyActionsUseCase.kt | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) 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 6e2c244f20..09792b495e 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 @@ -7,6 +7,7 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -211,6 +212,7 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, networksRepository: NetworksRepository, stakingRepository: StakingRepository, + stakingFeatureToggles: StakingFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -221,6 +223,7 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, networksRepository = networksRepository, stakingRepository = stakingRepository, + stakingFeatureToggles = stakingFeatureToggles, dispatchers = dispatchers, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 8cb5dbfc99..16083adcbb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -12,6 +12,7 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -31,6 +32,7 @@ class GetCryptoCurrencyActionsUseCase( private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, + private val stakingFeatureToggles: StakingFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -126,14 +128,16 @@ class GetCryptoCurrencyActionsUseCase( } // staking - if (isStakingAvailable(cryptoCurrency)) { - activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) - } else { - disabledList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), - ), - ) + if (stakingFeatureToggles.isStakingEnabled) { + if (isStakingAvailable(cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) + } else { + disabledList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + ), + ) + } } // send @@ -240,7 +244,6 @@ class GetCryptoCurrencyActionsUseCase( actionsList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable)) actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.Unreachable)) actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { val scenario = if (needAssociateAsset) { ScenarioUnavailabilityReason.UnassociatedAsset @@ -249,8 +252,11 @@ class GetCryptoCurrencyActionsUseCase( } actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } - actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) + if (stakingFeatureToggles.isStakingEnabled) { + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) + } actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) + return actionsList } From e4fc013173099e4f13bf5cc899d71f5292bb3f58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 19 Jul 2024 15:53:19 +0500 Subject: [PATCH 178/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + .../PushNotificationAnalyticEvents.kt | 9 +++++ .../impl/PushNotificationsFragment.kt | 7 ++-- .../ui/PushNotificationsScreen.kt | 15 ++++---- .../viewmodel/PushNotificationViewModel.kt | 34 +++++++++++++------ .../PushNotificationsClickIntents.kt | 6 ++-- .../PushNotificationsBottomSheetConfig.kt | 1 + .../PushNotificationsBottomSheet.kt | 3 +- .../wallet/viewmodels/WalletViewModel.kt | 9 ++--- .../WalletPushPermissionClickIntents.kt | 18 +++++++--- 10 files changed, 72 insertions(+), 31 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 14c45c2681..fba85fe955 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -68,6 +68,7 @@ Недостаточно ADA Принять Доступ запрещен + Разрешить Применить Одобрение Подтвердить diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt index d3c8a94ed5..c35b196ec8 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -29,4 +29,13 @@ sealed class PushNotificationAnalyticEvents( data object ButtonCancel : PushNotificationAnalyticEvents( event = "Button - Cancel", ) + + data class PermissionStatus( + val isAllowed: Boolean, + ) : PushNotificationAnalyticEvents( + event = "Permission Status", + params = mapOf( + AnalyticsParam.STATE to if (isAllowed) "Allow" else "Cancel", + ), + ) } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt index 0b8fedc562..1d6fb08e71 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt @@ -25,9 +25,10 @@ internal class PushNotificationsFragment : ComposeFragment() { BackHandler(onBack = requireActivity()::finish) NavigationBar3ButtonsScrim() PushNotificationsScreen( - onShowAllow = viewModel::onAllowPermission, - onAllow = viewModel::onAllowedPermission, - onLater = viewModel::onAskLater, + onRequest = viewModel::onRequest, + onRequestLater = viewModel::onRequestLater, + onAllowPermission = viewModel::onAllowPermission, + onDenyPermission = viewModel::onDenyPermission, onOpenSettings = viewModel::openSettings, ) } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt index 9fe0f81030..c039d9109d 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -16,17 +16,18 @@ import kotlinx.collections.immutable.persistentListOf @Composable internal fun PushNotificationsScreen( - onShowAllow: () -> Unit, - onAllow: () -> Unit, - onLater: () -> Unit, + onRequest: () -> Unit, + onRequestLater: () -> Unit, + onAllowPermission: () -> Unit, + onDenyPermission: () -> Unit, onOpenSettings: () -> Unit, ) { val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( isFirstTimeAsking = true, isClicked = isClicked, - onAllow = onAllow, - onDeny = onLater, + onAllow = onAllowPermission, + onDeny = onDenyPermission, onOpenSettings = onOpenSettings, pushPermission = getPushPermissionOrNull(), ) @@ -48,13 +49,13 @@ internal fun PushNotificationsScreen( buttonText = resourceReference(R.string.common_allow), onClick = { isClicked.value = true - onShowAllow() + onRequest() requestPushPermission() }, ), secondaryButton = ShowcaseButtonModel( buttonText = resourceReference(R.string.common_later), - onClick = onLater, + onClick = onRequestLater, ), modifier = Modifier.systemBarsPadding(), ) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt index fcba5aa01c..b87d3f4f88 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -28,7 +28,16 @@ internal class PushNotificationViewModel @Inject constructor( private val analyticHandler: AnalyticsEventHandler, ) : ViewModel(), PushNotificationsClickIntents { - override fun onAskLater() { + override fun onRequest() { + analyticHandler.send( + PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), + ) + viewModelScope.launch { + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + } + + override fun onRequestLater() { analyticHandler.send( PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories), ) @@ -36,25 +45,30 @@ internal class PushNotificationViewModel @Inject constructor( delayPermissionRequestUseCase(PUSH_PERMISSION) setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + router.openHome() } - router.openHome() } override fun onAllowPermission() { - viewModelScope.launch { - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) - } - } - - override fun onAllowedPermission() { analyticHandler.send( - PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), + PushNotificationAnalyticEvents.PermissionStatus(isAllowed = true), ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + router.openHome() + } + } + + override fun onDenyPermission() { + analyticHandler.send( + PushNotificationAnalyticEvents.PermissionStatus(isAllowed = false), + ) + viewModelScope.launch { + delayPermissionRequestUseCase(PUSH_PERMISSION) + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + router.openHome() } - router.openHome() } override fun openSettings() = settingsManager.openSettings() diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt index f101e15e7a..2d4fe34217 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt @@ -1,11 +1,13 @@ package com.tangem.features.pushnotifications.impl.presentation.viewmodel internal interface PushNotificationsClickIntents { - fun onAskLater() + fun onRequest() + + fun onRequestLater() fun onAllowPermission() - fun onAllowedPermission() + fun onDenyPermission() fun openSettings() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt index fa0fa3cd6e..c659d29d7c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt @@ -6,6 +6,7 @@ data class PushNotificationsBottomSheetConfig( val isFirstTimeRequested: Boolean, val wasInitiallyAsk: Boolean, val onRequest: () -> Unit, + val onRequestLater: () -> Unit, val onAllow: () -> Unit, val onDeny: () -> Unit, val openSettings: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index 6a37a22f0c..dcc56be741 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -84,7 +84,7 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC stringResource(R.string.common_cancel) }, onClick = { - content.onDeny() + content.onRequestLater() onDismiss() }, modifier = Modifier.weight(1f), @@ -113,6 +113,7 @@ private fun PushNotificationsSheetContent_Preview() { isFirstTimeRequested = false, wasInitiallyAsk = false, onRequest = {}, + onRequestLater = {}, onAllow = {}, onDeny = {}, openSettings = {}, 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 585bfc901e..a15817a145 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 @@ -159,21 +159,22 @@ internal class WalletViewModel @Inject constructor( val isFirstTimeRequested = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } - val onDenyClick: () -> Unit = if (wasInitiallyAsk) { + val onRequestLater: () -> Unit = if (wasInitiallyAsk) { clickIntents::onDelayAskPushPermission } else { - clickIntents::onDenyPushPermission + clickIntents::onNeverAskPushPermission } stateHolder.showBottomSheet( content = PushNotificationsBottomSheetConfig( isFirstTimeRequested = isFirstTimeRequested, wasInitiallyAsk = wasInitiallyAsk, onRequest = clickIntents::onRequestPushPermission, + onRequestLater = onRequestLater, onAllow = clickIntents::onAllowPushPermission, - onDeny = onDenyClick, + onDeny = clickIntents::onDenyPushPermission, openSettings = settingsManager::openSettings, ), - onDismiss = onDenyClick, + onDismiss = onRequestLater, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index 16994a8a7a..fe8848daf5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -17,6 +17,8 @@ internal interface WalletPushPermissionClickIntents { fun onDelayAskPushPermission() + fun onNeverAskPushPermission() + fun onDenyPushPermission() fun onAllowPushPermission() @@ -31,6 +33,9 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( ) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { override fun onRequestPushPermission() { + analyticsEventHandler.send( + PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Main), + ) viewModelScope.launch { setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) } @@ -45,17 +50,22 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( } } + override fun onNeverAskPushPermission() { + viewModelScope.launch { + analyticsEventHandler.send(PushNotificationAnalyticEvents.ButtonCancel) + neverRequestPermissionUseCase(PUSH_PERMISSION) + } + } + override fun onDenyPushPermission() { - analyticsEventHandler.send(PushNotificationAnalyticEvents.ButtonCancel) + analyticsEventHandler.send(PushNotificationAnalyticEvents.PermissionStatus(isAllowed = false)) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) } } override fun onAllowPushPermission() { - analyticsEventHandler.send( - PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Main), - ) + analyticsEventHandler.send(PushNotificationAnalyticEvents.PermissionStatus(isAllowed = true)) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) } From 5e597758b97143dfc2c040d5ad1dd1fbb8b42902 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 18 Jul 2024 21:22:11 +0500 Subject: [PATCH 179/223] Updated on 2026-08-14 --- .../state/InnerYieldBalanceState.kt | 2 + .../presentation/state/StakingStateRouter.kt | 4 +- .../impl/presentation/state/StakingUiState.kt | 3 + .../RewardsValidatorStateConverter.kt | 9 +- .../converters/YieldBalancesConverter.kt | 1 + .../ConfirmationStatePreviewData.kt | 1 + .../previewdata/InitialStakingStatePreview.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 4 +- .../SetConfirmationStateAssentTransformer.kt | 8 +- .../SetConfirmationStateLoadingTransformer.kt | 5 +- .../ui/StakingNavigationButtons.kt | 85 ++++++++++++------- .../viewmodel/StakingClickIntents.kt | 5 +- .../viewmodel/StakingViewModel.kt | 58 ++++++++----- 13 files changed, 121 insertions(+), 65 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index b5a01e4cf9..dcea12e0ed 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -31,6 +32,7 @@ data class BalanceState( val fiatAmount: TextReference, val rawCurrencyId: String?, val unbondingPeriod: TextReference, + val pendingActions: ImmutableList, ) enum class BalanceGroupType { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 2c0b98f544..2629f6405b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -16,7 +16,9 @@ internal class StakingStateRouter( when (stateController.value.currentStep) { StakingStep.InitialInfo -> when (stateController.value.routeType) { RouteType.STAKE -> showAmount() - RouteType.UNSTAKE -> showConfirmation() + RouteType.OTHER, + RouteType.UNSTAKE, + -> showConfirmation() RouteType.CLAIM -> showRewardsValidators() } StakingStep.RewardsValidators, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 4a1be94850..085a7fcd94 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList @@ -83,6 +84,7 @@ internal sealed class StakingStates { val innerState: InnerConfirmationStakingState, val feeState: FeeState, val validatorState: ValidatorState, + val pendingActions: ImmutableList, val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, @@ -106,4 +108,5 @@ enum class RouteType { STAKE, UNSTAKE, CLAIM, + OTHER, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index c162e9df1f..107207c36c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -4,10 +4,7 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState @@ -15,6 +12,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -58,6 +56,7 @@ internal class RewardsValidatorStateConverter( count = unbondingPeriod, formatArgs = wrappedList(unbondingPeriod), ), + pendingActions = balance.pendingActions.toPersistentList(), ) } @@ -66,6 +65,7 @@ internal class RewardsValidatorStateConverter( cryptoValue: BigDecimal, fiatValue: BigDecimal?, unbondingPeriod: TextReference, + pendingActions: ImmutableList, ): BalanceState { val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency @@ -95,6 +95,7 @@ internal class RewardsValidatorStateConverter( fiatAmount = fiatAmount, rawCurrencyId = cryptoCurrency.id.rawCurrencyId, unbondingPeriod = unbondingPeriod, + pendingActions = pendingActions, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index dbecbf6274..23141275d2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -104,6 +104,7 @@ internal class YieldBalancesConverter( count = unbondingPeriod, formatArgs = wrappedList(unbondingPeriod), ), + pendingActions = balance.pendingActions.toPersistentList(), ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index 9bb95df370..0994dd7cdb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -85,5 +85,6 @@ internal object ConfirmationStatePreviewData { ), ), transactionDoneState = TransactionDoneState.Empty, + pendingActions = persistentListOf(), ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index ce4bd5f41a..ac561e7608 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -51,6 +51,7 @@ internal object InitialStakingStatePreview { preferred = false, ), unbondingPeriod = stringReference("3 days"), + pendingActions = persistentListOf(), ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 5c29ffcb0e..b9a90741b4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,15 +1,17 @@ package com.tangem.features.staking.impl.presentation.state.stub +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import kotlinx.collections.immutable.ImmutableList object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick() {} + override fun onNextClick(pendingActions: ImmutableList) {} override fun onPrevClick() {} diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 7c540291b5..90e5434e09 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -1,22 +1,25 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer -import com.tangem.blockchain.common.Amount -import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +import kotlinx.collections.immutable.ImmutableList @Suppress("UnusedPrivateMember") internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val stakingGasEstimate: StakingGasEstimate, + private val pendingActionList: ImmutableList, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -45,6 +48,7 @@ internal class SetConfirmationStateAssentTransformer( isFeeApproximate = false, ), validatorState = validatorState.copySealed(isClickable = true), + pendingActions = pendingActionList, isPrimaryButtonEnabled = true, ) } else { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 382738bb2f..e6ba83a11d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -2,10 +2,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingNotification -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf @@ -37,6 +33,7 @@ internal class SetConfirmationStateLoadingTransformer( ), footerText = "", transactionDoneState = TransactionDoneState.Empty, + pendingActions = persistentListOf(), ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt index 41b426c018..1ce479e4a5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -25,8 +25,6 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.res.TangemTheme import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState @Composable internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) { @@ -70,10 +68,8 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier val showTangemIcon = uiState.currentStep == StakingStep.Confirmation && (isInProgressInnerState || isInAssentInnerState) - val (buttonTextId, buttonClick) = getButtonData( - currentState = uiState, - ) - val isButtonEnabled = isButtonEnabled(uiState) + val buttonTextId = getButtonData(currentState = uiState) + val (isButtonEnabled, isButtonDisplayed) = isButtonEnabled(uiState) val buttonIcon = if (showTangemIcon) { TangemButtonIconPosition.End(R.drawable.ic_tangem_24) } else { @@ -101,17 +97,17 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier } } AnimatedVisibility( - visible = buttonClick != null, + visible = isButtonDisplayed, enter = fadeIn(), exit = fadeOut(), ) { TangemButton( text = stringResource(buttonTextId), icon = buttonIcon, - enabled = isButtonEnabled && buttonClick != null, + enabled = isButtonEnabled && isButtonDisplayed, onClick = { if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - if (buttonClick != null) buttonClick() + onPrimaryClick(uiState) }, showProgress = isInProgressInnerState, modifier = Modifier.fillMaxWidth(), @@ -121,37 +117,62 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier } } -private fun getButtonData(currentState: StakingUiState): Pair Unit)?> { +private fun getButtonData(currentState: StakingUiState): Int { return when (currentState.currentStep) { StakingStep.InitialInfo -> { val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { - val click = if (initialState.isStakeMoreAvailable) { - currentState.clickIntents::onNextClick - } else { - null - } - R.string.staking_stake_more to click + R.string.staking_stake_more } else { - R.string.common_next to currentState.clickIntents::onNextClick + R.string.common_next } } - StakingStep.Amount, - -> R.string.common_next to currentState.clickIntents::onNextClick StakingStep.Confirmation -> { val confirmationState = currentState.confirmationState if (confirmationState is StakingStates.ConfirmationState.Data) { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - R.string.common_close to currentState.clickIntents::onBackClick + R.string.common_close } else { - R.string.common_stake to currentState.clickIntents::onNextClick + R.string.common_stake } } else { - R.string.common_close to currentState.clickIntents::onBackClick + R.string.common_close } } - StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick - StakingStep.RewardsValidators -> R.string.common_next to null + StakingStep.Validators -> R.string.common_continue + StakingStep.Amount, + StakingStep.RewardsValidators, + -> R.string.common_next + } +} + +private fun onPrimaryClick(currentState: StakingUiState) { + when (currentState.currentStep) { + StakingStep.InitialInfo -> { + val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data + if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { + if (initialState.isStakeMoreAvailable) { + currentState.clickIntents.onNextClick() + } + } else { + currentState.clickIntents.onNextClick() + } + } + StakingStep.Amount -> currentState.clickIntents.onNextClick() + StakingStep.Confirmation -> { + val confirmationState = currentState.confirmationState + if (confirmationState is StakingStates.ConfirmationState.Data) { + if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { + currentState.clickIntents.onBackClick() + } else { + currentState.clickIntents.onNextClick() + } + } else { + currentState.clickIntents.onBackClick() + } + } + StakingStep.Validators -> currentState.clickIntents.onNextClick() + StakingStep.RewardsValidators -> Unit } } @@ -165,12 +186,16 @@ private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { -> true } -private fun isButtonEnabled(uiState: StakingUiState): Boolean { +private fun isButtonEnabled(uiState: StakingUiState): Pair { return when (uiState.currentStep) { - StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled - StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled - StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled - StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled - StakingStep.Validators -> true + StakingStep.InitialInfo -> { + val initialState = uiState.initialInfoState as? StakingStates.InitialInfoState.Data + val isDisplayed = initialState?.isStakeMoreAvailable == true + uiState.initialInfoState.isPrimaryButtonEnabled to isDisplayed + } + StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled to true + StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled to true + StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled to false + StakingStep.Validators -> true to true } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index e2cb8c86da..8302d36d14 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,15 +1,18 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick() + fun onNextClick(pendingActions: ImmutableList = persistentListOf()) fun onPrevClick() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 0e182f302b..1aa55e58c3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -15,6 +15,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.staking.* +import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams @@ -38,13 +39,14 @@ import com.tangem.features.staking.impl.presentation.state.transformers.validato import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class StakingViewModel @Inject constructor( private val stateController: StakingStateController, @@ -98,11 +100,11 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onBackClick() } - override fun onNextClick() { + override fun onNextClick(pendingActions: ImmutableList) { handleOnNextConfirmationClick() stakingStateRouter.onNextClick() if (isAssentState()) { - initStaking() + estimateGas(pendingActions) } } @@ -115,16 +117,11 @@ internal class StakingViewModel @Inject constructor( value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state") val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: error("No validator provided") - - val actionType = when (value.routeType) { - RouteType.STAKE -> StakingActionCommonType.ENTER - RouteType.UNSTAKE -> StakingActionCommonType.EXIT - RouteType.CLAIM -> TODO() - } + val pendingActions = confirmationState.pendingActions val stakingTransaction = getStakingTransactionUseCase( params = ActionParams( - actionCommonType = actionType, + actionCommonType = getStakingCommonType(), integrationId = yield.id, amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided"), @@ -132,6 +129,8 @@ internal class StakingViewModel @Inject constructor( ?: error("No available address"), validatorAddress = validatorState.chosenValidator.address, token = yield.token, + passthrough = pendingActions.firstOrNull()?.passthrough, + type = pendingActions.firstOrNull()?.type, ), ).getOrElse { error(it) @@ -141,37 +140,35 @@ internal class StakingViewModel @Inject constructor( sendStakingTransaction( transactionId = stakingTransaction.id, gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"), - TransactionData.Compiled(value = it.hexToBytes()), + txData = TransactionData.Compiled(value = it.hexToBytes()), + pendingActions = pendingActions, ) } ?: error("No unsigned transaction available") } } } - private fun initStaking() { + private fun estimateGas(pendingActions: ImmutableList) { viewModelScope.launch { stateController.update( SetConfirmationStateLoadingTransformer( yield = yield, ), ) - - val actionType = when (value.routeType) { - RouteType.STAKE -> StakingActionCommonType.ENTER - RouteType.UNSTAKE -> StakingActionCommonType.EXIT - RouteType.CLAIM -> TODO() - } + val cryptoCurrencyValue = cryptoCurrencyStatus.value val stakingGasEstimate = estimateGasUseCase( params = ActionParams( - actionCommonType = actionType, + actionCommonType = getStakingCommonType(), integrationId = yield.id, amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided"), - address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value ?: error("No available address"), validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), token = yield.token, + passthrough = pendingActions.firstOrNull()?.passthrough, + type = pendingActions.firstOrNull()?.type, ), ).getOrElse { error("Can't get fee info") } @@ -180,6 +177,7 @@ internal class StakingViewModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = stakingGasEstimate, + pendingActionList = pendingActions, ), ) } @@ -230,9 +228,14 @@ internal class StakingViewModel @Inject constructor( } override fun onActiveStake(activeStake: BalanceState) { - stateController.update { it.copy(routeType = RouteType.UNSTAKE) } + val routeType = if (activeStake.pendingActions.isEmpty()) { + RouteType.UNSTAKE + } else { + RouteType.OTHER + } + stateController.update { it.copy(routeType = routeType) } stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue)) - onNextClick() + onNextClick(activeStake.pendingActions) } override fun onExploreClick() { @@ -314,6 +317,7 @@ internal class StakingViewModel @Inject constructor( transactionId: String, gasEstimate: StakingGasEstimate, txData: TransactionData, + pendingActions: ImmutableList, ) { sendTransactionUseCase( txData = txData, @@ -327,8 +331,10 @@ internal class StakingViewModel @Inject constructor( appCurrencyProvider = Provider { appCurrency }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, stakingGasEstimate = gasEstimate, + pendingActionList = pendingActions, ), ) + // todo add error dialog }, ifRight = { txHash -> submitHash(transactionId, txHash) @@ -370,4 +376,12 @@ internal class StakingViewModel @Inject constructor( (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == InnerConfirmationStakingState.ASSENT } + + private fun getStakingCommonType() = when (value.routeType) { + RouteType.STAKE -> StakingActionCommonType.ENTER + RouteType.UNSTAKE -> StakingActionCommonType.EXIT + RouteType.CLAIM, + RouteType.OTHER, + -> StakingActionCommonType.PENDING + } } \ No newline at end of file From f426a1f92271e10df3c3e2e2576f94f23ad64153 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 12:02:56 +0300 Subject: [PATCH 180/223] Updated on 2026-08-14 --- .../response/TokenMarketListResponse.kt | 2 +- .../java/com/tangem/pagination/BatchAction.kt | 6 + .../com/tangem/pagination/BatchFetchResult.kt | 2 + .../com/tangem/pagination/BatchListSource.kt | 52 ++-- .../fetcher/LimitOffsetBatchFetcher.kt | 5 +- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 6 + .../markets/DefaultMarketsTokenRepository.kt | 81 ++++--- .../converters/TokenMarketListConverter.kt | 10 +- .../markets/GetMarketsTokenListFlowUseCase.kt | 16 +- .../repositories/MarketsTokenRepository.kt | 6 +- features/markets/impl/build.gradle.kts | 1 + .../impl/DefaultMarketsListComponent.kt | 10 +- .../markets/model/MarketsListModel.kt | 122 +++++++--- .../converters/MarketsTokenItemConverter.kt | 11 +- ...ager.kt => MarketsListBatchFlowManager.kt} | 67 +++++- .../statemanager/MarketsListUMStateManager.kt | 108 +++++++-- .../markets/model/utils/LoggingUtils.kt | 10 + .../tangem/features/markets/ui/MarketsList.kt | 189 +++++++-------- .../ui/components/MarketsListLazyColumn.kt | 222 ++++++++++++++++++ .../MarketsListSortByBottomSheet.kt | 2 +- .../markets/ui/components/UnableToLoadData.kt | 47 ++++ .../features/markets/ui/entity/ListItem.kt | 3 - .../markets/ui/entity/MarketsListItemUM.kt | 1 + .../markets/ui/entity/MarketsListUM.kt | 12 + .../entity}/SortByBottomSheetContentUM.kt | 3 +- 26 files changed, 748 insertions(+), 247 deletions(-) rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/{MarketsListUiItemsManager.kt => MarketsListBatchFlowManager.kt} (81%) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt rename features/markets/impl/src/main/kotlin/com/tangem/features/markets/{model => ui/entity}/SortByBottomSheetContentUM.kt (70%) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 4157bd2bd6..b1e8675206 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -5,7 +5,7 @@ import java.math.BigDecimal data class TokenMarketListResponse( @Json(name = "imageHost") - val imageHost: String, + val imageHost: String?, @Json(name = "tokens") val tokens: List, @Json(name = "total") diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt index 8078c699b1..fd013069eb 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -68,4 +68,10 @@ sealed class BatchAction { class CancelUpdates( val predicate: (UpdateBatches) -> Boolean, ) : BatchAction() + + /** + * Clears the state and stops all current batch loading and updates + * After this status becomes [PaginationStatus.None] + */ + data object Reset : BatchAction() } \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt index a358df8bf2..f7b78b226f 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -12,10 +12,12 @@ sealed class BatchFetchResult { * Represents a successful result of a batch fetch request. * * @param data fetched data. + * @param empty indicates that data is empty and [BatchListSource] shouldn't create new batch for this result * @param last indicates if this is the last batch for the request. */ data class Success( val data: TData, + val empty: Boolean, val last: Boolean, ) : BatchFetchResult() diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt index d286dafab8..158de617e7 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -92,11 +92,7 @@ private class DefaultBatchListSource awaitCancellation() } finally { withContext(NonCancellable) { - stopAllUpdates() - loadMoreActionJob = null - loadMoreActionJob = null - lastRequestResult.value = null - state.value = BatchListState(emptyList(), PaginationStatus.None) + resetState() } } } @@ -110,6 +106,7 @@ private class DefaultBatchListSource } } + @Suppress("CyclomaticComplexMethod") private fun collectActions(action: BatchAction) { when (action) { is BatchAction.Reload -> { @@ -165,6 +162,9 @@ private class DefaultBatchListSource loadMoreActionJob?.cancel() reloadActionJob?.cancel() } + BatchAction.Reset -> { + resetState() + } } } @@ -237,13 +237,17 @@ private class DefaultBatchListSource state.value = when (res) { is BatchFetchResult.Success -> { - val key = generateNewKey(listOf()) - val batch = Batch( - key = key, - data = res.data, - ) + val batch = if (res.empty.not()) { + Batch( + key = generateNewKey(listOf()), + data = res.data, + ) + } else { + null + } + BatchListState( - data = listOf(batch), + data = batch?.let { listOf(it) } ?: emptyList(), status = if (res.last) { PaginationStatus.EndOfPagination } else { @@ -289,13 +293,17 @@ private class DefaultBatchListSource state.update { currentState -> when (res) { is BatchFetchResult.Success -> { - val newBatch = Batch( - key = generateNewKey(currentState.data.map { it.key }), - data = res.data, - ) + val newBatch = if (res.empty.not()) { + Batch( + key = generateNewKey(currentState.data.map { it.key }), + data = res.data, + ) + } else { + null + } currentState.copy( - data = currentState.data + newBatch, + data = newBatch?.let { currentState.data + it } ?: currentState.data, status = if (res.last) { PaginationStatus.EndOfPagination } else { @@ -400,6 +408,18 @@ private class DefaultBatchListSource waitingUpdateJobs.value.any { it.first.operationId == operationId } } + private fun resetState() { + if (updateFetcher != null) { + stopAllUpdates() + } + loadMoreActionJob?.cancel() + loadMoreActionJob = null + reloadActionJob?.cancel() + reloadActionJob = null + lastRequestResult.value = null + state.value = BatchListState(emptyList(), PaginationStatus.None) + } + private fun stopAllUpdates() { updateAsyncJobs.update { actionAsyncJobs -> actionAsyncJobs.forEach { diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt index de125a62ff..73a2c5f0a7 100644 --- a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -32,6 +32,7 @@ class LimitOffsetBatchFetcher( suspend fun fetch( request: Request, lastResult: BatchFetchResult?, + isFirstBatchFetching: Boolean, ): BatchFetchResult } @@ -45,7 +46,7 @@ class LimitOffsetBatchFetcher( ) val res = runCatching { - subFetcher.fetch(req, null) + subFetcher.fetch(request = req, lastResult = null, isFirstBatchFetching = true) }.getOrElse { currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) @@ -77,7 +78,7 @@ class LimitOffsetBatchFetcher( } val res = runCatching { - subFetcher.fetch(req, lastResult) + subFetcher.fetch(request = req, lastResult = lastResult, isFirstBatchFetching = false) }.getOrElse { currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 14c45c2681..fba85fe955 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -68,6 +68,7 @@ Недостаточно ADA Принять Доступ запрещен + Разрешить Применить Одобрение Подтвердить diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index c1a5d5ad43..2c36f14fdf 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -337,7 +337,12 @@ My portfolio Market To generate addresses for selected networks, you need to attach a Tangem card + Unable to load the data… Quick actions + Result + See tokens under 100k market cap + Show tokens + No result Select network Select wallet 1m @@ -722,6 +727,7 @@ Operation from: %s to: %s + Try again You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d You\'ve scanned wrong twin card. Please try another one This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index a0b50723dc..be83e51fa2 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -10,7 +10,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.* +import java.util.concurrent.atomic.AtomicLong internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, @@ -20,42 +20,61 @@ internal class DefaultMarketsTokenRepository( private val tokenListConverter = TokenMarketListConverter() - private val tokenMarketsFetcher - get() = LimitOffsetBatchFetcher( - prefetchDistance = 150, - batchSize = 100, - subFetcher = object : LimitOffsetBatchFetcher.SubFetcher> { + private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher( + prefetchDistance = firstBatchSize, + batchSize = nextBatchSize, + subFetcher = object : LimitOffsetBatchFetcher.SubFetcher> { - var requestTimeStamp: Long? = null // TODO when backend is ready + val requestTimeStamp = AtomicLong(0) - override suspend fun fetch( - request: LimitOffsetBatchFetcher.Request, - lastResult: BatchFetchResult>?, - ): BatchFetchResult> { - val res = retryOnError(priority = true) { - marketsApi.getCoinsList( - currency = request.params.fiatPriceCurrency, - interval = request.params.priceChangeInterval.toRequestParam(), - order = request.params.order.toRequestParam(), - search = request.params.searchText, - generalCoins = request.params.showUnder100kMarketCapTokens.not(), - offset = request.offset, - limit = request.limit, - ).getOrThrow() - } + override suspend fun fetch( + request: LimitOffsetBatchFetcher.Request, + lastResult: BatchFetchResult>?, + isFirstBatchFetching: Boolean, + ): BatchFetchResult> { + val searchText = + if (request.params.searchText.isNullOrBlank()) null else request.params.searchText - val last = res.tokens.size < request.limit - - return BatchFetchResult.Success( - data = tokenListConverter.convert(res), - last = last, - ) + val requestCall = suspend { + marketsApi.getCoinsList( + currency = request.params.fiatPriceCurrency, + interval = request.params.priceChangeInterval.toRequestParam(), + order = request.params.order.toRequestParam(), + search = searchText, + generalCoins = request.params.showUnder100kMarketCapTokens.not(), + offset = request.offset, + limit = request.limit, + ).getOrThrow() } - }, - ) + + // we shouldn't infinitely retry on the first batch request + val res = if (isFirstBatchFetching) { + requestCall() + } else { + retryOnError(priority = true) { + requestCall() + } + } + + if (isFirstBatchFetching) { + requestTimeStamp.set(0) // TODO when backend is ready + } + + val last = res.tokens.size < request.limit + + return BatchFetchResult.Success( + data = tokenListConverter.convert(res), + last = last, + empty = res.tokens.isEmpty(), + ) + } + }, + ) override fun getTokenListFlow( batchingContext: BatchingContext, + firstBatchSize: Int, + nextBatchSize: Int, ): BatchFlow, TokenMarketUpdateRequest> { val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher( tangemTechApi = tangemTechApi, @@ -66,7 +85,7 @@ internal class DefaultMarketsTokenRepository( fetchDispatcher = dispatcherProvider.io, context = batchingContext, generateNewKey = { it.size }, - batchFetcher = tokenMarketsFetcher, + batchFetcher = createTokenMarketsFetcher(firstBatchSize = firstBatchSize, nextBatchSize = nextBatchSize), updateFetcher = tokenMarketsUpdateFetcher, ).toBatchFlow() } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 8f9c571a86..96f7e01ebf 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -9,6 +9,14 @@ import com.tangem.utils.converter.Converter class TokenMarketListConverter : Converter> { override fun convert(value: TokenMarketListResponse): List { + val imageHost = value.imageHost ?: run { + if (value.tokens.isEmpty()) { + return emptyList() + } else { + error("imageHost cannot be null") + } + } + return value.tokens.map { token -> TokenMarket( id = token.id, @@ -16,7 +24,7 @@ class TokenMarketListConverter : Converter, TokenMarketUpda class GetMarketsTokenListFlowUseCase( private val marketsTokenRepository: MarketsTokenRepository, ) { - operator fun invoke(batchingContext: TokenListBatchingContext): TokenListBatchFlow { - return marketsTokenRepository.getTokenListFlow(batchingContext) + operator fun invoke(batchingContext: TokenListBatchingContext, batchFlowType: BatchFlowType): TokenListBatchFlow { + return marketsTokenRepository.getTokenListFlow( + batchingContext = batchingContext, + firstBatchSize = batchFlowType.firstBatchSize, + nextBatchSize = batchFlowType.nextBatchSize, + ) + } + + enum class BatchFlowType( + val firstBatchSize: Int, + val nextBatchSize: Int, + ) { + Main(firstBatchSize = 150, nextBatchSize = 100), + Search(firstBatchSize = 50, nextBatchSize = 50), } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt index b72061f9f2..31693705dc 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -4,5 +4,9 @@ import com.tangem.domain.markets.* interface MarketsTokenRepository { - fun getTokenListFlow(batchingContext: TokenListBatchingContext): TokenListBatchFlow + fun getTokenListFlow( + batchingContext: TokenListBatchingContext, + firstBatchSize: Int, + nextBatchSize: Int, + ): TokenListBatchFlow } \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 201feb6d22..46bc8c674a 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) /* DI */ implementation(deps.hilt.android) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt index 55757a3a42..c5329d71a3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/component/impl/DefaultMarketsListComponent.kt @@ -30,15 +30,17 @@ internal class DefaultMarketsListComponent @AssistedInject constructor( modifier: Modifier, ) { val state by model.state.collectAsStateWithLifecycle() + val bsState by bottomSheetState - LaunchedEffect(bottomSheetState.value) { - model.containerBottomSheetState.value = bottomSheetState.value + LaunchedEffect(bsState) { + model.containerBottomSheetState.value = bsState } MarketsList( - onHeaderSizeChange = onHeaderSizeChange, - state = state, modifier = modifier, + state = state, + onHeaderSizeChange = onHeaderSizeChange, + bottomSheetState = bsState, ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt index b0d1dac2b3..97fc37c161 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/MarketsListModel.kt @@ -9,21 +9,21 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.model.statemanager.MarketsListUMStateManager -import com.tangem.features.markets.model.statemanager.MarketsListUiItemsManager +import com.tangem.features.markets.model.statemanager.MarketsListBatchFlowManager import com.tangem.features.markets.ui.entity.ListUM import com.tangem.features.markets.ui.entity.SortByTypeUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L +private const val SEARCH_QUERY_DEBOUNCE_MILLIS = 800L +@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) @ComponentScoped @Stable internal class MarketsListModel @Inject constructor( @@ -42,38 +42,65 @@ internal class MarketsListModel @Inject constructor( ) private val visibleItemIds = MutableStateFlow>(emptyList()) - val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) + private val marketsListUMStateManager = MarketsListUMStateManager( onLoadMoreUiItems = { activeListManager.loadMore() }, visibleItemsChanged = { visibleItemIds.value = it }, + onRetryButtonClicked = { activeListManager.reload() }, ) - private val marketsListManager = MarketsListUiItemsManager( - logTag = "main", + private val mainMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, currentAppCurrency = Provider { currentAppCurrency.value }, currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, + currentSortByType = Provider { marketsListUMStateManager.selectedSortByType }, + currentSearchText = Provider { null }, modelScope = modelScope, dispatchers = dispatchers, ) - private val searchMarketsListManager = MarketsListUiItemsManager( - logTag = "search", + private val searchMarketsListManager = MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, currentAppCurrency = Provider { currentAppCurrency.value }, - currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, + currentTrendInterval = Provider { marketsListUMStateManager.selectedInterval }, // FIXME fix on backend + currentSortByType = Provider { SortByTypeUM.Rating }, // FIXME maybe fix on backend + currentSearchText = Provider { marketsListUMStateManager.searchQuery }, modelScope = modelScope, dispatchers = dispatchers, ) - private var activeListManager: MarketsListUiItemsManager = marketsListManager + private var activeListManager: MarketsListBatchFlowManager = mainMarketsListManager + + val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED) val state = marketsListUMStateManager.state.asStateFlow() init { + @Suppress("UnnecessaryParentheses") modelScope.launch { - marketsListManager.uiItems - .collectLatest { - marketsListUMStateManager.onUiItemsChanged(it) + marketsListUMStateManager.isInSearchStateFlow + .flatMapLatest { isInSearchMode -> + if (isInSearchMode) { + combine( + searchMarketsListManager.uiItems, + searchMarketsListManager.isInInitialLoadingErrorState, + searchMarketsListManager.isSearchNotFoundState, + ) { items, isError, notFound -> + (items to isError) to notFound + } + } else { + combine( + mainMarketsListManager.uiItems, + mainMarketsListManager.isInInitialLoadingErrorState, + ) { items, isError -> (items to isError) to false } + } + }.collect { + marketsListUMStateManager.onUiItemsChanged( + uiItems = it.first.first, + isInErrorState = it.first.second, + isSearchNotFound = it.second, + ) } } @@ -87,23 +114,16 @@ internal class MarketsListModel @Inject constructor( currentAppCurrency .drop(1) .onEach { - marketsListManager.reload( - interval = marketsListUMStateManager.selectedInterval, - sortBy = marketsListUMStateManager.selectedSortByType, - ) + mainMarketsListManager.reload() if (marketsListUMStateManager.isInSearchState) { - // TODO - searchMarketsListManager.reload( - interval = marketsListUMStateManager.selectedInterval, - sortBy = marketsListUMStateManager.selectedSortByType, - ) + searchMarketsListManager.reload() } }.launchIn(modelScope) // load charts when new batch is being loaded - marketsListManager.onLastBatchLoadedSuccess + mainMarketsListManager.onLastBatchLoadedSuccess .onEach { - marketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + mainMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) } .launchIn(modelScope) @@ -117,11 +137,11 @@ internal class MarketsListModel @Inject constructor( .collectLatest { interval -> when (marketsListUMStateManager.selectedSortByType) { SortByTypeUM.Rating -> { - marketsListManager.updateUIWithSameState() - val batchKeys = marketsListManager.getBatchKeysByItemIds(visibleItemIds.value) - marketsListManager.loadCharts(batchKeys, interval) + mainMarketsListManager.updateUIWithSameState() + val batchKeys = mainMarketsListManager.getBatchKeysByItemIds(visibleItemIds.value) + mainMarketsListManager.loadCharts(batchKeys, interval) } - else -> marketsListManager.reload(interval, marketsListUMStateManager.selectedSortByType) + else -> mainMarketsListManager.reload() } } } @@ -133,12 +153,12 @@ internal class MarketsListModel @Inject constructor( .distinctUntilChanged() .drop(1) .collectLatest { - marketsListManager.reload(marketsListUMStateManager.selectedInterval, it) + mainMarketsListManager.reload() } } // listen current visible batch and update charts - modelScope.launch(dispatchers.default) { + modelScope.launch { visibleItemIds .mapNotNull { if (it.isNotEmpty()) { @@ -153,11 +173,41 @@ internal class MarketsListModel @Inject constructor( } } + // ===Search=== + + modelScope.launch { + marketsListUMStateManager.isInSearchStateFlow + .collectLatest { isInSearchMode -> + activeListManager = if (isInSearchMode) { + searchMarketsListManager + } else { + searchMarketsListManager.clearStateAndStopAllActions() + mainMarketsListManager + } + } + } + + modelScope.launch { + marketsListUMStateManager.searchQueryFlow + .filter { it.isNotEmpty() } + .debounce(timeoutMillis = SEARCH_QUERY_DEBOUNCE_MILLIS) + .filter { activeListManager == searchMarketsListManager } + .collectLatest { + searchMarketsListManager.reload(searchText = it) + } + } + + modelScope.launch { + searchMarketsListManager + .onLastBatchLoadedSuccess + .collectLatest { + searchMarketsListManager.loadCharts(setOf(it), marketsListUMStateManager.selectedInterval) + modelScope.loadQuotesWithTimer(timeMillis = UPDATE_QUOTES_TIMER_MILLIS) + } + } + // initial loading - marketsListManager.reload( - interval = marketsListUMStateManager.selectedInterval, - sortBy = marketsListUMStateManager.selectedSortByType, - ) + mainMarketsListManager.reload() } private var updateQuotesJob = JobHolder() @@ -167,7 +217,7 @@ internal class MarketsListModel @Inject constructor( delay(timeMillis) // Update quotes only when the container bottom sheet is in the expanded state containerBottomSheetState.first { it == BottomSheetState.EXPANDED } - activeListManager.updateQuotes() + activeListManager.updateQuotes() // TODO update a batch that is currently on screen } }.saveIn(updateQuotesJob) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt index 6366b68d38..c2badcd569 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/converters/MarketsTokenItemConverter.kt @@ -30,6 +30,7 @@ internal class MarketsTokenItemConverter( trendPercentText = value.getTrendPercent(), trendType = value.getTrendType(), chardData = value.getChartData(), + showUnder100kMarketCap = value.isUnder100kMarketCap(), ) } @@ -76,7 +77,7 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotes?.currentPrice - val priceText = BigDecimalFormatter.formatFiatAmount( + val priceText = BigDecimalFormatter.formatFiatAmountUncapped( fiatAmount = tokenQuotes.currentPrice, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, @@ -141,4 +142,12 @@ internal class MarketsTokenItemConverter( useAbsoluteValue = true, ) } + + private fun TokenMarket.isUnder100kMarketCap(): Boolean { + return tokenQuotes.currentPrice.compareTo(decimal100k) == -1 + } + + private companion object { + val decimal100k: BigDecimal = BigDecimal.valueOf(100_000) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListBatchFlowManager.kt similarity index 81% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListBatchFlowManager.kt index 53147eb848..ae897a5b9a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUiItemsManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListBatchFlowManager.kt @@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.features.markets.model.converters.MarketsTokenItemConverter import com.tangem.features.markets.model.utils.logAction +import com.tangem.features.markets.model.utils.logStatus import com.tangem.features.markets.model.utils.logUpdateResults import com.tangem.features.markets.ui.entity.MarketsListItemUM import com.tangem.features.markets.ui.entity.MarketsListUM.TrendInterval @@ -18,23 +19,27 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -private const val LOG_EVENTS = false +private const val LOG_EVENTS = true -internal class MarketsListUiItemsManager( - private val logTag: String = "main", +@Suppress("LongParameterList") +internal class MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, private val currentTrendInterval: Provider, private val currentAppCurrency: Provider, + private val currentSearchText: Provider, + private val currentSortByType: Provider, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { private val actionsFlow = MutableSharedFlow>() private val batchFlow = getMarketsTokenListFlowUseCase( - TokenListBatchingContext( + batchingContext = TokenListBatchingContext( actionsFlow = actionsFlow, coroutineScope = modelScope, ), + batchFlowType = batchFlowType, ) val uiItems: StateFlow> @@ -53,7 +58,7 @@ internal class MarketsListUiItemsManager( ) val onLastBatchLoadedSuccess = batchFlow.state - .distinctUntilChanged { old, new -> old.status === new.status } + .distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size } .mapNotNull { when (val status = it.status) { is PaginationStatus.Paginating -> { @@ -70,6 +75,28 @@ internal class MarketsListUiItemsManager( } } + val isInInitialLoadingErrorState = batchFlow.state + .map { it.status is PaginationStatus.InitialLoadingError } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + val isSearchNotFoundState = batchFlow.state + .map { + currentSearchText().isNullOrEmpty().not() && + it.status is PaginationStatus.EndOfPagination && + it.data.isEmpty() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + private val uiBatches = MutableStateFlow>>>(emptyList()) init { @@ -86,11 +113,16 @@ internal class MarketsListUiItemsManager( if (LOG_EVENTS) { batchFlow.updateResults - .onEach { logUpdateResults(logTag, it) } + .onEach { logUpdateResults(batchFlowType.name, it) } + .launchIn(modelScope) + + batchFlow.state + .map { it.status } + .onEach { logStatus(batchFlowType.name, it) } .launchIn(modelScope) actionsFlow - .onEach { logAction(logTag, it) } + .onEach { logAction(batchFlowType.name, it) } .launchIn(modelScope) } } @@ -144,17 +176,21 @@ internal class MarketsListUiItemsManager( } } - fun reload(interval: TrendInterval, sortBy: SortByTypeUM) { + fun reload(searchText: String? = null) { modelScope.launch { uiBatches.value = emptyList() actionsFlow.emit( BatchAction.Reload( requestParams = TokenMarketListConfig( fiatPriceCurrency = currentAppCurrency().code, - searchText = null, - showUnder100kMarketCapTokens = false, - priceChangeInterval = interval.toBatchRequestInterval(), - order = sortBy.toRequestOrder(), + searchText = if (currentSearchText() == null) { + null + } else { + searchText ?: currentSearchText() + }, + showUnder100kMarketCapTokens = false, // TODO + priceChangeInterval = currentTrendInterval().toBatchRequestInterval(), + order = currentSortByType().toRequestOrder(), ), ), ) @@ -229,6 +265,13 @@ internal class MarketsListUiItemsManager( } } + fun clearStateAndStopAllActions() { + uiBatches.value = emptyList() + modelScope.launch { + actionsFlow.emit(BatchAction.Reset) + } + } + fun getBatchKeysByItemIds(ids: List): Set { val currentData = batchFlow.state.value.data diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt index 052239a869..d845850f44 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/statemanager/MarketsListUMStateManager.kt @@ -3,32 +3,43 @@ package com.tangem.features.markets.model.statemanager import androidx.compose.runtime.Stable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.markets.impl.R -import com.tangem.features.markets.model.SortByBottomSheetContentUM +import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM import com.tangem.features.markets.ui.entity.ListUM import com.tangem.features.markets.ui.entity.MarketsListItemUM import com.tangem.features.markets.ui.entity.MarketsListUM import com.tangem.features.markets.ui.entity.SortByTypeUM import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* @Stable internal class MarketsListUMStateManager( private val onLoadMoreUiItems: () -> Unit, private val visibleItemsChanged: (itemsKeys: List) -> Unit, + private val onRetryButtonClicked: () -> Unit, ) { private var sortByBottomSheetIsShown get() = state.value.sortByBottomSheet.isShow set(value) = state.update { it.copy(sortByBottomSheet = it.sortByBottomSheet.copy(isShow = value)) } - private val isInSearchStateFlow = MutableStateFlow(false) + var searchQuery + get() = state.value.searchBar.query + private set(value) = state.update { + it.copy( + searchBar = it.searchBar.copy( + query = value, + isActive = value.isNotEmpty(), + ), + ) + } var isInSearchState - get() = isInSearchStateFlow.value - set(value) { isInSearchStateFlow.value = value } + get() = state.value.searchBar.isActive + private set(value) = state.update { it.copy(searchBar = it.searchBar.copy(isActive = value)) } var selectedSortByType get() = state.value.selectedSortBy @@ -40,29 +51,64 @@ internal class MarketsListUMStateManager( selectedOption = value, ), ), + list = if (it.list is ListUM.Content && it.selectedSortBy != value) { + it.list.copy(triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() }) + } else { + it.list + }, ) } var selectedInterval get() = state.value.selectedInterval - set(value) = state.update { it.copy(selectedInterval = value) } + set(value) = state.update { + it.copy( + selectedInterval = value, + list = if (it.list is ListUM.Content && + it.selectedSortBy != SortByTypeUM.Rating && + it.selectedInterval != value + ) { + it.list.copy(triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() }) + } else { + it.list + }, + ) + } val state = MutableStateFlow(state()) + val isInSearchStateFlow = state.map { it.searchBar.isActive }.distinctUntilChanged() + val searchQueryFlow = state.map { it.searchBar.query }.distinctUntilChanged() - fun onUiItemsChanged(uiItems: ImmutableList) { + fun onUiItemsChanged( + isInErrorState: Boolean, + isSearchNotFound: Boolean, + uiItems: ImmutableList, + ) { state.update { - if (uiItems.isEmpty()) { - it.copy( - list = ListUM.Loading, - ) - } else { - it.copy( - list = ListUM.Content( - items = uiItems, - loadMore = onLoadMoreUiItems, - visibleIdsChanged = visibleItemsChanged, - ), - ) + when { + isInErrorState -> { + it.copy( + list = ListUM.LoadingError(onRetryClicked = onRetryButtonClicked), + ) + } + isSearchNotFound -> { + it.copy(list = ListUM.SearchNothingFound) + } + uiItems.isEmpty() -> { + it.copy(list = ListUM.Loading) + } + else -> { + it.copy( + list = ListUM.Content( + items = uiItems, + loadMore = onLoadMoreUiItems, + visibleIdsChanged = visibleItemsChanged, + showUnder100kTokens = true, + onShowTokensUnder100kClicked = { }, + triggerScrollReset = consumedEvent(), + ), + ) + } } } } @@ -71,10 +117,10 @@ internal class MarketsListUMStateManager( list = ListUM.Loading, searchBar = SearchBarUM( placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), - query = "", // TODO - onQueryChange = {}, // TODO - isActive = false, // TODO - onActiveChange = { }, // TODO + query = "", + onQueryChange = { searchQuery = it }, + isActive = false, + onActiveChange = { }, ), selectedSortBy = SortByTypeUM.Rating, selectedInterval = MarketsListUM.TrendInterval.H24, @@ -103,4 +149,18 @@ internal class MarketsListUMStateManager( ) } } + + private fun consumeTriggerResetScrollEvent() { + state.update { + it.copy( + list = if (it.list is ListUM.Content) { + it.list.copy( + triggerScrollReset = consumedEvent(), + ) + } else { + it.list + }, + ) + } + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt index a7594edf81..e9cc0e8b31 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/utils/LoggingUtils.kt @@ -5,8 +5,18 @@ import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.markets.TokenMarketUpdateRequest import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchUpdateResult +import com.tangem.pagination.PaginationStatus import timber.log.Timber +internal fun logStatus(tag: String, status: PaginationStatus>) { + Timber.tag(tag).d( + """ + Status + $status + """.trimIndent(), + ) +} + internal fun logAction(tag: String, action: BatchAction) { when (action) { is BatchAction.Reload -> Timber.tag(tag).d( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt index 4f61361468..3ff1f1d4e7 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsList.kt @@ -1,25 +1,25 @@ package com.tangem.features.markets.ui +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.SecondarySmallButton @@ -28,31 +28,41 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.keyboardAsState +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.component.BottomSheetState import com.tangem.features.markets.impl.R -import com.tangem.features.markets.model.SortByBottomSheetContentUM -import com.tangem.features.markets.ui.components.MarketsListItem -import com.tangem.features.markets.ui.components.MarketsListItemPlaceholder +import com.tangem.features.markets.ui.components.MarketsListLazyColumn import com.tangem.features.markets.ui.components.MarketsListSortByBottomSheet import com.tangem.features.markets.ui.entity.ListUM import com.tangem.features.markets.ui.entity.MarketsListUM +import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM import com.tangem.features.markets.ui.entity.SortByTypeUM import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Composable -internal fun MarketsList(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) { +internal fun MarketsList( + state: MarketsListUM, + onHeaderSizeChange: (Dp) -> Unit, + bottomSheetState: BottomSheetState, + modifier: Modifier = Modifier, +) { Content( modifier = modifier, state = state, onHeaderSizeChange = onHeaderSizeChange, ) - MarketsListSortByBottomSheet(config = state.sortByBottomSheet) + KeyboardEvents( + isSortByBottomSheetShown = state.sortByBottomSheet.isShow, + bottomSheetState = bottomSheetState, + ) } @Composable @@ -66,7 +76,6 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi .background(color = TangemTheme.colors.background.primary), ) { SearchBar( - state = state.searchBar, modifier = Modifier .background(color = TangemTheme.colors.background.primary) .padding( @@ -77,28 +86,40 @@ private fun Content(state: MarketsListUM, onHeaderSizeChange: (Dp) -> Unit, modi .onGloballyPositioned { with(density) { onHeaderSizeChange(it.size.height.toDp()) } }, + state = state.searchBar, ) Spacer(Modifier.height(TangemTheme.dimens.spacing20)) Column(Modifier.padding(horizontal = TangemTheme.dimens.size16)) { - Title() - SpacerH12() - Options( - sortByTypeUM = state.selectedSortBy, - trendInterval = state.selectedInterval, - onIntervalClick = state.onIntervalClick, - onSortByClick = state.onSortByButtonClick, - ) + Title(isInSearchMode = state.isInSearchMode) + AnimatedVisibility(state.isInSearchMode.not()) { + Column { + SpacerH12() + Options( + sortByTypeUM = state.selectedSortBy, + trendInterval = state.selectedInterval, + onIntervalClick = state.onIntervalClick, + onSortByClick = state.onSortByButtonClick, + ) + } + } } SpacerH12() - Items(state = state.list) + ItemsList( + isInSearchMode = state.isInSearchMode, + state = state.list, + ) } } @Composable -private fun Title(modifier: Modifier = Modifier) { +private fun Title(isInSearchMode: Boolean, modifier: Modifier = Modifier) { Text( modifier = modifier, - text = stringResource(id = R.string.markets_common_title), + text = if (isInSearchMode) { + stringResource(id = R.string.markets_search_result_title) + } else { + stringResource(id = R.string.markets_common_title) + }, style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) @@ -158,105 +179,49 @@ private fun Options( } @Composable -private fun Items(state: ListUM, modifier: Modifier = Modifier) { - val lazyListState = rememberLazyListState() - val scrollEnabled = state !is ListUM.Loading - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } +private fun ItemsList(isInSearchMode: Boolean, state: ListUM, modifier: Modifier = Modifier) { + val searchLazyListState = rememberLazyListState() + val mainLazyListState = rememberLazyListState() - LazyColumn( - modifier = modifier.nestedScroll(DisableParentConnection), - state = lazyListState, - contentPadding = PaddingValues(bottom = bottomBarHeight), - userScrollEnabled = scrollEnabled, - ) { - // ATTENTION! There should be no elements with a string key value except MarketsListItem! - when (state) { - ListUM.Loading -> { - items(count = 50, key = { it }) { - MarketsListItemPlaceholder() - } - } - ListUM.SearchNothingFound -> { - // TODO - } - is ListUM.Content -> { - items( - items = state.items, - key = { it.id }, - ) { item -> - MarketsListItem( - model = item, - ) - } - } - } - } - - LaunchedEffect(state) { - if (state is ListUM.Loading) { - lazyListState.scrollToItem(0) - } - } - - VisibleItemsTracker(lazyListState, state) - - InfiniteListHandler( - listState = lazyListState, - buffer = 50, - onLoadMore = remember(state) { - { - if (state is ListUM.Content) { - state.loadMore() - } - } + MarketsListLazyColumn( + modifier = modifier, + state = state, + isInSearchMode = isInSearchMode, + lazyListState = if (isInSearchMode) { + searchLazyListState + } else { + mainLazyListState }, ) } @Composable -fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { - val visibleItems by remember { - derivedStateOf { - listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String } +private fun KeyboardEvents(isSortByBottomSheetShown: Boolean, bottomSheetState: BottomSheetState) { + val keyboardController = LocalSoftwareKeyboardController.current + val keyboard by keyboardAsState() + val focusManager = LocalFocusManager.current + + BackHandler(enabled = keyboard is Keyboard.Opened) { + keyboardController?.hide() + } + + LaunchedEffect(keyboard) { + if (keyboard is Keyboard.Closed) { + focusManager.clearFocus() } } - LaunchedEffect(listState.isScrollInProgress, visibleItems) { - if (state is ListUM.Content && listState.isScrollInProgress.not()) { - state.visibleIdsChanged(visibleItems) - } - } -} - -@Composable -fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) { - val loadMore by remember { - derivedStateOf { - val layoutInfo = listState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 - - lastVisibleItemIndex > totalItemsNumber - buffer - } + LaunchedEffect(isSortByBottomSheetShown) { + keyboardController?.hide() } - val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } - var emitted by remember(totalItemsCount) { mutableStateOf(false) } - - LaunchedEffect(loadMore) { - if (loadMore && !emitted) { - emitted = true - onLoadMore() + LaunchedEffect(bottomSheetState) { + if (bottomSheetState == BottomSheetState.COLLAPSED) { + focusManager.clearFocus() } } } -private object DisableParentConnection : NestedScrollConnection { - override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - return available.copy(x = 0f) - } -} - //region: Preview @Preview @@ -272,8 +237,11 @@ private fun Preview() { item.copy(id = index.toString()) } .toImmutableList(), + showUnder100kTokens = false, loadMore = {}, visibleIdsChanged = {}, + onShowTokensUnder100kClicked = {}, + triggerScrollReset = consumedEvent(), ), searchBar = SearchBarUM( placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), @@ -287,12 +255,13 @@ private fun Preview() { onIntervalClick = {}, onSortByButtonClick = {}, sortByBottomSheet = TangemBottomSheetConfig( - false, + isShow = false, onDismissRequest = {}, content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), ), onHeaderSizeChange = {}, + bottomSheetState = BottomSheetState.EXPANDED, ) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt new file mode 100644 index 0000000000..dd7753fb72 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListLazyColumn.kt @@ -0,0 +1,222 @@ +package com.tangem.features.markets.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.ui.entity.ListUM +import kotlinx.coroutines.launch + +private const val LOAD_NEXT_PAGE_ON_END_INDEX = 50 + +@Composable +@Suppress("LongMethod") +internal fun MarketsListLazyColumn( + state: ListUM, + isInSearchMode: Boolean, + lazyListState: LazyListState, + modifier: Modifier = Modifier, +) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val coroutineScope = rememberCoroutineScope() + + SideEffect { + if (state is ListUM.Loading) { + coroutineScope.launch { + lazyListState.scrollToItem(0) + } + } + } + + if (state is ListUM.Content) { + EventEffect(state.triggerScrollReset) { + lazyListState.scrollToItem(0) + } + } + + if (state is ListUM.Loading) { + LazyColumn( + modifier = Modifier.nestedScroll(DisableParentConnection), + state = rememberLazyListState(), + contentPadding = PaddingValues(bottom = bottomBarHeight), + userScrollEnabled = false, + ) { + items(count = 100, key = { it }) { + MarketsListItemPlaceholder() + } + } + } else { + LazyColumn( + modifier = modifier.nestedScroll(DisableParentConnection), + state = lazyListState, + contentPadding = PaddingValues(bottom = bottomBarHeight), + userScrollEnabled = true, + ) { + // ATTENTION! There should be no elements with a string key value except MarketsListItem! + when (state) { + is ListUM.LoadingError -> { + item(key = "loading error".hashCode()) { + LoadingErrorItem( + modifier = Modifier.fillParentMaxSize(), + onTryAgain = state.onRetryClicked, + ) + } + } + ListUM.SearchNothingFound -> { + item(key = "not found text".hashCode()) { + SearchNothingFoundText( + modifier = Modifier.fillParentMaxSize(), + ) + } + } + is ListUM.Content -> { + items( + items = state.items, + key = { it.id }, + ) { item -> + MarketsListItem(model = item) + } + + if (isInSearchMode && state.showUnder100kTokens.not()) { + item(key = "show tokens under 100k".hashCode()) { + ShowTokensUnder100kItem( + onShowTokensClick = state.onShowTokensUnder100kClicked, + ) + } + } + } + else -> {} + } + } + } + + VisibleItemsTracker(lazyListState, state) + + InfiniteListHandler( + listState = lazyListState, + buffer = LOAD_NEXT_PAGE_ON_END_INDEX, + onLoadMore = remember(state) { + { + if (state is ListUM.Content) { + state.loadMore() + } + } + }, + ) +} + +@Composable +private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ) + .fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = onTryAgain) + } +} + +@Composable +private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.markets_search_see_tokens_under_100k), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.markets_search_show_tokens), + onClick = onShowTokensClick, + ), + ) + } +} + +@Composable +private fun SearchNothingFoundText(modifier: Modifier = Modifier) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.markets_search_token_no_result_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun VisibleItemsTracker(listState: LazyListState, state: ListUM) { + val visibleItems by remember { + derivedStateOf { + listState.layoutInfo.visibleItemsInfo.mapNotNull { it.key as? String } + } + } + + LaunchedEffect(listState.isScrollInProgress, visibleItems) { + if (state is ListUM.Content && listState.isScrollInProgress.not()) { + state.visibleIdsChanged(visibleItems) + } + } +} + +@Composable +fun InfiniteListHandler(listState: LazyListState, onLoadMore: () -> Unit, buffer: Int = 2) { + val loadMore by remember { + derivedStateOf { + val layoutInfo = listState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = (layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) + 1 + + lastVisibleItemIndex > totalItemsNumber - buffer + } + } + + val totalItemsCount by remember { derivedStateOf { listState.layoutInfo.totalItemsCount } } + var emitted by remember(totalItemsCount) { mutableStateOf(false) } + + LaunchedEffect(loadMore) { + if (loadMore && !emitted) { + emitted = true + onLoadMore() + } + } +} + +private object DisableParentConnection : NestedScrollConnection { + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return available.copy(x = 0f) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt index ae2a87f4a0..4ee563d08c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/MarketsListSortByBottomSheet.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.markets.impl.R -import com.tangem.features.markets.model.SortByBottomSheetContentUM +import com.tangem.features.markets.ui.entity.SortByBottomSheetContentUM import com.tangem.features.markets.ui.entity.SortByTypeUM @Composable diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt new file mode 100644 index 0000000000..cf58422fb5 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/components/UnableToLoadData.kt @@ -0,0 +1,47 @@ +package com.tangem.features.markets.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R + +@Composable +internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.markets_loading_error_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = onRetryClick, + ), + ) + } +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + UnableToLoadData(onRetryClick = {}) + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt deleted file mode 100644 index 70f3921dac..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/ListItem.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.features.markets.ui.entity - -sealed class ListItem \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt index a6f2c0c038..a561cf8517 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListItemUM.kt @@ -17,6 +17,7 @@ data class MarketsListItemUM( val trendPercentText: String, val trendType: PriceChangeType, val chardData: MarketChartRawData?, + val showUnder100kMarketCap: Boolean = false, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt index 602191058c..9167060698 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/MarketsListUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.markets.ui.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.markets.impl.R @@ -17,6 +18,9 @@ internal data class MarketsListUM( val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, ) { + val isInSearchMode + get() = searchBar.isActive + enum class TrendInterval(val text: TextReference) { H24(resourceReference(R.string.markets_selector_interval_24h_title)), D7(resourceReference(R.string.markets_selector_interval_7d_title)), @@ -37,10 +41,18 @@ sealed class ListUM { data class Content( val items: ImmutableList, + val showUnder100kTokens: Boolean, val loadMore: () -> Unit, val visibleIdsChanged: (List) -> Unit, + val onShowTokensUnder100kClicked: () -> Unit, + val triggerScrollReset: StateEvent, ) : ListUM() data object Loading : ListUM() + + data class LoadingError( + val onRetryClicked: () -> Unit, + ) : ListUM() + data object SearchNothingFound : ListUM() } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/SortByBottomSheetContentUM.kt similarity index 70% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt rename to features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/SortByBottomSheetContentUM.kt index 092b37423a..af420055f5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/model/SortByBottomSheetContentUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/entity/SortByBottomSheetContentUM.kt @@ -1,7 +1,6 @@ -package com.tangem.features.markets.model +package com.tangem.features.markets.ui.entity import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.features.markets.ui.entity.SortByTypeUM data class SortByBottomSheetContentUM( val selectedOption: SortByTypeUM, From 909ff3be7c49fb142b667f4a79b37576cf6f3e10 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jul 2024 21:48:53 +0300 Subject: [PATCH 181/223] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../transaction/error/FeeErrorsMapper.kt | 13 +++++++++++ .../domain/transaction/error/GetFeeError.kt | 6 ++++- .../transaction/usecase/EstimateFeeUseCase.kt | 3 ++- .../transaction/usecase/GetFeeUseCase.kt | 3 ++- .../errors/FeeErrorStateMapper.kt | 19 +++++++++++++++ .../presentation/state/SendNotification.kt | 8 +++++++ .../state/confirm/SendNotificationFactory.kt | 14 +++++++++-- .../state/fee/FeeNotificationFactory.kt | 23 +++++++++++++++---- .../state/fee/FeeSelectorState.kt | 5 +++- .../presentation/state/fee/FeeStateFactory.kt | 4 ++-- .../state/previewdata/FeeStatePreviewData.kt | 2 +- .../ui/fee/SendSpeedSelectorItem.kt | 2 +- .../impl/presentation/ui/send/FeeBlock.kt | 2 +- .../presentation/viewmodel/SendViewModel.kt | 17 ++++++++++---- gradle/dependencies.toml | 2 +- 17 files changed, 104 insertions(+), 21 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt create mode 100644 features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/errors/FeeErrorStateMapper.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index fba85fe955..aa18939439 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -564,6 +564,7 @@ ≈ %1$s (вкл. комиссию: %2$s) %s будет отправлено Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время + %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. Неверный адрес Транзакция отправлена Подготовьтесь к сканированию карты, которую вы хотите настроить. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 2c36f14fdf..954e7e59ca 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -627,6 +627,7 @@ ≈ %1$s (inc. fee: %2$s) %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while + %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron(TRX) in your account. Invalid address %1$s (%2$s) Transaction sent diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt new file mode 100644 index 0000000000..718ccb6f9b --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/FeeErrorsMapper.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.transaction.error + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.extensions.Result + +fun Result.Failure.mapToFeeError(): GetFeeError { + return when (this.error) { + is BlockchainSdkError.Tron.AccountActivationError -> { + GetFeeError.BlockchainErrors.TronActivationError + } + else -> GetFeeError.DataError(this.error) + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt index f2e3f7f60f..bc57770c0f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt @@ -2,5 +2,9 @@ package com.tangem.domain.transaction.error sealed class GetFeeError { data class DataError(val cause: Throwable?) : GetFeeError() - object UnknownError : GetFeeError() + data object UnknownError : GetFeeError() + + sealed class BlockchainErrors : GetFeeError() { + data object TronActivationError : BlockchainErrors() + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt index 2a32a62d43..134cb9f3f5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -12,6 +12,7 @@ import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.mapToFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow @@ -47,7 +48,7 @@ class EstimateFeeUseCase( val maybeFee = when (result) { is Result.Success -> result.data.right() - is Result.Failure -> GetFeeError.DataError(result.error).left() + is Result.Failure -> result.mapToFeeError().left() null -> GetFeeError.UnknownError.left() } emit(maybeFee) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt index 2f113a02a0..548a43ab6c 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetFeeUseCase.kt @@ -10,6 +10,7 @@ import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.mapToFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import java.math.BigDecimal @@ -47,7 +48,7 @@ class GetFeeUseCase( val maybeFee = when (result) { is Result.Success -> result.data - is Result.Failure -> raise(GetFeeError.DataError(result.error)) + is Result.Failure -> raise(result.mapToFeeError()) } maybeFee }, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/errors/FeeErrorStateMapper.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/errors/FeeErrorStateMapper.kt new file mode 100644 index 0000000000..ab218f7fe0 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/errors/FeeErrorStateMapper.kt @@ -0,0 +1,19 @@ +package com.tangem.features.send.impl.presentation.errors + +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState + +internal class FeeErrorStateMapper { + + fun getFeeError(loadFeeError: GetFeeError?, tokenName: String): FeeSelectorState.Error { + return when (loadFeeError) { + GetFeeError.BlockchainErrors.TronActivationError -> FeeSelectorState.Error.TronAccountActivationError( + tokenName, + ) + is GetFeeError.DataError, + GetFeeError.UnknownError, + null, + -> FeeSelectorState.Error.NetworkError + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index 65318c9c39..017c91f8d4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -157,6 +157,14 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) + data class TronAccountNotActivated(val tokenName: String) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference( + R.string.send_tron_account_activation_error, + wrappedList(tokenName), + ), + ) + data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( title = resourceReference(R.string.send_network_fee_warning_title), subtitle = resourceReference( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 98001756d9..a4e3bcbf9c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -126,8 +126,18 @@ internal class SendNotificationFactory( } private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { - if (feeSelectorState is FeeSelectorState.Error) { - add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) + when (feeSelectorState) { + is FeeSelectorState.Error.TronAccountActivationError -> add( + SendNotification.Warning.TronAccountNotActivated( + feeSelectorState.tokenName, + ), + ) + is FeeSelectorState.Error.NetworkError -> add( + SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload), + ) + else -> { + /* do nothing */ + } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt index 229568a930..aba37ba2ea 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeNotificationFactory.kt @@ -1,6 +1,9 @@ package com.tangem.features.send.impl.presentation.state.fee -import com.tangem.features.send.impl.presentation.state.* +import com.tangem.features.send.impl.presentation.state.SendNotification +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.features.send.impl.presentation.state.SendUiStateType +import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.persistentListOf @@ -21,13 +24,25 @@ internal class FeeNotificationFactory( val state = currentStateProvider() val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return@map persistentListOf() buildList { - addFeeUnreachableNotification(feeState.feeSelectorState) + addFeeUnreachableNotification( + feeState.feeSelectorState, + ) }.toImmutableList() } private fun MutableList.addFeeUnreachableNotification(feeSelectorState: FeeSelectorState) { - if (feeSelectorState is FeeSelectorState.Error) { - add(SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload)) + when (feeSelectorState) { + is FeeSelectorState.Error.TronAccountActivationError -> add( + SendNotification.Warning.TronAccountNotActivated( + feeSelectorState.tokenName, + ), + ) + is FeeSelectorState.Error.NetworkError -> add( + SendNotification.Warning.NetworkFeeUnreachable(clickIntents::feeReload), + ) + else -> { + /* do nothing */ + } } } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt index cd1fe29939..6ee78a7939 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeSelectorState.kt @@ -17,7 +17,10 @@ internal sealed class FeeSelectorState { data object Loading : FeeSelectorState() - data object Error : FeeSelectorState() + sealed class Error : FeeSelectorState() { + data object NetworkError : Error() + data class TronAccountActivationError(val tokenName: String) : Error() + } } enum class FeeType { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index e933b89477..3a30e424b9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -99,13 +99,13 @@ internal class FeeStateFactory( ) } - fun onFeeOnErrorState(): SendUiState { + fun onFeeOnErrorState(feeError: FeeSelectorState.Error): SendUiState { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState return state.copyWrapped( isEditState = isEditState, feeState = state.getFeeState(isEditState)?.copy( - feeSelectorState = FeeSelectorState.Error, + feeSelectorState = feeError, ), sendState = state.sendState?.copy( isPrimaryButtonEnabled = false, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt index c024b39ee0..7d00f7102d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/FeeStatePreviewData.kt @@ -102,6 +102,6 @@ internal object FeeStatePreviewData { ) val errorFeeState = feeState.copy( - feeSelectorState = FeeSelectorState.Error, + feeSelectorState = FeeSelectorState.Error.NetworkError, ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 08e3ff9853..f7aacce75a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -98,7 +98,7 @@ private fun FeeError(feeSelectorState: FeeSelectorState) { Row { SpacerWMax() AnimatedVisibility( - visible = feeSelectorState == FeeSelectorState.Error, + visible = feeSelectorState is FeeSelectorState.Error, label = "Fee Error State Change", modifier = Modifier.align(Alignment.CenterVertically), ) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index 11c4f4ebab..0339ddc783 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -105,7 +105,7 @@ private fun BoxScope.FeeError(feeSelectorState: FeeSelectorState) { label = "Fee Error State Change", modifier = Modifier.align(Alignment.CenterEnd), ) { - if (it == FeeSelectorState.Error) { + if (it is FeeSelectorState.Error) { Text( text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, color = TangemTheme.colors.text.primary1, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 446343bfeb..a57a241ebf 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -45,6 +45,7 @@ import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.analytics.SendScreenSource import com.tangem.features.send.impl.presentation.analytics.utils.SendScreenAnalyticSender import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.errors.FeeErrorStateMapper import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.amount.AmountStateFactory import com.tangem.features.send.impl.presentation.state.confirm.SendNotificationFactory @@ -120,6 +121,8 @@ internal class SendViewModel @Inject constructor( var stateRouter: StateRouter by Delegates.notNull() private set + private val feeErrorHandler = FeeErrorStateMapper() + private val stateFactory = SendStateFactory( clickIntents = this, stateRouterProvider = Provider { stateRouter }, @@ -711,20 +714,24 @@ internal class SendViewModel @Inject constructor( uiState.value = feeStateFactory.onFeeOnLoadedState(it) sendIdleTimer = SystemClock.elapsedRealtime() }, - ifLeft = { - onFeeLoadFailed(isShowStatus) + ifLeft = { loadFeeError -> + onFeeLoadFailed(isShowStatus, loadFeeError) }, ) if (result == null) { - onFeeLoadFailed(isShowStatus) + onFeeLoadFailed(isShowStatus, null) } updateNotifications() updateFeeNotifications() }.saveIn(feeJobHolder) } - private fun onFeeLoadFailed(isShowStatus: Boolean) { - if (isShowStatus) uiState.value = feeStateFactory.onFeeOnErrorState() + private fun onFeeLoadFailed(isShowStatus: Boolean, loadFeeError: GetFeeError?) { + if (isShowStatus) { + uiState.value = feeStateFactory.onFeeOnErrorState( + feeErrorHandler.getFeeError(loadFeeError, cryptoCurrency.name), + ) + } } private suspend fun checkIfSubtractAvailable() { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index cc82331baf..ce194116f9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-700" +tangemBlockchainSdk = "develop-702" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From cc327a86aec515b274d421ce6f9ff91b97700c18 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 16:03:28 +0500 Subject: [PATCH 182/223] Updated on 2026-08-14 --- .../main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e7c941c536..778d4f08d8 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 @@ -93,7 +93,7 @@ object BigDecimalFormatter { maximumFractionDigits = cryptoCurrency.decimals minimumFractionDigits = 2 isGroupingUsed = true - roundingMode = RoundingMode.DOWN + roundingMode = RoundingMode.HALF_UP } return formatter.format(cryptoAmount).let { From 2d8d125c3d20e9f18312dc69bd69dfda716ad546 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 16:04:00 +0500 Subject: [PATCH 183/223] Updated on 2026-08-14 --- features/disclaimer/impl/build.gradle.kts | 1 + .../impl/local/LocalTermOfServices.kt | 233 ++++++++++++++++++ .../disclaimer/impl/ui/DisclaimerScreen.kt | 91 +++---- .../impl/ui/DisclaimerWebViewClient.kt | 51 +--- 4 files changed, 288 insertions(+), 88 deletions(-) create mode 100644 features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index bb7ee06e94..1e72ef4a09 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.accompanist.permission) + implementation(deps.compose.accompanist.webView) implementation(deps.compose.material3) implementation(deps.compose.material) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt new file mode 100644 index 0000000000..1d60540d5e --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt @@ -0,0 +1,233 @@ +package com.tangem.features.disclaimer.impl.local + +internal val localTermsOfServices = """ + + + + + + + + Legal Disclaimer + + + + +

TANGEM WALLET AND TANGEM MOBILE APPLICATION

+

Terms of Service

+ +

PLEASE READ THESE TERMS OF SERVICE CAREFULLY. BY CLICKING TO ACCEPT, OR BY ACCESSING OR USING OUR SERVICES, YOU AGREE THAT YOU HAVE READ, UNDERSTOOD, AND ACCEPT ALL OF THE TERMS AND CONDITIONS CONTAINED HEREIN. BY PURCHASE OF TANGEM WALLET (CARD) OR BY USING TANGEM WALLET (CARD) OR BY USING TANGEM MOBILE APPLICATION, YOU DEMONSTRATE YOUR AGREEMENT TO THESE TERMS AND CONDITIONS CONTAINED HEREIN.

+ +

1. DEFINITIONS

+

“Cardholder” refers to an individual who owns a Tangem Card that is used to access the Tangem Wallet and Tangem Mobile Application.

+

“Blockchain Asset” refers to digital assets, including but not limited to cryptocurrencies, that can be managed through the Tangem Wallet.

+

“Blockchain Address” means a unique identifier that serves as a virtual location of the Blockchain Asset in the blockchain.

+

“Card Transaction” means transfer of Blockchain Asset from the Blockchain Address associated with the Public Key stored on the Tangem Wallet (Card).

+

“Official Mobile Application” means an application developed and distributed by Tangem, providing interoperability between Tangem Wallet (Card) and blockchain, and working on NFC-capable smartphones and tablets using Google Android and Apple iOS operation systems.

+

“Private Key” means a secret cryptographic key which provides full control over a Blockchain Asset.

+

“Tangem” means Tangem AG, a company incorporated under the laws of Switzerland (CHE-390.112.525), with a registered address at Baarerstrasse 10, CH-6300 Zug.

+

“Public Key” means a cryptographic key, which provides access to information about a Blockchain Asset, including but not limited to Blockchain Address

+

“Services” means the purchase and/or use of Tangem Wallet (Card) and the services or any other features, technologies or functionalities linked to the Tangem Wallet (Card) provided or operated by Tangem via the website or Official Mobile Applications.

+

“Tangem Wallet (Card) / Tangem Card / Card” means physical card which stores Private Key and Public Key and is used as a backup card.

+ +

2. DISCLAIMER

+

2.1. Bitcoin and other cryptocurrencies are virtual currencies, digital representations of value that are neither issued by a central bank of any state or public authority attached to a conventional currency, but may be used by any natural or legal persons as a means of exchange and can be transferred, stored or traded electronically.

+

2.2. Blockchain technologies and related services are subject to continuous regulatory changes and scrutiny around the world, including but not limited to anti-money laundering and financial regulations. You acknowledge that certain Services, including their availability, could be impacted by one or more regulatory requirements.

+

2.3. No advice. No part of the information herein should be considered to be business, legal, financial or tax advice regarding the Products or Services. You should consult your own legal, financial, tax or other professional advisor regarding the matter. By using the Services, you represent that Tangem is responsible neither for obtaining the information about tax or similar obligations arising in relation to usage of the Services nor for fulfillment of such tax (or similar) obligations.

+ +

3. GENERAL PROVISIONS

+

3.1. These Terms of Service (the “Terms”) govern the use of Tangem Wallet and/or Official Mobile Application provided by Tangem (referred to as "Tangem", "we" or "us" in this document) and the related services or any other features, technologies or functionalities linked to Tangem Wallet (Card). Tangem is a company incorporated under the laws of Switzerland (CHE-390.112.525), with a registered address at Baarerstrasse 10, CH-6300 Zug.

+

3.2. Tangem Cards may be used for storage of Private Key and Public Key to Cardholder’s Blockchain Assets and authentication of Card Transactions with the purpose of “person-to-person” transfer of Blockchain Assets to another blockchain address.

+

3.3. Official Mobile Application is intended for usage only with Tangem Wallet (Card), providing interoperability between the cards and blockchain via NFC interface. Official Mobile Application DOES NOT:

+

3.3.1. Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain wallets holding Blockchain assets.

+

3.3.2. Generate, store, transmit, or have access to secret keys, passwords, passphrases, recovery phrases that can be used to restore or to copy private (secret) keys to blockchain wallets holding Blockchain assets.

+

3.3.3 Provide exchange, trading, investment services on behalf of Tangem.

+ +

4. RIGHTS AND OBLIGATIONS

+

4.1. Cardholder agrees that these Terms are binding.

+

4.2. Cardholder shall be the only person having physical access to Tangem Wallet.

+

4.3. Cardholder acknowledges and agrees that Tangem does not provide backup or recovery of Private Key and Public Key stored by Tangem Wallet (Card).

+

4.4. Tangem does not keep records of Cardholder’s personal data, the amount of Blockchain Asset stored on the Card, the Private Key, or personalized history of Card usage.

+ +

5. COSTS

+

5.1. Costs, fees and commission (the “Costs”) may be charged in connection with the use of Card. These Costs are disclosed in Official Mobile Applications to Cardholder.

+

5.2. Amendments to Costs due to changing expenses or market conditions may be made at any time via adjustments to the fee schedules. Such amendments shall be communicated to Cardholder in an appropriate manner. Upon notification and in the event of the objection, Cardholder may cancel the Card with immediate effect.

+ +

6. CARDHOLDER’S DUTIES OF CARE

+

6.1. In particular, Cardholder shall exercise the following duties of care:

+

6.1.1. Upon receiving the Card, Cardholder should download Official Mobile Applications in order to create the backup and if applicable determine the amount of Blockchain Asset stored through Official Mobile Applications.

+

6.1.2. Cardholder shall keep the means of access and Tangem Wallet (Card) with care and all Cards separate from each other.

+

6.1.3. Cardholder must always know where Tangem Wallet (Card) is and regularly ensure that it is still in his/her possession. He/she shall avoid even temporary possession of Tangem Wallet (Card) by any other person.

+

6.1.4. Cardholder shall treat Tangem Wallet in the same manner as physical money (cash) and keep it safe. If any of the Card is lost, stolen or destroyed, control over the corresponding Blockchain Asset may be permanently lost.

+

6.1.5. Before using the Card with Official Mobile Applications, Cardholder shall locate Official Mobile Applications in Google Play or Apple app store and install it as instructed in the Card box.

+

6.1.6. Card shall be used only with Official Mobile Applications and as instructed in the Card box.

+

6.1.7. Official Mobile Application shall be the only source of information about the Blockchain Address of the Blockchain Asset and corresponding Public Key stored on Tangem Wallet (Card).

+

6.1.8. Cardholder shall only use Near-Field Devices (the “NFC”) devices that are capable of running Official Mobile Applications. He/she shall avoid leaving Tangem Wallet (Card) in the proximity of the NFC devices of other persons.

+

6.1.9. Tangem Wallet (Card) shall be used only for physically tapping and holding near Cardholder’s NFC device when Official Mobile Application requests it.

+

6.1.10. Cardholder shall keep Tangem Wallet (Card) with care and protect Tangem Wallet (Card) from mechanical damage, high temperatures, strong electromagnetic fields, and other harmful factors.

+

6.2. No retrieval of Private Keys. Tangem operates non-custodial services, which means that we do not store, nor do we have access to your Blockchain Assets nor your Private Keys. Tangem does not have access to or store passwords, 24-word Recovery Phrase, Private Keys, passphrases, transaction history, PIN, or other credentials associated with your use of the Services. You are solely responsible for remembering, storing, and keeping your credentials in a secure location, away from prying eyes. Any third party with knowledge of one or more of your 24-word Recovery Phrase can gain control of the Private Keys associated with your Tangem Wallet (Card) or of the 24-word Recovery Phrase, and therefore steal your Blockchain Assets, without any possibility for you or Tangem to retrieve them.

+ +

7. RIGHTS AND RESPONSIBILITIES OF CARDHOLDER

+

7.1. Cardholder is liable for all liabilities arising from the use of Tangem Wallet (Card) and/or Tangem Official Mobile Application. Any disputes in relation to discrepancies and complaints about goods or services and any resulting claims must be settled directly by Cardholder with the respective Reseller.

+

7.2. As a matter of principle, Cardholder is liable for any risks resulting from the misuse of Tangem Wallet (Card) and/or Official Mobile Application. In any case, Cardholder is solely liable for all transactions authorized using a means of access.

+

7.3. Any loss or damage resulting from the forwarding of Tangem Wallet (Card) and/or means of access shall be borne by Cardholder.

+

7.4. Loss or damage incurred by Cardholder in connection with the possession or use of Tangem Wallet (Card) and/or Official Mobile Application shall be borne solely by Cardholder. Tangem assumes no liability if Tangem Wallet (Card) and/or Official Mobile Application cannot be used due to a technical defect or because it has been canceled, blocked or the spending limit has been adjusted.

+

7.5. Cardholder is only permitted to use Tangem Wallet (Card) and Official Mobile Application for his personal, non-commercial use. Cardholder is not allowed to resell Tangem Wallet (Card).

+

7.6. Cardholder is solely responsible to determinate what, if any, taxes apply to Card Transactions. Tangem or contributors to Official Mobile Applications are NOT responsible for determining the taxes that apply to Card Transactions.

+

7.7. Before Cardholder engages in transactions using an electronic system, Cardholder should carefully review the rules and regulations of the exchanges offering the system and/or listing the instruments Cardholder intends to trade. Online trading has inherent risk due to system response and access times that may vary due to market conditions, system performance, and other factors. Cardholder should understand, fully accept and take on these and additional risks before trading.

+

7.8. There is considerable exposure to risk in the Blockchain Asset exchange transaction. Any transaction involving the Blockchain Asset involves risks including, but not limited to, the potential for changing economic conditions that may substantially affect the price or liquidity of the Blockchain Asset. Investments in the Blockchain Asset exchange speculation may also be susceptible to sharp rises and falls as the relevant market values fluctuate. It is for this reason that when speculating in such markets it is advisable to use only risk capital.

+

7.9. Before initiating any transactions through third-party resources via widgets or links within the application, Cardholder is expressly advised to meticulously review and comprehend the terms, rules, and regulations governing such resources. It is imperative for Cardholder to be cognizant of the inherent risks associated with online trading, including variations in system response times, access delays influenced by market conditions, system performance, and other pertinent factors.

+

7.10. Cardholder unequivocally assumes sole responsibility for all actions undertaken, encompassing but not limited to swap transactions, on-ramp, and off-ramp activities, when transitioning to third-party resources through widgets or links within the application. This responsibility extends to compliance with the terms and conditions of the relevant third-party resources and adherence to applicable laws and regulations.

+

7.11. Cardholder acknowledges and accepts that the use of third-party resources involves inherent risks, and Tangem shall bear no liability for the consequences arising from Cardholder's independent actions on these external platforms.

+

7.12. Cardholder is responsible for implementing adequate security measures and precautions when interacting with third-party resources to safeguard personal information, financial assets, and to mitigate potential risks associated with such engagements.

+

7.13. Cardholder agrees to indemnify and hold Tangem, its affiliates, and service providers harmless from any claims, losses, or damages incurred as a result of their actions on third-party platforms, as outlined in the Terms of Services.

+

7.14. Tangem explicitly disclaims any affiliation, endorsement, or responsibility for the content, policies, or transactions on third-party resources, and Cardholder interactions with such resources are entirely at their own risk.

+ +

8. THIRD-PARTY SERVICES

+

8.1. We may incorporate, reference and/or provide access to Third Party Services. For instance, buy, sell and crypto to crypto exchange (“swap”) services are Third Party Services. You agree that your use of Third-Party Services is subject to separate terms and conditions between you and the third-party identified in Tangem.

+

8.2. Tangem is not responsible for the content, accuracy, security, availability, any performance, or failure to perform of the Third-Party Services or any issue in relation with the use of Third-Party Services. Tangem does not provide any guarantees that access to Third-Party Services will not be interrupted or that there will be no delays, failures, errors, omissions, corruption or loss of transmitted information, data or funds, and Tangem shall not be liable for any such Third-Party Services. You agree to use the Third-Party Services at your own risk. It is your responsibility to review the third party’s terms and policies before using a Third-Party Service. Third-Party Services may not be available in all languages and may not be appropriate or available for use in any particular location. To the extent you choose to use such Third-Party Services, you are solely responsible for compliance with any applicable laws in relation to such use. In addition, Tangem reserves the right to block access to these Third-Party Services through Tangem Live in particular, but not exclusively, in the event of non-compliance with the applicable regulations by the Third-Party partner. We retain the exclusive right to suspend, remove, or cancel the availability of any such Third-Party Service for any reason and without prior notice.

+ +

9. RESPONSIBILITIES AND LIABILITIES OF TANGEM

+

9.1. Tangem does not warrant or make any representations regarding the use, the inability to use or operate, or the results of the use or operation of Tangem Wallet (Card) and/or Official Mobile Application.

+

9.2. Tangem does not keep any records of Cardholder information, the amount of Blockchain Asset stored on Tangem Wallet (Card), the Private Key, or personalized history of cards usage.

+

9.3. Tangem does not provide any backup or recovery of Private Key and Public Key stored on Tangem Wallet (Card).

+

9.4. Tangem shall not be held liable for any failure to be able to use Tangem Wallet (Card) and/or Official Mobile Application, for any reason whatsoever, nor will Tangem be held liable for the loss of the Blockchain Asset resulting from a malfunction or inoperability of the blockchain network hosting Blockchain Asset, as well as the inaccessibility of its public servers and services.

+

9.5. Tangem does not guarantee that the operation of Tangem Wallet (Card) and/or Official Mobile Application will be secure, accurate, complete, uninterrupted, without error or free of viruses, worms, other harmful components or other program limitations. Tangem may, at its sole discretion and without obligation to do so, correct, modify, amend, enhance, improve and make any other changes to Tangem Wallet and/or Official Mobile Application, change, update or suspend the Services, temporarily or indefinitely, so as to carry out works including, but not limited to: firmware and software updates, maintenance operations, amendments to the servers, bug fixes, etc. We will make reasonable efforts to give you prior notice of any significant disruption of the Services. Tangem does not guarantee the correct functioning of the Services in the event of the installation or use of programs or applications that do not conform to Service specifications and technical standards.

+

9.6. Tangem shall not be held liable for the loss of profits, income, value or any indirect, extraordinary, consequential, exemplary or punitive damages.

+

9.7. Tangem shall not be held liable for loss or breakdown of Tangem Wallet (Card).

+

9.8. Tangem shall not be held liable for any loss of Blockchain Asset in the event of loss or total breakdown of Tangem Wallet (Card).

+ +

10. GUARANTEES

+

10.1. Under the condition that Cardholder exercises the duties of care as stated in Clause 5, Tangem guarantees that Tangem Wallet (Card) will function properly and without restriction for a period of 2 (two) years. In the event of a breakdown of Tangem Wallet (Card) without it being the fault of Cardholder due to reasons mentioned in Clause 5 of these Terms, Tangem will replace Tangem Wallet with a new one. Cardholder shall inform Tangem on the event of a breakdown by sending an e-mail to support@tangem.com. If failed to resolve with support team of Tangem, Cardholder shall wait for instructions on safe shipping of Tangem Wallet (Card).

+

10.2. Tangem guarantees that Tangem Wallet (Card) prevents duplication of the Private Key and that Cardholder has exclusive control over the Blockchain Asset unless the contrary is imposed by specific blockchain network rules, e.g. two or more private keys can be used to control the same Blockchain Asset.

+ +

11. LIMITATION OF LIABILITY

+

11.1. Tangem Wallet (Card), including without limitation any content, data and information related thereto, is provided on an “as is” basis and “as available” basis, without any warranties of any kind, express or implied warranties of use, merchantability or suitability for a certain purpose or use, including without limitation, the quality of products and services provided by users, third-party services, and/or exchanges (except for the guarantees set forth in Section 9).

+

11.2. Official Mobile Application is provided on an “as is” basis and “as available” basis without any warranties of any kind regarding Official Mobile Application and/or any content, data, materials and/or services provided on Official Mobile Application.

+

11.3. Tangem and its affiliates, including any of their officers, directors, shareholders, employees, sub-contractors, agents, parent companies, subsidiaries and other affiliates (collectively, the “Tangem Affiliates”), jointly and severally, disclaim and make no representations or warranties as to the usability, accuracy, quality, availability, reliability, suitability, completeness, truthfulness, usefulness or effectiveness of any content, data, results or other information obtained or generated by Tangem and/or any user related to you or any other user of Tangem Wallet (Card), and Official Mobile Applications.

+

11.4. In no event shall Tangem and/or any of Tangem Affiliates be liable for any damages whatsoever, including direct, indirect, extraordinary, incidental or consequential damages of any kind, but not limited to, resulting from or arising out of the use of Tangem Wallet (Card) and/or Official Mobile Applications or inability to use Tangem Wallet (Card) and/or Official Mobile Applications, failure of Tangem Wallet (Card) and/or Official Mobile Applications to perform as represented or expected, loss of goodwill or profits, or loss of data arising out of or in any way connected with the use of Tangem Wallet (Card) and/or Official Mobile Applications. In no event shall Tangem and/or any of Tangem Affiliates be liable for the performance or failure of Tangem Wallet (Card) and/ or Official Mobile Applications to perform under these Terms of Use and any other act or omission by Tangem by any cause whatsoever including without limitation damages arising from the conduct of any users, third party services and/or exchanges. In no way Tangem or contributors to Official Mobile Application are responsible for the actions, decisions, or other behavior taken or not taken by Cardholder in reliance upon Tangem Official Mobile Application.

+

11.5. You hereby acknowledge and agree that these limitations of liability are agreed allocations of risk constituting in part the consideration for using Tangem Wallet (Card) and Official Mobile Applications and such limitations will apply notwithstanding the failure of essential purpose of any limited remedy, and even if Tangem and/or any Tangem Affiliates has been advised of the possibility of such liabilities and/or damages.

+

11.6. Tangem will not be responsible for any losses, damages or claims arising from events falling within the scope of the following five categories:

+

11.6.1. Mistakes made by Cardholder, e.g., forgotten passwords, payments sent to wrong addresses, and accidental deletion of blockchain wallets on Tangem Wallet (Card).

+

11.6.2. Problems of Official Mobile Application and/or any blockchain- or cryptocurrency- related software or service, e.g., corrupted files, incorrectly constructed transactions, unsafe cryptographic libraries, malware.

+

11.6.3. Technical failures in the hardware of Cardholder, including cards, of any blockchain- or cryptocurrency- related software or service, e.g., data loss due to a faulty or damaged storage device.

+

11.6.4. Security problems experienced by Cardholder, e.g., unauthorized access to Cardholders' wallets and/or accounts.

+

11.6.5. Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy of service providers, information security attacks on service providers, and fraud conducted by third parties.

+ +

12. GOVERNING LAW AND DISPUTE RESOLUTION

+

12.1. Unless otherwise required by a mandatory law of a member state of the European Union or any other jurisdiction these Terms of Service and any separate agreements whereby we provide you Services shall be governed by the laws of Switzerland without regard to its conflict of laws principles.

+

12.2. You can submit a claim in written form regarding the operation of the Services to us via email at store@tangem.com. You may also reach us in writing at the following address: Tangem AG, Baarerstrasse 10, Zug, CH-6300 Switzerland. In case of failure to resolve disputes and disagreements by way of negotiations the settlement shall be in accordance with claim procedure. Claims shall be reviewed within 30 calendar days.

+

12.3. Subject to compulsory legal provisions, any use of the Services and all legal disputes arising out of or in connection therewith shall be submitted to the exclusive jurisdiction of the courts of the Canton of Zug.

+ +

13. COMMUNICATION

+

13.1. In the event when under the Terms Tangem provides the User with any information that relates to the Services provided hereunder, this information may be given to the Client through the Website without sending said information directly to the User’s address and / or using other secure means.

+

13.2. Tangem shall respond to requests from the User promptly and within 7 calendar days following the date of receipt of the request. The response time may in some cases may exceed 7 calendar days.

+ +

14. MISCELLANEOUS

+

14.1. Entire agreement. These Terms and any policies or operating rules posted by us on the Website or in respect to the Services constitutes the entire agreement and understanding between you and us and govern your use of the Services, superseding any prior or contemporaneous agreements, communications and proposals, whether oral or written, between you and us (including, but not limited to, any prior versions of the Terms).

+

14.2. Severability. In the event that any provision of these Terms is determined to be unlawful, void or unenforceable, such provision will nonetheless be enforceable to the fullest extent permitted by applicable law, and the unenforceable portion will be deemed to be severed from these Terms of Service, such determination will not affect the validity and enforceability of any other remaining provisions.

+

14.3. Assignment. You may not assign your rights or obligations under these Terms in whole or in part to any third party. You acknowledge and agree that Tangem may assign its rights and obligations under these Terms, including rights and obligations concerning insurance, and, in such context, share or transfer information provided by you while using the Services to a third party.

+

14.4. No waiver. The failure of us to exercise or enforce any right or provision of these Terms will not constitute a waiver of such right or provision.

+

14.5. Any ambiguities in the interpretation of these Terms will not be construed against the drafting party.

+

14.6. Errors, Inaccuracies, And Omissions. Occasionally there may be information in the Services that contains typographical errors, inaccuracies or omissions that may relate to product descriptions, pricing, promotions, offers, product shipping charges, transit times and availability. We reserve the right to correct any errors, inaccuracies or omissions, and to change or update information or cancel orders if any information in the Services or on any related website is inaccurate at any time without prior notice (including after you have submitted your order).

+

14.7. Terms concerning Recovery Phrase apply to Tangem Wallet (Card) supporting this feature.

+

14.8. We undertake no obligation to update, amend or clarify information in the Services, including without limitation, pricing information, except as required by law. No specified update or refresh date applied in the Services or on any related website, should be taken to indicate that all information in the Services has been modified or updated.

+

14.9. These Terms may be drawn up in different languages. In case of any inconsistency the English version of the Terms shall prevail.

+ +

Last amended on: March 1st, 2024

+ + + +""".trimIndent() \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index fddb1a255b..b3740659cc 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -2,26 +2,28 @@ package com.tangem.features.disclaimer.impl.ui import android.annotation.SuppressLint import android.content.res.Configuration -import android.view.View -import android.view.ViewGroup -import android.webkit.WebView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue 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.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.viewinterop.AndroidView import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState +import com.google.accompanist.web.rememberWebViewStateWithHTMLData import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton @@ -35,6 +37,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.disclaimer.impl.R import com.tangem.features.disclaimer.impl.entity.DisclaimerUM import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer +import com.tangem.features.disclaimer.impl.local.localTermsOfServices import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull @Composable @@ -63,6 +66,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { iconRes = R.drawable.ic_back_24, onIconClicked = state.popBack, ).takeIf { state.isTosAccepted }, + titleAlignment = Alignment.CenterHorizontally, textColor = textColor, iconTint = iconColor, ) @@ -81,52 +85,51 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { @SuppressLint("SetJavaScriptEnabled") @Composable private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { - val progressState = remember { mutableStateOf(ProgressState.Loading) } - val webClient = remember { DisclaimerWebViewClient(progressState) } - val transparent = Color.Transparent val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 - Box( - modifier = Modifier, - ) { - AndroidView( - factory = { - WebView(it).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - setBackgroundColor(transparent.toArgb()) - settings.allowFileAccess = false - // to inject css style to display only in dark theme - settings.javaScriptEnabled = !isTosAccepted - overScrollMode = View.OVER_SCROLL_NEVER - webViewClient = webClient - clearHistory() - clearFormData() - clearCache(true) + val webViewStateUrl = rememberWebViewState(url) + val webViewStateData = + rememberWebViewStateWithHTMLData(data = localTermsOfServices, mimeType = "text/html", encoding = "UTF-8") - loadUrl(url) - } + val webViewState by remember { + derivedStateOf { + if (webViewStateUrl.errorsForCurrentRequest.isNotEmpty()) { + webViewStateData + } else { + webViewStateUrl + } + } + } + + Box { + WebView( + state = webViewState, + captureBackPresses = false, + onCreated = { + it.settings.javaScriptEnabled = !isTosAccepted + it.setBackgroundColor(backgroundColor.toArgb()) }, + client = remember { DisclaimerWebViewClient() }, ) - when (progressState.value) { - ProgressState.Loading -> { - Box( + AnimatedVisibility( + visible = webViewState.isLoading, + label = "Loading state change animation", + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor), + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, modifier = Modifier - .fillMaxSize() - .background(backgroundColor), - ) { - CircularProgressIndicator( - color = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.Center) - .padding(TangemTheme.dimens.spacing8), - ) - } + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) } - else -> Unit } } } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt index 5ad8aedb8f..c3046d0855 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -1,8 +1,8 @@ package com.tangem.features.disclaimer.impl.ui import android.graphics.Bitmap -import android.webkit.* -import androidx.compose.runtime.MutableState +import android.webkit.WebView +import com.google.accompanist.web.AccompanistWebViewClient internal enum class ProgressState { Loading, @@ -26,57 +26,20 @@ private fun WebView.injectCSS() { evaluateJavascript(code, null) } -internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { - - private var loadingUrl: String? = null - private var loadedUrl: String? = null - - fun reset() { - loadingUrl = null - loadedUrl = null - progressState.value = ProgressState.Loading - } +internal class DisclaimerWebViewClient : AccompanistWebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { view?.injectCSS() super.onPageStarted(view, url, favicon) + } - if (loadingUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Loading - loadingUrl = url + override fun onPageCommitVisible(view: WebView?, url: String?) { + view?.injectCSS() + super.onPageCommitVisible(view, url) } override fun onPageFinished(view: WebView?, url: String?) { view?.injectCSS() super.onPageFinished(view, url) - - if (loadedUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Done - loadedUrl = url - } - - override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) { - view?.injectCSS() - super.onReceivedError(view, resourceRequest, error) - error?.let { progressState.value = ProgressState.Error } - } - - override fun onReceivedHttpError( - view: WebView?, - resourceRequest: WebResourceRequest?, - errorResponse: WebResourceResponse?, - ) { - view?.injectCSS() - super.onReceivedHttpError(view, resourceRequest, errorResponse) - - if (resourceRequest != null && errorResponse != null) { - val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl - val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE - val isNotDone = progressState.value != ProgressState.Done - if (isDifferentUrl || isSuccessCode || isNotDone) return - progressState.value = ProgressState.Error - } - } - - companion object { - private const val RESPONSE_USER_ERROR_STATUS_CODE = 400 } } \ No newline at end of file From 67016b47558972caa4410a1123f8d58f83133675 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 22 Jul 2024 18:22:40 +0300 Subject: [PATCH 184/223] Updated on 2026-08-14 --- .../domain/GetMultiWalletWarningsFactory.kt | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index e3bef9a24d..25ef6de005 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -39,8 +39,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val backupValidator: BackupValidator, ) { - private var readyForRateAppNotification = false - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver @@ -52,8 +50,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flow4 = shouldShowSwapPromoWalletUseCase(), flow5 = promoFlow, ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner -> - - readyForRateAppNotification = true buildList { addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) @@ -63,7 +59,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents) - addRateTheAppNotification(isReadyToShowRating, clickIntents) + val hasCriticalOrWarning = any { notification -> + notification is WalletNotification.Critical || notification is WalletNotification.Warning + } + + if (!hasCriticalOrWarning) { + addRateTheAppNotification(isReadyToShowRating, clickIntents) + } }.toImmutableList() } } @@ -200,17 +202,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( onDislikeClick = clickIntents::onDislikeAppClick, onCloseClick = clickIntents::onCloseRateAppWarningClick, ), - condition = isReadyToShowRating && readyForRateAppNotification, + condition = isReadyToShowRating, ) } private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { - if (condition) { - add(element = element) - if (element is WalletNotification.Critical || element is WalletNotification.Warning) { - readyForRateAppNotification = false - } - } + if (condition) add(element = element) } private companion object { From 57eb2359a71dfb799fb256b0f5e5804d4f61351f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 14:43:09 +0300 Subject: [PATCH 185/223] Updated on 2026-08-14 --- .../api/stakekit/models/request/Address.kt | 1 + .../list/RoundedListWithDividers.kt | 123 ++++++++++++++++++ .../src/main/res/drawable/ic_staking_24.xml | 5 + .../data/staking/DefaultStakingRepository.kt | 4 +- .../staking/GetStakingEntryInfoUseCase.kt | 13 +- .../staking/repositories/StakingRepository.kt | 2 +- .../repository/MockStakingRepository.kt | 11 +- .../impl/presentation/state/StakingUiState.kt | 14 +- .../previewdata/InitialStakingStatePreview.kt | 52 +++++++- .../SetConfirmationStateAssentTransformer.kt | 1 - .../SetConfirmationStateLoadingTransformer.kt | 1 - .../SetInitialDataStateTransformer.kt | 94 ++++++++++--- .../ui/StakingInitialInfoContent.kt | 82 +----------- .../feature/swap/domain/SwapInteractorImpl.kt | 1 - .../feature/swap/viewmodels/SwapViewModel.kt | 1 - .../components/TokenDetailsActionButton.kt | 2 +- .../viewmodels/TokenDetailsViewModel.kt | 5 +- .../wallet/state/model/WalletManageButton.kt | 2 +- .../MultiWalletCurrencyActionsConverter.kt | 2 +- 19 files changed, 291 insertions(+), 125 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt create mode 100644 core/ui/src/main/res/drawable/ic_staking_24.xml diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt index 03921fa39c..4e93f0353a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt @@ -21,6 +21,7 @@ data class Address( val binanceBeaconAddress: String? = null, // solana-specific + @Deprecated("Legacy in StakeKit, isn't used in Solana") @Json(name = "stakeAccounts") val stakeAccounts: List? = null, @Json(name = "lidoStakeAccounts") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt new file mode 100644 index 0000000000..8161f2ff69 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/list/RoundedListWithDividers.kt @@ -0,0 +1,123 @@ +package com.tangem.core.ui.components.list + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun RoundedListWithDividers(rows: List, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = rows, + key = { _, item -> item.id }, + ) { index, row -> + InitialInfoContentRow( + startText = row.startText.resolveReference(), + endText = row.endText.resolveReference(), + cornersToRound = getCornersToRound(index, rows.size), + iconClick = row.iconClick, + ) + if (index < rows.lastIndex) { + RoundedListDivider() + } + } + } +} + +@Composable +private fun InitialInfoContentRow( + startText: String, + endText: String, + cornersToRound: CornersToRound, + iconClick: (() -> Unit)? = null, +) { + RoundableCornersRow( + startText = startText, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.body2, + endText = endText, + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = cornersToRound, + iconResId = R.drawable.ic_information_24, + iconClick = iconClick, + ) +} + +@Composable +fun RoundedListDivider() { + Row( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size0_5), + ) { + Box( + modifier = Modifier + .width(TangemTheme.dimens.size16) + .height(TangemTheme.dimens.size0_5) + .background(TangemTheme.colors.background.primary), + ) + Box( + modifier = Modifier + .weight(1f) + .height(TangemTheme.dimens.size0_5) + .background(TangemTheme.colors.background.tertiary), + ) + } +} + +private fun getCornersToRound(currentIndex: Int, listSize: Int): CornersToRound { + return when (currentIndex) { + 0 -> CornersToRound.TOP_2 + listSize - 1 -> CornersToRound.BOTTOM_2 + else -> CornersToRound.ZERO + } +} + +data class RoundedListWithDividersItemData( + val id: Int, + val startText: TextReference, + val endText: TextReference, + val iconClick: (() -> Unit)? = null, +) + +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_StakingInfoBottomSheet() { + TangemThemePreview { + RoundedListWithDividers( + rows = persistentListOf( + RoundedListWithDividersItemData( + id = 1, + startText = TextReference.Str("Key 1"), + endText = TextReference.Str("Value 1"), + ), + RoundedListWithDividersItemData( + id = 2, + startText = TextReference.Str("Key 2"), + endText = TextReference.Str("Value 2"), + ), + RoundedListWithDividersItemData( + id = 3, + startText = TextReference.Str("Key 3"), + endText = TextReference.Str("Value 3"), + iconClick = {}, + ), + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_staking_24.xml b/core/ui/src/main/res/drawable/ic_staking_24.xml new file mode 100644 index 0000000000..9a74909897 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_staking_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index ddb05b9851..bc015d17b5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -124,9 +124,9 @@ internal class DefaultStakingRepository( } } - override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { + override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo { return withContext(dispatchers.io) { - val yield = stakeKitApi.getSingleYield(integrationId).getOrThrow() + val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( interestRate = yield.apy, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt index 6192e9d92a..ffa3cbb0a3 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingEntryInfoUseCase.kt @@ -5,6 +5,7 @@ import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency /** * Use case for getting entry info about staking on token screen. @@ -14,9 +15,17 @@ class GetStakingEntryInfoUseCase( private val stakingErrorResolver: StakingErrorResolver, ) { - suspend operator fun invoke(integrationId: String): Either { + suspend operator fun invoke( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): Either { return Either - .catch { stakingRepository.getEntryInfo(integrationId) } + .catch { + stakingRepository.getEntryInfo( + cryptoCurrencyId = cryptoCurrencyId, + symbol = symbol, + ) + } .mapLeft { stakingErrorResolver.resolve(it) } } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 356dc7aac9..a9ff51285e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -21,7 +21,7 @@ interface StakingRepository { suspend fun fetchEnabledYields(refresh: Boolean) - suspend fun getEntryInfo(integrationId: String): StakingEntryInfo + suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 6116e0afda..ad8a88a0fd 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -25,11 +25,12 @@ class MockStakingRepository : StakingRepository { /* no-op */ } - override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo = StakingEntryInfo( - interestRate = 1.toBigDecimal(), - periodInDays = 2, - tokenSymbol = "SOL", - ) + override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo = + StakingEntryInfo( + interestRate = 1.toBigDecimal(), + periodInDays = 2, + tokenSymbol = "SOL", + ) override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield( id = "1", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 085a7fcd94..2ad72821a5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.staking.model.stakekit.PendingAction @@ -47,6 +48,14 @@ internal sealed class StakingStates { sealed class InitialInfoState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, + val infoItems: ImmutableList, + val aprRange: TextReference, + val onInfoClick: (InfoType) -> Unit, + val yieldBalance: InnerYieldBalanceState, + val isStakeMoreAvailable: Boolean, + ) : InitialInfoState() + + data class InitialInfoItems( val available: String, val onStake: String, val aprRange: TextReference, @@ -55,10 +64,7 @@ internal sealed class StakingStates { val rewardClaiming: String, val warmupPeriod: String, val rewardSchedule: String, - val onInfoClick: (InfoType) -> Unit, - val yieldBalance: InnerYieldBalanceState, - val isStakeMoreAvailable: Boolean, - ) : InitialInfoState() + ) data class Empty( override val isPrimaryButtonEnabled: Boolean = false, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index ac561e7608..2255ba2015 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -1,21 +1,59 @@ package com.tangem.features.staking.impl.presentation.state.previewdata +import com.tangem.core.ui.components.list.RoundedListWithDividersItemData +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { val defaultState = StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = true, - available = "15 SOL", - onStake = "0 SOL", aprRange = stringReference("2.54-5.12%"), - unbondingPeriod = "3d", - minimumRequirement = "12 SOL", - rewardClaiming = "Auto", - warmupPeriod = "Days", - rewardSchedule = "Block", + infoItems = persistentListOf( + RoundedListWithDividersItemData( + id = R.string.staking_details_available, + startText = TextReference.Res(R.string.staking_details_available), + endText = TextReference.Str("15 SOL"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_apy, + startText = TextReference.Res(R.string.staking_details_apy), + endText = TextReference.Str("2.54-5.12%"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_on_stake, + startText = TextReference.Res(R.string.staking_details_on_stake), + endText = TextReference.Str("0 SOL"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_unbonding_period, + startText = TextReference.Res(R.string.staking_details_unbonding_period), + endText = TextReference.Str("3d"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_minimum_requirement, + startText = TextReference.Res(R.string.staking_details_minimum_requirement), + endText = TextReference.Str("12 SOL"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_reward_claiming, + startText = TextReference.Res(R.string.staking_details_reward_claiming), + endText = TextReference.Str("Auto"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_warmup_period, + startText = TextReference.Res(R.string.staking_details_warmup_period), + endText = TextReference.Str("Days"), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_reward_schedule, + startText = TextReference.Res(R.string.staking_details_reward_schedule), + endText = TextReference.Str("Block"), + ), + ), onInfoClick = {}, yieldBalance = InnerYieldBalanceState.Empty, isStakeMoreAvailable = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 90e5434e09..f2dc22698e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -14,7 +14,6 @@ import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList -@Suppress("UnusedPrivateMember") internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index e6ba83a11d..b9bd36aff7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -5,7 +5,6 @@ import com.tangem.features.staking.impl.presentation.state.* import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf -@Suppress("UnusedPrivateMember") internal class SetConfirmationStateLoadingTransformer( private val yield: Yield, ) : Transformer { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index c361dcf147..7e45dc8557 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.common.extensions.remove import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -26,6 +27,8 @@ import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickInten import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal internal class SetInitialDataStateTransformer( @@ -70,33 +73,87 @@ internal class SetInitialDataStateTransformer( } private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance - return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = true, - available = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoCurrencyStatus.value.amount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), - onStake = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), aprRange = getAprRange(), - unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(), - minimumRequirement = yield.metadata.minimumStake.toString(), - rewardClaiming = yield.metadata.rewardClaiming, - warmupPeriod = yield.metadata.warmupPeriod.days.toString(), - rewardSchedule = yield.metadata.rewardSchedule, + infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalancesConverter.convert(Unit), isStakeMoreAvailable = isStakeMoreAvailable, ) } + private fun getInfoItems(): PersistentList { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + + return persistentListOf( + RoundedListWithDividersItemData( + id = R.string.staking_details_available, + startText = TextReference.Res(R.string.staking_details_available), + endText = TextReference.Str( + value = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoCurrencyStatus.value.amount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + ), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_apy, + startText = TextReference.Res(R.string.staking_details_apy), + endText = getAprRange(), + iconClick = { clickIntents.onInfoClick(InfoType.APY) }, + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_on_stake, + startText = TextReference.Res(R.string.staking_details_on_stake), + endText = TextReference.Str( + value = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + ), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_unbonding_period, + startText = TextReference.Res(R.string.staking_details_unbonding_period), + endText = TextReference.Str(yield.metadata.cooldownPeriod.days.toString()), + iconClick = { clickIntents.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_minimum_requirement, + startText = TextReference.Res(R.string.staking_details_minimum_requirement), + endText = TextReference.Str( + value = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = yield.args.enter.args[KEY_AMOUNT]?.minimum?.toBigDecimal(), + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + ), + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_reward_claiming, + startText = TextReference.Res(R.string.staking_details_reward_claiming), + endText = TextReference.Str(yield.metadata.rewardClaiming), + iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) }, + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_warmup_period, + startText = TextReference.Res(R.string.staking_details_warmup_period), + endText = TextReference.Str(yield.metadata.warmupPeriod.days.toString()), + iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) }, + ), + RoundedListWithDividersItemData( + id = R.string.staking_details_reward_schedule, + startText = TextReference.Res(R.string.staking_details_reward_schedule), + endText = TextReference.Str(yield.metadata.rewardSchedule), + iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) }, + ), + ) + } + private fun createInitialAmountState(): AmountState { return amountStateConverter.convert("") } @@ -134,5 +191,6 @@ internal class SetInitialDataStateTransformer( companion object { private val EQUALITY_THRESHOLD = BigDecimal(1E-10) + private const val KEY_AMOUNT = "amount" } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 4eb8244ef3..859bc837c1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -7,9 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -27,8 +25,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo -import com.tangem.core.ui.components.rows.CornersToRound -import com.tangem.core.ui.components.rows.RoundableCornersRow +import com.tangem.core.ui.components.list.RoundedListWithDividers import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -37,7 +34,6 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.state.transformers.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.PLUS @@ -55,13 +51,12 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, cl start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16, - ) - .verticalScroll(rememberScrollState()), + ), ) { AnimatedVisibility(state.yieldBalance == InnerYieldBalanceState.Empty) { MetricsBlock(state) } - StakingDetailsRows(state) + RoundedListWithDividers(state.infoItems) AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { if (it is InnerYieldBalanceState.Data) { StakingRewardBlock( @@ -142,57 +137,6 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { } } -@Composable -internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { - Column { - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_available), - endText = state.available, - cornersToRound = CornersToRound.TOP_2, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_on_stake), - endText = state.onStake, - cornersToRound = CornersToRound.ZERO, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_apy), - endText = state.aprRange.resolveReference(), - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.APY) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_unbonding_period), - endText = state.unbondingPeriod, - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_minimum_requirement), - endText = state.minimumRequirement, - cornersToRound = CornersToRound.ZERO, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_reward_claiming), - endText = state.rewardClaiming, - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_warmup_period), - endText = state.warmupPeriod, - cornersToRound = CornersToRound.ZERO, - iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) }, - ) - InitialInfoContentRow( - startText = stringResource(id = R.string.staking_details_reward_schedule), - endText = state.rewardSchedule, - cornersToRound = CornersToRound.BOTTOM_2, - iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) }, - ) - } -} - @Composable private fun StakingRewardBlock( rewardCrypto: String, @@ -295,25 +239,7 @@ private fun ActiveStakingBlock(groups: List, onClick: (Bala } } -@Composable -private fun InitialInfoContentRow( - startText: String, - endText: String, - cornersToRound: CornersToRound, - iconClick: (() -> Unit)? = null, -) { - RoundableCornersRow( - startText = startText, - startTextColor = TangemTheme.colors.text.primary1, - startTextStyle = TangemTheme.typography.body2, - endText = endText, - endTextColor = TangemTheme.colors.text.tertiary, - endTextStyle = TangemTheme.typography.body2, - cornersToRound = cornersToRound, - iconResId = R.drawable.ic_information_24, - iconClick = iconClick, - ) -} +// region preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 9fb83bd2a5..18679abdb6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1866,7 +1866,6 @@ internal class SwapInteractorImpl @Inject constructor( } companion object { - @Suppress("UnusedPrivateMember") private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 51497e7f98..f02139cb42 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -847,7 +847,6 @@ internal class SwapViewModel @Inject constructor( onAmountChanged(newAmount.formatToUIRepresentation()) } - @Suppress("UnusedPrivateMember") private fun onAmountSelected(selected: Boolean) { if (selected) { analyticsEventHandler.send(SwapEvents.SendTokenBalanceClicked) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index d1ef7792c8..f3104d8636 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -101,7 +101,7 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { data class Stake(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_stake), - iconResId = R.drawable.ic_arrow_down_24, // TODO staking + iconResId = R.drawable.ic_staking_24, onClick = onClick, dimContent = dimContent, ), 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 6bdc2a3375..b885be8766 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 @@ -393,7 +393,10 @@ internal class TokenDetailsViewModel @Inject constructor( internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { - val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) + val stakingInfo = getStakingEntryInfoUseCase( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) internalUiState.value = stateFactory.getStateWithStaking(stakingInfo) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index 19da0ca6a5..4c811a13ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -96,7 +96,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_stake), - iconResId = R.drawable.ic_arrow_down_24, // TODO staking + iconResId = R.drawable.ic_staking_24, onClick = onClick, dimContent = dimContent, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 417ff93ede..d924dabe10 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -61,7 +61,7 @@ internal class MultiWalletCurrencyActionsConverter( } is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) - icon = R.drawable.ic_arrow_down_24 // TODO staking replace icon + icon = R.drawable.ic_staking_24 action = { clickIntents.onStakeClick(cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Sell -> { From 324ae269feee34b2cd1bf3b91713f4de5ae03b6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 19:50:31 +0500 Subject: [PATCH 186/223] Updated on 2026-08-14 --- features/disclaimer/impl/build.gradle.kts | 3 +++ .../features/disclaimer/impl/model/DisclaimerModel.kt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index 1e72ef4a09..d60dcd1885 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -42,6 +42,9 @@ dependencies { implementation(projects.features.disclaimer.api) implementation(projects.features.pushNotifications.api) + /** Other dependencies */ + implementation(deps.arrow.core) + /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index e3007b5a5a..f1be4240e0 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -6,8 +6,11 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.disclaimer.impl.entity.DisclaimerUM +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -18,6 +21,8 @@ internal class DisclaimerModel @Inject constructor( private val cardRepository: CardRepository, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, + private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -38,6 +43,8 @@ internal class DisclaimerModel @Inject constructor( if (shouldAskPushPermission) { router.push(AppRoute.PushNotification) } else { + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + neverRequestPermissionUseCase(PUSH_PERMISSION) router.push(AppRoute.Home) } } From 54833d8fc6fa76c50b88126298377c6657b8e854 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 17:53:30 +0300 Subject: [PATCH 187/223] Updated on 2026-08-14 --- .../domain/card/DefaultResetCardUseCase.kt | 40 ++++---- .../tangem/tap/domain/sdk/TangemSdkManager.kt | 4 +- .../sdk/impl/DefaultTangemSdkManager.kt | 5 +- .../domain/sdk/impl/MockTangemSdkManager.kt | 8 +- .../tasks/product/ResetBackupCardTask.kt | 8 +- .../product/ResetToFactorySettingsTask.kt | 15 +-- .../ui/cardsettings/CardSettingsViewModel.kt | 49 +++++----- .../details/ui/common/utils/ResetToFactory.kt | 7 +- .../ui/resetcard/ResetCardViewModel.kt | 96 +++++++++++-------- common/routing/build.gradle.kts | 1 + .../com/tangem/common/routing/AppRoute.kt | 14 ++- .../tangem/domain/card/ResetCardUseCase.kt | 18 ++-- domain/models/build.gradle.kts | 2 + .../com/tangem/domain/models/scan/CardDTO.kt | 10 ++ 14 files changed, 166 insertions(+), 111 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index 966f9613e9..1e322aeaba 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -11,8 +11,8 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.card.models.ResetCardError -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.sdk.TangemSdkManager @@ -20,24 +20,25 @@ internal class DefaultResetCardUseCase( private val tangemSdkManager: TangemSdkManager, ) : ResetCardUseCase { - override suspend fun invoke(card: CardDTO): Either = resourceScope { - either { - withUserCodeRequestPolicy(card) + override suspend fun invoke(cardId: String, params: ResetCardUserCodeParams): Either = + resourceScope { + either { + withUserCodeRequestPolicy(params) - tangemSdkManager.resetToFactorySettings( - cardId = card.cardId, - allowsRequestAccessCodeFromRepository = true, - ).bind(raise = this) + tangemSdkManager.resetToFactorySettings( + cardId = cardId, + allowsRequestAccessCodeFromRepository = true, + ).bind(raise = this) + } } - } override suspend fun invoke( cardNumber: Int, - card: CardDTO, + params: ResetCardUserCodeParams, userWalletId: UserWalletId, - ): Either = resourceScope { + ): Either = resourceScope { either { - withUserCodeRequestPolicy(card) + withUserCodeRequestPolicy(params) tangemSdkManager.resetBackupCard( cardNumber = cardNumber, @@ -46,11 +47,11 @@ internal class DefaultResetCardUseCase( } } - private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) { + private suspend fun ResourceScope.withUserCodeRequestPolicy(params: ResetCardUserCodeParams) { install( acquire = { val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy - requestMandatoryAccessCodeEntry(card) + requestMandatoryAccessCodeEntry(params) policyBeforeReset }, @@ -60,10 +61,10 @@ internal class DefaultResetCardUseCase( ) } - private fun requestMandatoryAccessCodeEntry(card: CardDTO) { - val type = if (card.isAccessCodeSet) { + private fun requestMandatoryAccessCodeEntry(params: ResetCardUserCodeParams) { + val type = if (params.isAccessCodeSet) { UserCodeType.AccessCode - } else if (card.isPasscodeSet == true) { + } else if (params.isPasscodeSet == true) { UserCodeType.Passcode } else { null @@ -74,15 +75,14 @@ internal class DefaultResetCardUseCase( } } - private fun CompletionResult<*>.bind(raise: Raise) { + private fun CompletionResult.bind(raise: Raise): Boolean { return when (this) { is CompletionResult.Failure -> { val domainError = error.mapToDomainError() raise.raise(domainError) } - is CompletionResult.Success -> { /* no-op */ - } + is CompletionResult.Success -> data } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt index 0f5e408f5e..2dbfaec5b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -70,9 +70,9 @@ interface TangemSdkManager { suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, - ): CompletionResult + ): CompletionResult - suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult + suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 00bacdbe26..835a2fee69 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -202,7 +202,7 @@ class DefaultTangemSdkManager( override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, - ): CompletionResult { + ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = ResetToFactorySettingsTask( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, @@ -210,10 +210,9 @@ class DefaultTangemSdkManager( cardId = cardId, initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)), ) - .map { CardDTO(it) } } - override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { + override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { return runTaskAsyncReturnOnMain( runnable = ResetBackupCardTask(userWalletId), initialMessage = Message( diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index dcc850f798..0dffc198a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -87,12 +87,12 @@ class MockTangemSdkManager( override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, - ): CompletionResult { - return MockProvider.getCardDto() + ): CompletionResult { + return CompletionResult.Success(true) } - override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { - return CompletionResult.Success(Unit) + override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { + return CompletionResult.Success(true) } override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index 7de7ceb374..288ca22a9d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -19,11 +19,11 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter */ internal class ResetBackupCardTask( private val userWalletId: UserWalletId, -) : CardSessionRunnable { +) : CardSessionRunnable { override val allowsRequestAccessCodeFromRepository: Boolean = false - override fun run(session: CardSession, callback: CompletionCallback) { + override fun run(session: CardSession, callback: CompletionCallback) { PreflightReadTask( readMode = PreflightReadMode.FullCardRead, filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), @@ -35,10 +35,10 @@ internal class ResetBackupCardTask( } } - private fun resetCard(session: CardSession, callback: CompletionCallback) { + private fun resetCard(session: CardSession, callback: CompletionCallback) { ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository).run(session) { result -> when (result) { - is CompletionResult.Success -> callback(CompletionResult.Success(Unit)) + is CompletionResult.Success -> callback(CompletionResult.Success(result.data)) is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } } 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 f06f2f801a..51ff0b2d80 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 @@ -10,13 +10,15 @@ import com.tangem.operations.wallet.PurgeWalletCommand class ResetToFactorySettingsTask( override val allowsRequestAccessCodeFromRepository: Boolean, -) : CardSessionRunnable { +) : CardSessionRunnable { - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + private var isResetCompleted = false + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { deleteWallets(session, callback) } - private fun deleteWallets(session: CardSession, callback: (result: CompletionResult) -> Unit) { + private fun deleteWallets(session: CardSession, callback: (result: CompletionResult) -> Unit) { val wallet = session.environment.card?.wallets?.lastOrNull().guard { resetBackup(session, callback) return @@ -25,6 +27,7 @@ class ResetToFactorySettingsTask( PurgeWalletCommand(wallet.publicKey).run(session) { result -> when (result) { is CompletionResult.Success -> { + isResetCompleted = true deleteWallets(session, callback) } is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) @@ -32,18 +35,18 @@ class ResetToFactorySettingsTask( } } - private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { + private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { if (session.environment.card?.backupStatus == null || session.environment.card?.backupStatus == Card.BackupStatus.NoBackup ) { - callback(CompletionResult.Success(session.environment.card!!)) + callback(CompletionResult.Success(isResetCompleted)) return } ResetBackupCommand().run(session) { result -> when (result) { is CompletionResult.Success -> { - callback(CompletionResult.Success(session.environment.card!!)) + callback(CompletionResult.Success(true)) } is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 4715114d15..97d4a464d9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute @@ -14,10 +13,9 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.CardTypesResolver 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.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow @@ -38,7 +36,6 @@ import javax.inject.Inject @HiltViewModel internal class CardSettingsViewModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, - private val getUserWalletUseCase: GetUserWalletUseCase, private val tangemSdkManager: TangemSdkManager, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -47,6 +44,8 @@ internal class CardSettingsViewModel @Inject constructor( ?.unbundle(UserWalletId.serializer()) ?: error("User wallet ID is required for CardSettingsViewModel") + private val scannedScanResponse = MutableStateFlow(value = null) + val screenState: MutableStateFlow = MutableStateFlow(getInitialState()) private fun getInitialState() = CardSettingsScreenState( @@ -58,13 +57,11 @@ internal class CardSettingsViewModel @Inject constructor( private fun scanCard() = viewModelScope.launch { scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> + scannedScanResponse.value = scanResponse + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() - val isCorrectUserWalletScanned = scannedUserWalletId == userWalletId - - if (isCorrectUserWalletScanned) { - val userWallet = getUserWallet() - - updateCardDetails(userWallet) + if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) { + updateCardDetails(scanResponse) } else { store.dispatchDialogShow( AppDialog.SimpleOkDialogRes( @@ -76,9 +73,9 @@ internal class CardSettingsViewModel @Inject constructor( } } - private fun updateCardDetails(userWallet: UserWallet) { - val card = userWallet.scanResponse.card - val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + private fun updateCardDetails(scanResponse: ScanResponse) { + val card = scanResponse.card + val cardTypesResolver = scanResponse.cardTypesResolver val cardId = cardTypesResolver.getCardId() val currentSecurityOption = getCurrentSecurityOption(card) @@ -112,7 +109,7 @@ internal class CardSettingsViewModel @Inject constructor( if (isResetCardAllowed) { CardInfo.ResetToFactorySettings( - description = getResetToFactoryDescription(card, cardTypesResolver), + description = getResetToFactoryDescription(card.backupStatus, cardTypesResolver), ).let(::add) } } @@ -129,9 +126,21 @@ internal class CardSettingsViewModel @Inject constructor( changeAccessCode() } is CardInfo.ResetToFactorySettings -> { + val card = requireNotNull(scannedScanResponse.value) { + "Impossible to reset card if ScanResponse is null" + }.card + Analytics.send(Settings.CardSettings.ButtonFactoryReset()) store.dispatchNavigationAction { - push(route = AppRoute.ResetToFactory(userWalletId)) + push( + route = AppRoute.ResetToFactory( + userWalletId = userWalletId, + cardSpecificInfo = AppRoute.ResetToFactory.CardSpecificInfo( + cardId = card.cardId, + backupStatus = card.backupStatus, + ), + ), + ) } } is CardInfo.SecurityMode -> { @@ -150,9 +159,9 @@ internal class CardSettingsViewModel @Inject constructor( } private fun changeAccessCode() = viewModelScope.launch { - val card = getUserWallet().scanResponse.card + val scanResponse = requireNotNull(scannedScanResponse.value) { "Scan response is null" } - when (val result = tangemSdkManager.setAccessCode(card.cardId)) { + when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) { is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) is CompletionResult.Failure -> { Timber.e("Failed to change access code: ${result.error}") @@ -160,12 +169,6 @@ internal class CardSettingsViewModel @Inject constructor( } } - private fun getUserWallet(): UserWallet { - return getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } - } - private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean { val hasPermanentWallet = card.wallets.any { it.settings.isPermanent } val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt index b576e8193e..5be7b9eb88 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -5,8 +5,11 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.wallet.R -internal fun getResetToFactoryDescription(card: CardDTO, typesResolver: CardTypesResolver): TextReference { - return if (card.backupStatus?.isActive != true || typesResolver.isTangemTwins()) { +internal fun getResetToFactoryDescription( + backupStatus: CardDTO.BackupStatus?, + typesResolver: CardTypesResolver, +): TextReference { + return if (backupStatus?.isActive != true || typesResolver.isTangemTwins()) { TextReference.Res(R.string.reset_card_without_backup_to_factory_message) } else { TextReference.Res(R.string.reset_card_with_backup_to_factory_message) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index d578f0875f..2131cd42e2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -11,12 +11,11 @@ import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.ResetCardUserCodeParams 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.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -39,7 +38,7 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel internal class ResetCardViewModel @Inject constructor( - private val getUserWalletUseCase: GetUserWalletUseCase, + getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val resetCardUseCase: ResetCardUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, @@ -49,9 +48,26 @@ internal class ResetCardViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val userWalletId = savedStateHandle.get(AppRoute.ResetToFactory.USER_WALLET_ID) + // region Card-set specific data. All cards from single set have the same userWalletId and cardTypesResolver + private val currentUserWalletId = savedStateHandle.get(AppRoute.ResetToFactory.USER_WALLET_ID) ?.unbundle(UserWalletId.serializer()) - ?: error("User wallet ID must be provided for ResetCardViewModel") + ?: error("UserWalletId must be provided for ResetCardViewModel") + + // Use only for card-specific data + private val userWallet = getUserWalletUseCase(userWalletId = currentUserWalletId) + .getOrElse { error("Failed to get user wallet: $it") } + + private val currentCardTypesResolver = userWallet.cardTypesResolver + private val currentUserCodeParams = ResetCardUserCodeParams( + isAccessCodeSet = userWallet.scanResponse.card.isAccessCodeSet, + isPasscodeSet = userWallet.scanResponse.card.isPasscodeSet, + ) + // endregion + + // region Data of card that was scanned on CardSettings + private val primaryCardId: String + private val primaryBackupStatus: CardDTO.BackupStatus? + // endregion // TODO: move logic to separate domain entity private var resetBackupCardCount = 0 @@ -60,29 +76,33 @@ internal class ResetCardViewModel @Inject constructor( value = getInitialState(), ) - private fun getInitialState(): ResetCardScreenState { - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } - val card = userWallet.scanResponse.card - val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - val descriptionText = getResetToFactoryDescription(card, cardTypesResolver) - val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() - val showResetPasswordButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active + init { + val cardSpecificInfo = savedStateHandle.get(AppRoute.ResetToFactory.CARD_SPECIFIC_DATA) + ?.unbundle(AppRoute.ResetToFactory.CardSpecificInfo.serializer()) + ?: error("CardSpecificData must be provided for ResetCardViewModel") + primaryCardId = cardSpecificInfo.cardId + primaryBackupStatus = cardSpecificInfo.backupStatus + } + + private fun getInitialState(): ResetCardScreenState { + val shouldShowResetPasswordButton = shouldShowResetPasswordButton() val warningsToShow = buildList { add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS) - if (showResetPasswordButton) { + if (shouldShowResetPasswordButton) { add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE) } } return ResetCardScreenState( resetButtonEnabled = false, - descriptionText = descriptionText, + descriptionText = getResetToFactoryDescription( + backupStatus = primaryBackupStatus, + typesResolver = currentCardTypesResolver, + ), warningsToShow = warningsToShow, - showResetPasswordButton = showResetPasswordButton, + showResetPasswordButton = shouldShowResetPasswordButton, acceptCondition1Checked = false, acceptCondition2Checked = false, onAcceptCondition1ToggleClick = ::toggleFirstCondition, @@ -92,6 +112,12 @@ internal class ResetCardViewModel @Inject constructor( ) } + private fun shouldShowResetPasswordButton(): Boolean { + val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2() + + return isTangemWallet && primaryBackupStatus is CardDTO.BackupStatus.Active + } + private fun toggleFirstCondition(isAccepted: Boolean) { screenState.update { prevState -> val resetButtonEnabled = if (prevState.showResetPasswordButton) { @@ -154,12 +180,9 @@ internal class ResetCardViewModel @Inject constructor( private fun makeFullReset() { viewModelScope.launch { - val userWallet = getUserWallet() - val scanResponse = userWallet.scanResponse - - resetCardUseCase(card = scanResponse.card).onRight { - deleteSavedAccessCodesUseCase(scanResponse.card.cardId) - val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight { + deleteSavedAccessCodesUseCase(cardId = primaryCardId) + val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse { Timber.e("Unable to delete user wallet: $it") return@launch } @@ -183,14 +206,15 @@ internal class ResetCardViewModel @Inject constructor( dismissDialog() viewModelScope.launch { - val userWallet = getUserWallet() resetCardUseCase( cardNumber = resetBackupCardCount + 1, - card = userWallet.scanResponse.card, - userWalletId = userWalletId, + params = currentUserCodeParams, + userWalletId = currentUserWalletId, ) - .onRight { - resetBackupCardCount++ + .onRight { isResetCompleted -> + if (isResetCompleted) { + resetBackupCardCount++ + } delay(DELAY_SDK_DIALOG_CLOSE) @@ -213,7 +237,7 @@ internal class ResetCardViewModel @Inject constructor( } private fun checkRemainingBackupCards() { - val backupCardsCount = getUserWallet().scanResponse.getBackupCardsCount() + val backupCardsCount = getBackupCardsCount() when { backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog) @@ -227,12 +251,6 @@ internal class ResetCardViewModel @Inject constructor( } } - private fun getUserWallet(): UserWallet { - return getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } - } - private fun dismissAndFinishFullReset() { dismissDialog() @@ -249,7 +267,7 @@ internal class ResetCardViewModel @Inject constructor( if (isLocked && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { popTo() } } else { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } } } } @@ -262,10 +280,10 @@ internal class ResetCardViewModel @Inject constructor( screenState.update { it.copy(dialog = null) } } - private fun ScanResponse.getBackupCardsCount(): Int { - if (!cardTypesResolver.isMultiwalletAllowed()) return 0 + private fun getBackupCardsCount(): Int { + if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0 - return when (val status = card.backupStatus) { + return when (val status = primaryBackupStatus) { is CardDTO.BackupStatus.Active -> status.cardCount is CardDTO.BackupStatus.CardLinked, is CardDTO.BackupStatus.NoBackup, diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index ae4552a1f0..a492ede7aa 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Domain */ implementation(projects.domain.qrScanning.models) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.staking.models) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index bdd1ea3030..8f9796de2b 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -5,6 +5,7 @@ import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency @@ -143,15 +144,26 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object AppSettings : AppRoute(path = "/app_settings") + /** + * Reset to factory route + * + * @property userWalletId user wallet id + * @property cardSpecificInfo info about card that was scanned on CardSettings + */ @Serializable data class ResetToFactory( val userWalletId: UserWalletId, - ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}"), RouteBundleParams { + val cardSpecificInfo: CardSpecificInfo, + ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}/$cardSpecificInfo"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) + @Serializable + data class CardSpecificInfo(val cardId: String, val backupStatus: CardDTO.BackupStatus?) + companion object { const val USER_WALLET_ID = "userWalletId" + const val CARD_SPECIFIC_DATA = "cardSpecificInfo" } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt index 7bdd4f2293..7a71009602 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.card import arrow.core.Either import com.tangem.domain.card.models.ResetCardError -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWalletId /** @@ -12,13 +11,18 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface ResetCardUseCase { - /** Reset card [card] to factory settings */ - suspend operator fun invoke(card: CardDTO): Either + /** Reset card [cardId] to factory settings */ + suspend operator fun invoke(cardId: String, params: ResetCardUserCodeParams): Either - /** Reset backup card [cardNumber] with expected [UserWalletId] using [card] of reset card */ + /** Reset backup card [cardNumber] with expected [UserWalletId] using [params] of reset card */ suspend operator fun invoke( cardNumber: Int, - card: CardDTO, + params: ResetCardUserCodeParams, userWalletId: UserWalletId, - ): Either -} \ No newline at end of file + ): Either +} + +data class ResetCardUserCodeParams( + val isAccessCodeSet: Boolean, + val isPasscodeSet: Boolean?, +) \ No newline at end of file diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index fbccb4120d..bf25c0f8b6 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -1,9 +1,11 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") } dependencies { implementation(deps.tangem.card.core) implementation(deps.moshi.kotlin) + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index ff2b5be1d0..91ceeb0049 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 @@ -8,6 +8,8 @@ import com.tangem.common.card.EncryptionMode import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.attestation.Attestation +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.util.Date import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion @@ -298,11 +300,19 @@ data class CardDTO( } } + @Serializable sealed class BackupStatus { + + @Serializable + @SerialName("card_linked") data class CardLinked(val cardCount: Int) : BackupStatus() + @Serializable + @SerialName("active") data class Active(val cardCount: Int) : BackupStatus() + @Serializable + @SerialName("no_backup") data object NoBackup : BackupStatus() val isActive: Boolean From e7106ec7b8e3e1d21460a9d16485545ed21ce8b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 23 Jul 2024 20:32:55 +0300 Subject: [PATCH 188/223] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d6bb419a92..ca1addf175 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-703" +tangemBlockchainSdk = "release-app_5.13-704" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-375" +tangemCardSdk = "release-app_5.13-376" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.21-tangem14" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 9cb70782cf812fb1a542d8c07fe18d3d7af4466c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 24 Jul 2024 11:33:04 +0300 Subject: [PATCH 189/223] Updated on 2026-08-14 --- .../tangem/tap/data/RuntimeUserWalletsStore.kt | 8 ++++++-- .../domain/card/DefaultDerivationsRepository.kt | 17 ++++++++--------- .../viewmodels/TokensListMigration.kt | 1 + .../viewmodels/TokensListViewModel.kt | 4 ---- .../card/DefaultDerivationsRepositoryTest.kt | 6 ++++-- .../local/userwallet/UserWalletsStore.kt | 7 +++++-- 6 files changed, 24 insertions(+), 19 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 58d13616e4..1661cedb1d 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -1,5 +1,6 @@ package com.tangem.tap.data +import com.tangem.common.CompletionResult import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet @@ -30,7 +31,10 @@ internal class RuntimeUserWalletsStore( return userWalletsListManager.userWallets.firstOrNull() } - override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) { - userWalletsListManager.update(userWalletId, update) + override suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult { + return userWalletsListManager.update(userWalletId, update) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index d7bb96e4c3..64e1b31472 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -19,7 +19,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.withContext import timber.log.Timber internal typealias Derivations = Map> @@ -48,13 +48,12 @@ internal class DefaultDerivationsRepository( tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> - updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold( - onSuccess = { - validateDerivations(userWallet.scanResponse, derivations) + updatePublicKeys(userWalletId = userWalletId, keys = response.entries) + .doOnSuccess { + validateDerivations(scanResponse = it.scanResponse, derivations = derivations) return - }, - onFailure = { throw it }, - ) + } + .doOnFailure { throw it } } .doOnFailure { throw it } @@ -99,8 +98,8 @@ internal class DefaultDerivationsRepository( } } - private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result { - return runCatching(dispatchers.io) { + private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult { + return withContext(dispatchers.io) { userWalletsStore.update( userWalletId = userWalletId, update = { userWallet -> userWallet.updateDerivedKeys(keys) }, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index b10741f96c..41495924b3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -135,6 +135,7 @@ internal class TokensListMigration( derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList) .onRight { addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList) + store.dispatchNavigationAction { popTo() } } .onLeft { Timber.e(it, "Failed to derive public keys") } } 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 a716c916bb..6e471d192d 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 @@ -11,8 +11,6 @@ import androidx.lifecycle.viewModelScope import androidx.paging.* import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getGreyedOutIconRes @@ -26,7 +24,6 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType @@ -326,7 +323,6 @@ internal class TokensListViewModel @Inject constructor( ) uiState = state.copy(isSavingInProgress = false) - store.dispatchNavigationAction { popTo() } } } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index fdf0ed8db2..12be6b889d 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -12,7 +12,9 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.Test @@ -144,7 +146,7 @@ internal class DefaultDerivationsRepositoryTest { coEvery { tangemSdkManager.derivePublicKeys(null, any()) } returns CompletionResult.Success( DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys), ) - coEvery { userWalletsStore.update(defaultUserWalletId, any()) } just Runs + coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet) runCatching { repository.derivePublicKeys( 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 6aa2933ba2..54fe4ff322 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 @@ -1,5 +1,6 @@ package com.tangem.datasource.local.userwallet +import com.tangem.common.CompletionResult import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -14,6 +15,8 @@ interface UserWalletsStore { suspend fun getAllSyncOrNull(): List? - @Throws - suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) + suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult } \ No newline at end of file From a10342f948c41b68ade7c973ee5191c104afd532 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jul 2024 11:56:30 +0300 Subject: [PATCH 190/223] Updated on 2026-08-14 --- app/build.gradle.kts | 12 ---- .../java/com/tangem/tap/TangemApplication.kt | 10 +--- .../com/tangem/tap/common/chat/ChatManager.kt | 23 -------- .../tap/common/chat/opener/ChatOpener.kt | 8 --- .../implementation/SprinklrChatOpener.kt | 57 ------------------- .../common/feedback/LegacyFeedbackManager.kt | 42 -------------- .../tap/common/redux/global/GlobalAction.kt | 2 - .../common/redux/global/GlobalMiddleware.kt | 20 ------- .../ui/dialogs/AttestationFailedDialog.kt | 2 +- gradle/dependencies.toml | 1 - 10 files changed, 3 insertions(+), 174 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbe71b61be..a003607888 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,20 +27,12 @@ configurations.all { resolutionStrategy { dependencySubstitution { - substitute(module("com.facebook.react:react-native")) - .using(module("com.facebook.react:react-android:0.72.4")) - - substitute(module("com.facebook.react:hermes-engine")) - .using(module("com.facebook.react:hermes-android:0.72.4")) - substitute(module("org.bouncycastle:bcprov-jdk15on")) .using(module("org.bouncycastle:bcprov-jdk18on:1.73")) } force( "org.bouncycastle:bcpkix-jdk15on:1.70", - "com.facebook.react:react-android:0.72.4", - "com.facebook.react:hermes-android:0.72.4", ) } } @@ -224,10 +216,6 @@ dependencies { implementation(deps.walletConnectCore) implementation(deps.walletConnectWeb3) implementation(deps.prettyLogger) - implementation("com.facebook.react:react-android:0.72.4") - implementation(deps.sprClient) { - exclude(group = "com.github.stephenc.jcip") - } /** Testing libraries */ testImplementation(deps.test.coroutine) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 9a6577a66f..eaca995355 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -52,7 +52,6 @@ import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler -import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.images.createCoilImageLoader @@ -316,7 +315,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private fun initWithConfigDependency(config: Config) { initAnalytics(this, config) - initFeedbackManager(this, foregroundActivityObserver, store) + initFeedbackManager(this, store) } private fun initAnalytics(application: Application, config: Config) { @@ -337,11 +336,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { // ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA] } - private fun initFeedbackManager( - context: Context, - foregroundActivityObserver: ForegroundActivityObserver, - store: Store, - ) { + private fun initFeedbackManager(context: Context, store: Store) { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo { return AdditionalFeedbackInfo().apply { appVersion = try { @@ -382,7 +377,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { val feedbackManager = LegacyFeedbackManager( infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, - chatManager = ChatManager(foregroundActivityObserver), feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, getFeedbackEmailUseCase = getFeedbackEmailUseCase, ) 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 deleted file mode 100644 index 5bbe3fc679..0000000000 --- a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.common.chat - -import android.content.Context -import com.tangem.datasource.config.models.ChatConfig -import com.tangem.datasource.config.models.SprinklrConfig -import com.tangem.tap.ForegroundActivityObserver -import com.tangem.tap.common.chat.opener.ChatOpener -import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener -import java.io.File - -class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) { - private val openers = mutableMapOf() - - fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) { - val opener = openers.getOrPut(config) { - when (config) { - is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver) - } - } - - opener.open(createFeedbackFile, createLogsFile) - } -} \ No newline at end of file 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 deleted file mode 100644 index aff21cad05..0000000000 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.common.chat.opener - -import android.content.Context -import java.io.File - -internal interface ChatOpener { - 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 deleted file mode 100644 index 0fcb2a4cf2..0000000000 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.tap.common.chat.opener.implementation - -import android.annotation.SuppressLint -import android.app.Application -import android.content.Context -import android.provider.Settings -import com.spr.messengerclient.config.SPRMessenger -import com.spr.messengerclient.config.bean.SPRMessengerConfig -import com.tangem.common.extensions.guard -import com.tangem.datasource.config.models.SprinklrConfig -import com.tangem.tap.ForegroundActivityObserver -import com.tangem.tap.common.chat.opener.ChatOpener -import timber.log.Timber -import java.io.File -import java.util.Locale - -internal class SprinklrChatOpener( - private val config: SprinklrConfig, - private val foregroundActivityObserver: ForegroundActivityObserver, -) : ChatOpener { - - override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) { - val messenger = SPRMessenger.shared() - - if (messenger.config == null) { - initSprConfig(messenger) - } - - messenger.startApplication() - } - - private fun initSprConfig(messenger: SPRMessenger) { - val application = foregroundActivityObserver.foregroundActivity?.application.guard { - Timber.e("The SPR chat cannot be opened because there are no activities in foreground") - return - } - - messenger.takeOff(application, createSprConfig(application, config)) - } - - @SuppressLint("HardwareIds") - private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig { - return SPRMessengerConfig().apply { - appId = config.appId - appKey = CHAT_APP_KEY - deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID) - environment = config.environment - skin = CHAT_SKIN - locale = Locale.getDefault().language - } - } - - private companion object { - const val CHAT_APP_KEY = "com.sprinklr.messenger.release" - const val CHAT_SKIN = "MODERN" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt index 9f2f6bb335..e7713995ac 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt @@ -2,13 +2,11 @@ package com.tangem.tap.common.feedback import android.content.Context import com.tangem.core.navigation.email.EmailSender -import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.TapWorkarounds import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector @@ -29,12 +27,10 @@ import java.io.StringWriter class LegacyFeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, - private val chatManager: ChatManager, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase, ) { - private var sessionFeedbackFile: File? = null private var sessionLogsFile: File? = null fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) { @@ -97,43 +93,6 @@ class LegacyFeedbackManager( } } - fun openChat(config: ChatConfig, feedbackData: FeedbackData) { - 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() - - if (file.exists()) { - sessionFeedbackFile = file - sessionFeedbackFile - } else { - null - } - } catch (ex: Exception) { - Timber.e(ex, "Can't create the logs file") - null - } - } - private fun getLogFile(context: Context): File? { return try { if (sessionLogsFile != null) { @@ -172,7 +131,6 @@ class LegacyFeedbackManager( private 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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index e7fba06fb7..ade5d123b2 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -3,7 +3,6 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.datasource.config.ConfigManager -import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse @@ -80,7 +79,6 @@ sealed class GlobalAction : Action { data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction() - data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() object ExchangeManager : GlobalAction() { object Init : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 957b756803..9c6882deda 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -74,26 +74,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) { scanResponse = action.scanResponse, ) } - is GlobalAction.OpenChat -> { - val globalState = store.state.globalState - val feedbackManager = globalState.feedbackManager.guard { - store.dispatchDebugErrorNotification("FeedbackManager not initialized") - return - } - val config = globalState.configManager?.config.guard { - store.dispatchDebugErrorNotification("Config not initialized") - return - } - - // if config not set -> try to get it based on a scanResponse.productType - val unsafeChatConfig = action.chatConfig ?: config.sprinklr - - val chatConfig = unsafeChatConfig.guard { - store.dispatchDebugErrorNotification("The chat config is not initialized") - return - } - feedbackManager.openChat(chatConfig, action.feedbackData) - } is GlobalAction.ExchangeManager.Init -> { val appStateSafe = appState() ?: return val config = appStateSafe.globalState.configManager?.config ?: return diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt index 3e9df5885e..5b6c8b8c85 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt @@ -13,7 +13,7 @@ internal object AttestationFailedDialog { return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { setTitle(R.string.common_error) setMessage(R.string.issuer_signature_loading_failed) - setPositiveButton(R.string.ok) { dialog, _ -> + setPositiveButton(R.string.common_ok) { dialog, _ -> dialog.dismiss() } setOnDismissListener { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index ca1addf175..09d4ef606c 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -254,7 +254,6 @@ 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" } -sprClient = { module = "com.spr:messengerclient", version.ref = "spr-client" } chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" } chuckerStub = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" } mlKit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlKit-barcodeScanning" } From 14b9178e2b9be3f2b1621da8e787f5919b434fa5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jul 2024 14:57:58 +0500 Subject: [PATCH 191/223] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 09d4ef606c..66427fc530 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.13-704" +tangemBlockchainSdk = "release-app_5.13-706" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.13-376" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 27fc4bd9f55b12a4ab54a654590d1e657ef4c9ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jul 2024 14:56:42 +0300 Subject: [PATCH 192/223] Updated on 2026-08-14 --- .../tokendetails/ui/TokenDetailsScreen.kt | 13 +++++++++++-- .../wallet/presentation/wallet/ui/WalletScreen.kt | 4 +++- 2 files changed, 14 insertions(+), 3 deletions(-) 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 9ea4aa95ee..378b55e62d 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 @@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -32,6 +31,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.OkxPromoNotification +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.event.EventEffect @@ -64,7 +64,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = bottomBarHeight + TangemTheme.dimens.spacing16, + ), + hostState = snackbarHostState, + ) + }, contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> 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 41698402f7..806518dbe8 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 @@ -368,7 +368,9 @@ private fun BaseScaffoldWithMarkets( WalletSnackbarHost( snackbarHostState = it, event = state.event, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing4) + .navigationBarsPadding(), ) }, containerColor = TangemTheme.colors.background.secondary, From 6391ced12b6669f37386f2a50455e6eb6a92da7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jul 2024 15:11:15 +0300 Subject: [PATCH 193/223] Updated on 2026-08-14 --- .../extension/AppExtensionConfigurations.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index d65c211b5d..1d659a2d09 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -65,9 +65,7 @@ private fun AppExtension.configureBuildTypes() { private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buildType: BuildType) { when (buildType) { - BuildType.Release, - BuildType.External, - -> { + BuildType.Release -> { isDebuggable = false isMinifyEnabled = false proguardFiles(extension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") @@ -76,6 +74,11 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil isDebuggable = true isMinifyEnabled = false } + BuildType.External -> { + initWith(extension.buildTypes.getByName(BuildType.Release.id)) + matchingFallbacks.add(BuildType.Release.id) + signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id) + } BuildType.Internal, BuildType.Mocked, -> { From f234915b638ee1c692ef242badecc40ba0a03260 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Jul 2024 12:45:54 +0300 Subject: [PATCH 194/223] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsViewModel.kt | 18 +++++++--- .../details/ui/common/utils/ResetToFactory.kt | 5 ++- .../ui/resetcard/ResetCardViewModel.kt | 33 +++++++------------ .../com/tangem/common/routing/AppRoute.kt | 29 ++++++++++------ .../com/tangem/domain/models/scan/CardDTO.kt | 9 ----- .../wallet/viewmodels/WalletViewModel.kt | 2 ++ 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 97d4a464d9..d8b9f44ca9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -109,7 +109,10 @@ internal class CardSettingsViewModel @Inject constructor( if (isResetCardAllowed) { CardInfo.ResetToFactorySettings( - description = getResetToFactoryDescription(card.backupStatus, cardTypesResolver), + description = getResetToFactoryDescription( + isActiveBackupStatus = card.backupStatus?.isActive == true, + typesResolver = cardTypesResolver, + ), ).let(::add) } } @@ -135,10 +138,15 @@ internal class CardSettingsViewModel @Inject constructor( push( route = AppRoute.ResetToFactory( userWalletId = userWalletId, - cardSpecificInfo = AppRoute.ResetToFactory.CardSpecificInfo( - cardId = card.cardId, - backupStatus = card.backupStatus, - ), + cardId = card.cardId, + isActiveBackupStatus = card.backupStatus?.isActive == true, + backupCardsCount = when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + is CardDTO.BackupStatus.CardLinked, + CardDTO.BackupStatus.NoBackup, + null, + -> 0 + }, ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt index 5be7b9eb88..b269ee6352 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -1,15 +1,14 @@ package com.tangem.tap.features.details.ui.common.utils import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.wallet.R internal fun getResetToFactoryDescription( - backupStatus: CardDTO.BackupStatus?, + isActiveBackupStatus: Boolean, typesResolver: CardTypesResolver, ): TextReference { - return if (backupStatus?.isActive != true || typesResolver.isTangemTwins()) { + return if (!isActiveBackupStatus || typesResolver.isTangemTwins()) { TextReference.Res(R.string.reset_card_without_backup_to_factory_message) } else { TextReference.Res(R.string.reset_card_with_backup_to_factory_message) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 2131cd42e2..41d0535147 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -13,7 +13,6 @@ import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWalletId @@ -65,8 +64,15 @@ internal class ResetCardViewModel @Inject constructor( // endregion // region Data of card that was scanned on CardSettings - private val primaryCardId: String - private val primaryBackupStatus: CardDTO.BackupStatus? + private val primaryCardId: String = savedStateHandle.get(AppRoute.ResetToFactory.CARD_ID) + ?: error("CardId must be provided for ResetCardViewModel") + + private val isActiveBackupPrimaryCard = + savedStateHandle.get(AppRoute.ResetToFactory.IS_ACTIVE_BACKUP_STATUS) + ?: error("IsActiveBackupCard must be provided for ResetCardViewModel") + + private val primaryBackupCardsCount = savedStateHandle.get(AppRoute.ResetToFactory.BACKUP_CARDS_COUNT) + ?: error("CardCount must be provided for ResetCardViewModel") // endregion // TODO: move logic to separate domain entity @@ -76,15 +82,6 @@ internal class ResetCardViewModel @Inject constructor( value = getInitialState(), ) - init { - val cardSpecificInfo = savedStateHandle.get(AppRoute.ResetToFactory.CARD_SPECIFIC_DATA) - ?.unbundle(AppRoute.ResetToFactory.CardSpecificInfo.serializer()) - ?: error("CardSpecificData must be provided for ResetCardViewModel") - - primaryCardId = cardSpecificInfo.cardId - primaryBackupStatus = cardSpecificInfo.backupStatus - } - private fun getInitialState(): ResetCardScreenState { val shouldShowResetPasswordButton = shouldShowResetPasswordButton() val warningsToShow = buildList { @@ -98,7 +95,7 @@ internal class ResetCardViewModel @Inject constructor( return ResetCardScreenState( resetButtonEnabled = false, descriptionText = getResetToFactoryDescription( - backupStatus = primaryBackupStatus, + isActiveBackupStatus = isActiveBackupPrimaryCard, typesResolver = currentCardTypesResolver, ), warningsToShow = warningsToShow, @@ -115,7 +112,7 @@ internal class ResetCardViewModel @Inject constructor( private fun shouldShowResetPasswordButton(): Boolean { val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2() - return isTangemWallet && primaryBackupStatus is CardDTO.BackupStatus.Active + return isTangemWallet && isActiveBackupPrimaryCard } private fun toggleFirstCondition(isAccepted: Boolean) { @@ -283,12 +280,6 @@ internal class ResetCardViewModel @Inject constructor( private fun getBackupCardsCount(): Int { if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0 - return when (val status = primaryBackupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - is CardDTO.BackupStatus.NoBackup, - null, - -> 0 - } + return primaryBackupCardsCount } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 8f9796de2b..8bb8395a7d 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -5,7 +5,6 @@ import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency @@ -145,25 +144,35 @@ sealed class AppRoute(val path: String) : Route { data object AppSettings : AppRoute(path = "/app_settings") /** - * Reset to factory route + * Reset to factory * - * @property userWalletId user wallet id - * @property cardSpecificInfo info about card that was scanned on CardSettings + * @property userWalletId user wallet id + * @property cardId reset card id + * @property isActiveBackupStatus reset backup card status + * @property backupCardsCount backup cards count */ @Serializable data class ResetToFactory( val userWalletId: UserWalletId, - val cardSpecificInfo: CardSpecificInfo, - ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}/$cardSpecificInfo"), RouteBundleParams { + val cardId: String, + val isActiveBackupStatus: Boolean, + val backupCardsCount: Int, + ) : AppRoute( + path = "/reset_to_factory" + + "/${userWalletId.stringValue}" + + "/$cardId" + + "/$isActiveBackupStatus" + + "/$backupCardsCount", + ), + RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - @Serializable - data class CardSpecificInfo(val cardId: String, val backupStatus: CardDTO.BackupStatus?) - companion object { const val USER_WALLET_ID = "userWalletId" - const val CARD_SPECIFIC_DATA = "cardSpecificInfo" + const val CARD_ID = "cardId" + const val IS_ACTIVE_BACKUP_STATUS = "isActiveBackupStatus" + const val BACKUP_CARDS_COUNT = "backupCardsCount" } } 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 91ceeb0049..fcce746f21 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 @@ -8,8 +8,6 @@ import com.tangem.common.card.EncryptionMode import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.attestation.Attestation -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.util.Date import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion @@ -300,19 +298,12 @@ data class CardDTO( } } - @Serializable sealed class BackupStatus { - @Serializable - @SerialName("card_linked") data class CardLinked(val cardCount: Int) : BackupStatus() - @Serializable - @SerialName("active") data class Active(val cardCount: Int) : BackupStatus() - @Serializable - @SerialName("no_backup") data object NoBackup : BackupStatus() val isActive: Boolean 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 a15817a145..3a9681b918 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 @@ -341,6 +341,8 @@ internal class WalletViewModel @Inject constructor( } private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + walletScreenContentLoader.cancel(action.deletedWalletId) + walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, From eecf7955cae1a851be373b522055525d5dfb9b8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Jul 2024 14:39:05 +0300 Subject: [PATCH 195/223] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 8a80aff9f7..8758b045e3 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.utils.popTo import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable @@ -49,7 +48,7 @@ internal class LockUserWalletsTimer( start() if (shouldOpenWelcomeScreenOnResume) { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) } } @@ -127,7 +126,7 @@ internal class LockUserWalletsTimer( if (wasApplicationStopped) { settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) } else { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } } } } From 31d44e41508b5df6d2be2cc6f0a697597d07b452 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Jul 2024 18:13:54 +0500 Subject: [PATCH 196/223] Updated on 2026-08-14 --- .../features/details/component/impl/DefaultDetailsComponent.kt | 2 +- gradle/dependencies.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt index 9a9c5575d1..ec33cbbaaf 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt @@ -24,7 +24,7 @@ internal class DefaultDetailsComponent @AssistedInject constructor( private val model: DetailsModel = getOrCreateModel(params) private val userWalletListComponent = userWalletListComponentFactory.create( - context = child(key = "user_wallet_list"), + context = child(key = "user_wallet_list_component"), ) @Composable diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 66427fc530..74fab9ccbd 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.13-706" +tangemBlockchainSdk = "release-app_5.13-707" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.13-376" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 1e29b39eec4dfb29b6a12db4066cfaf3419a8533 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Jul 2024 17:03:36 +0300 Subject: [PATCH 197/223] Updated on 2026-08-14 --- .../details/redux/DetailsMiddleware.kt | 2 +- .../features/details/utils/UserWalletSaver.kt | 63 ++++++++++++------- 2 files changed, 42 insertions(+), 23 deletions(-) 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 689263efaf..14a1147b89 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 @@ -247,7 +247,7 @@ class DetailsMiddleware { deleteSavedAccessCodes() store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } return CompletionResult.Success(Unit) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 47ae7ed4f7..80a7f98ce1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -1,17 +1,22 @@ package com.tangem.features.details.utils -import arrow.core.raise.* -import arrow.core.recover +import androidx.compose.ui.res.stringResource +import arrow.core.raise.Raise +import arrow.core.raise.ensureNotNull +import arrow.core.raise.fold +import arrow.core.raise.recover import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.navigation.popTo import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.SimpleOkDialog import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse @@ -21,7 +26,7 @@ import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase import com.tangem.features.details.impl.R import javax.inject.Inject @@ -31,7 +36,7 @@ internal class UserWalletSaver @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, private val generateWalletNameUseCase: GenerateWalletNameUseCase, - private val selectWalletUseCase: SelectWalletUseCase, + private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val reduxStateHolder: ReduxStateHolder, private val messageSender: UiMessageSender, private val router: Router, @@ -43,8 +48,6 @@ internal class UserWalletSaver @Inject constructor( val userWallet = createUserWallet(response) saveWallet(userWallet) - - router.popTo() }, recover = { error -> val message = error.message @@ -56,28 +59,44 @@ internal class UserWalletSaver @Inject constructor( ) private suspend fun Raise.saveWallet(userWallet: UserWallet) { - saveWalletUseCase(userWallet).recover { error -> - when (error) { - is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet) - is SaveWalletError.DataError -> { - val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { - Error.Unknown + fold( + block = { saveWalletUseCase(userWallet).bind() }, + recover = { error -> + when (error) { + is SaveWalletError.WalletAlreadySaved -> { + if (shouldSaveUserWalletsSyncUseCase()) { + selectUserWallet() + } else { + router.popTo() + } } + is SaveWalletError.DataError -> { + val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { + Error.Unknown + } - raise(Error.Message(messageRef)) + raise(Error.Message(messageRef)) + } } - } - }.bind() + }, + transform = { + // call only if wallet is successfully saved + reduxStateHolder.onUserWalletSelected(userWallet) - reduxStateHolder.onUserWalletSelected(userWallet) + router.popTo() + }, + ) } - private suspend fun Raise.selectUserWallet(userWallet: UserWallet) { - withError({ Error.Unknown }) { - selectWalletUseCase(userWallet.walletId).bind() - } - - router.popTo() + private fun selectUserWallet() { + messageSender.send( + message = ContentMessage { onDismiss -> + SimpleOkDialog( + message = stringResource(id = R.string.user_wallet_list_error_wallet_already_saved), + onDismissDialog = onDismiss, + ) + }, + ) } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { From 1a2eb895be9f3cabffcfcda1e39ed8e15a30285c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jul 2024 15:20:47 +0500 Subject: [PATCH 198/223] Updated on 2026-08-14 --- .../features/disclaimer/impl/ui/DisclaimerScreen.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index b3740659cc..208feec00e 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -59,7 +59,11 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { .background(backgroundColor) .statusBarsPadding(), ) { - Column(modifier = Modifier.padding(bottom = bottomPadding)) { + Column( + modifier = Modifier + .padding(bottom = bottomPadding) + .fillMaxSize(), + ) { TangemTopAppBar( title = resourceReference(R.string.disclaimer_title), startButton = TopAppBarButtonUM( @@ -110,6 +114,7 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { it.setBackgroundColor(backgroundColor.toArgb()) }, client = remember { DisclaimerWebViewClient() }, + modifier = Modifier.fillMaxSize(), ) AnimatedVisibility( @@ -117,6 +122,9 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { label = "Loading state change animation", enter = fadeIn(), exit = fadeOut(), + modifier = Modifier + .fillMaxSize() + .background(backgroundColor), ) { Box( modifier = Modifier From 5ef6a4167db23f8a0357efded0bd7c99f4da58a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jul 2024 15:32:22 +0300 Subject: [PATCH 199/223] Updated on 2026-08-14 --- .../features/onboarding/OnboardingHelper.kt | 11 +++++----- .../products/note/OnboardingNoteFragment.kt | 9 +++++++- .../note/redux/OnboardingNoteReducer.kt | 2 +- .../note/redux/OnboardingNoteState.kt | 8 ++----- .../products/twins/redux/TwinCardsState.kt | 6 ----- .../twins/ui/OnboardingTwinsFragment.kt | 22 +++++++++++++------ .../exchangeServices/BuyExchangeService.kt | 4 +++- .../exchangeServices/CardExchangeRules.kt | 5 +++-- .../CurrencyExchangeManager.kt | 6 +++-- .../exchangeServices/DefaultRampManager.kt | 4 +++- .../exchangeServices/ExchangeService.kt | 7 +++--- .../mercuryo/MercuryoService.kt | 3 ++- .../moonpay/MoonPayService.kt | 3 ++- .../domain/exchange/RampStateManager.kt | 3 ++- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 7 +++--- 15 files changed, 59 insertions(+), 41 deletions(-) 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 d6c4adff57..404a469233 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 @@ -32,26 +32,27 @@ import timber.log.Timber */ object OnboardingHelper { suspend fun isOnboardingCase(response: ScanResponse): Boolean { - val onboardingManager = store.state.globalState.onboardingState.onboardingManager + val onboardingManager = + store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response) val cardId = response.card.cardId return when { response.cardTypesResolver.isTangemTwins() -> { if (!response.twinsIsTwinned()) { true } else { - onboardingManager?.isActivationInProgress(cardId) ?: false + onboardingManager.isActivationInProgress(cardId) ?: false } } response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> { val emptyWallets = response.card.wallets.isEmpty() - val activationInProgress = onboardingManager?.isActivationInProgress(cardId) + val activationInProgress = onboardingManager.isActivationInProgress(cardId) val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup && !DemoHelper.isDemoCard(response) - emptyWallets || activationInProgress == true || isNoBackup + emptyWallets || activationInProgress || isNoBackup } - response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false + response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) ?: false else -> true } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index 7ac3806126..757ec91454 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -11,12 +11,14 @@ import coil.load import com.tangem.blockchain.common.Blockchain import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.ShareElement +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.getDrawableCompat import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState @@ -134,7 +136,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { - if (state.isBuyAllowed) { + if (availableForBuy(state.scanResponse, state.walletBalance)) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.icon = null btnMainAction.setOnClickListener { @@ -217,6 +219,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment() { } } + private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean { + scanResponse ?: return false + return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency) + } + override fun handleOnBackPressed() { store.dispatch(OnboardingNoteAction.OnBackPressed) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt index 5552fc8b50..14f7989391 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt @@ -13,7 +13,7 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt when (action) { is GlobalAction.Onboarding.Start -> { - state = OnboardingNoteState() + state = OnboardingNoteState(scanResponse = action.scanResponse) } is OnboardingNoteAction.SetArtworkUrl -> { state = state.copy(cardArtworkUrl = action.artworkUrl) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt index ae7a2db594..3d659e39da 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt @@ -1,11 +1,10 @@ package com.tangem.tap.features.onboarding.products.note.redux import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import com.tangem.tap.store import org.rekotlin.StateType -import kotlin.properties.ReadOnlyProperty /** [REDACTED_AUTHOR] @@ -20,14 +19,11 @@ data class OnboardingNoteState( val currentStep: OnboardingNoteStep = OnboardingNoteStep.None, val steps: List = OnboardingNoteStep.values().toList(), val showConfetti: Boolean = false, + val scanResponse: ScanResponse? = null, ) : StateType { val progress: Int get() = steps.indexOf(currentStep) - - val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> - store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency) - } } enum class OnboardingNoteStep { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index fcc38388ac..a8867a1a2c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -6,9 +6,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import com.tangem.tap.store import org.rekotlin.StateType -import kotlin.properties.ReadOnlyProperty /** [REDACTED_AUTHOR] @@ -56,10 +54,6 @@ data class TwinCardsState( val twinningInProgress: Boolean get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet - - val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> - store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency) - } } enum class CreateTwinWalletMode { CreateWallet, RecreateWallet } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index e1e0a58536..69db8e0120 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -27,6 +27,7 @@ import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.onboarding.OnboardingMenuProvider +import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction @@ -65,12 +66,7 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( } override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( - scanResponseProvider = Provider { - store.state.twinCardsState.welcomeOnlyScanResponse - ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse - ?: store.state.detailsState.scanResponse - ?: error("ScanResponse must be not null") - }, + scanResponseProvider = Provider { getActualScanResponse() }, ) @Suppress("MagicNumber") @@ -342,7 +338,7 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( else -> {} } - if (state.isBuyAllowed) { + if (availableForBuy(getActualScanResponse(), state.walletBalance)) { btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) btnMainAction.setOnClickListener { store.dispatch(TwinCardsAction.TopUp) @@ -424,6 +420,18 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( } } + private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean { + scanResponse ?: return false + return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency) + } + + private fun getActualScanResponse(): ScanResponse { + return store.state.twinCardsState.welcomeOnlyScanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: store.state.detailsState.scanResponse + ?: error("ScanResponse must be not null") + } + override fun handleOnBackPressed() { store.dispatch( TwinCardsAction.OnBackPressed { should, popAction -> diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt index e6c523e666..a8861eed2f 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt @@ -1,5 +1,6 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService @@ -32,7 +33,8 @@ internal class BuyExchangeService( override fun isSellAllowed(): Boolean = currentService.isSellAllowed() - override fun availableForBuy(currency: Currency): Boolean = currentService.availableForBuy(currency) + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = + currentService.availableForBuy(scanResponse, currency) override fun availableForSell(currency: Currency): Boolean = currentService.availableForSell(currency) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt index 0f4fd42f7d..696f9288bc 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt @@ -2,6 +2,7 @@ package com.tangem.tap.network.exchangeServices import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.model.Currency import com.tangem.tap.features.demo.isDemoCard @@ -38,8 +39,8 @@ class CardExchangeRules( } } - override fun availableForBuy(currency: Currency): Boolean { - val card = cardProvider() ?: return false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { + val card = scanResponse.card return when { card.isDemoCard() -> true 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 e962f7ebf4..fb01befe82 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 @@ -8,6 +8,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate @@ -38,8 +39,9 @@ class CurrencyExchangeManager( override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed() override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed() - override fun availableForBuy(currency: Currency): Boolean { - return primaryRules.availableForBuy(currency) && buyService.availableForBuy(currency) + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { + return primaryRules.availableForBuy(scanResponse, currency) && + buyService.availableForBuy(scanResponse, currency) } override fun availableForSell(currency: Currency): Boolean { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 76de7375da..904e22c80e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -1,13 +1,15 @@ package com.tangem.tap.network.exchangeServices import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { private val cryptoCurrencyConverter = CryptoCurrencyConverter() - override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean { + override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean { return exchangeService?.availableForBuy( + scanResponse, currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), ) ?: false } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index e9b17488e3..1cfd8502ed 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -1,5 +1,6 @@ package com.tangem.tap.network.exchangeServices +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.feature.Feature import com.tangem.tap.domain.model.Currency @@ -7,7 +8,7 @@ import com.tangem.tap.domain.model.Currency interface Exchanger { fun isBuyAllowed(): Boolean fun isSellAllowed(): Boolean - fun availableForBuy(currency: Currency): Boolean + fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean fun availableForSell(currency: Currency): Boolean } @@ -20,7 +21,7 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { override suspend fun update() {} override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false - override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false override fun getUrl( action: CurrencyExchangeManager.Action, @@ -45,7 +46,7 @@ interface ExchangeRules : Feature, Exchanger { override fun featureIsSwitchedOn(): Boolean = false override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false - override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean = false } } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index d653e2babe..8bcc8168e9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,6 +6,7 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -28,7 +29,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun isSellAllowed(): Boolean = false - override fun availableForBuy(currency: Currency): Boolean { + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { if (!isBuyAllowed()) return false val mercuryoNetwork = currency.blockchain.mercuryoNetwork() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index c004c38b21..de34810420 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -7,6 +7,7 @@ import com.tangem.common.services.Result import com.tangem.common.services.performRequest import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.domain.common.extensions.withIOContext +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager @@ -83,7 +84,7 @@ class MoonPayService( return status?.responseUserStatus?.isSellAllowed ?: false } - override fun availableForBuy(currency: Currency): Boolean = false + override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean { if (!isSellAllowed()) return false diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index ac9b4fa892..2536d78ea9 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -1,5 +1,6 @@ package com.tangem.domain.exchange +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency /** @@ -7,7 +8,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency */ interface RampStateManager { - fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean + fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 16083adcbb..c83af879ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -106,7 +106,7 @@ class GetCryptoCurrencyActionsUseCase( return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) } if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(cryptoCurrencyStatus, needAssociateAsset) + return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, needAssociateAsset) } val activeList = mutableListOf() @@ -168,7 +168,7 @@ class GetCryptoCurrencyActionsUseCase( } // buy - if (rampManager.availableForBuy(cryptoCurrency)) { + if (rampManager.availableForBuy(userWallet.scanResponse, cryptoCurrency)) { activeList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { disabledList.add( @@ -222,6 +222,7 @@ class GetCryptoCurrencyActionsUseCase( } private fun getActionsForUnreachableCurrency( + userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, needAssociateAsset: Boolean, ): List { @@ -230,7 +231,7 @@ class GetCryptoCurrencyActionsUseCase( if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { actionsList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) } - if (rampManager.availableForBuy(cryptoCurrencyStatus.currency)) { + if (rampManager.availableForBuy(userWallet.scanResponse, cryptoCurrencyStatus.currency)) { actionsList.add(TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None)) } else { actionsList.add( From f4b545f348bb92b4204d3aba165e752d06ebf288 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jul 2024 16:16:14 +0300 Subject: [PATCH 200/223] Updated on 2026-08-14 --- .../ui/components/SaveWalletScreenContent.kt | 23 +-- core/res/src/main/res/values-de/strings.xml | 148 ++++++++++++++++-- core/res/src/main/res/values-fr/strings.xml | 15 +- core/res/src/main/res/values-ja/strings.xml | 7 +- core/res/src/main/res/values-ru/strings.xml | 7 + .../src/main/res/values-uk-rUA/strings.xml | 74 ++++++++- .../src/main/res/values-zh-rTW/strings.xml | 8 +- core/res/src/main/res/values/strings.xml | 14 +- .../features/details/utils/ItemsBuilder.kt | 2 +- .../walletsettings/utils/ItemsBuilder.kt | 2 +- 10 files changed, 259 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index c7679a466d..4206df1904 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -2,13 +2,7 @@ package com.tangem.tap.features.saveWallet.ui.components import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text @@ -20,16 +14,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerHHalf -import com.tangem.core.ui.components.SpacerW24 -import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.wallet.R @Composable @@ -37,10 +25,7 @@ internal fun SaveWalletScreenContent(showProgress: Boolean, onSaveWalletClick: ( Column(horizontalAlignment = Alignment.CenterHorizontally) { Header(onCloseClick = onCloseClick) SpacerHHalf() - Title( - modifier = Modifier - .widthIn(max = TangemTheme.dimens.size200), - ) + Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22)) SpacerH32() Description( modifier = Modifier diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 3f0832f3fb..12e30ed235 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,8 +1,10 @@ + Netzwerk wählen Benutzerdefiniertes Token hinzufügen Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. + So scannt man Hilfe anfordern Erneut versuchen Diese Funktion ist im Demomodus deaktiviert @@ -21,7 +23,7 @@ Durch das Entfernen der gespeicherten Karte werden alle gespeicherten Wallets und deren Zugangscodes aus der App gelöscht. Zugangscode speichern Bei Interaktionen mit deiner Karte wird anstelle des Zugangscodes eine biometrische Authentifizierung abgefragt. - Behalten die Wallet in der App + Behalte die Wallet in der App Aktiviere die Verknüpfung aller Wallets mit der Tangem-App. Die biometrische Authentifizierung ist zum Entsperren der App erforderlich. Das Signieren von Transaktionen erfordert das Antippen deiner Tangem-Karte. Dunkel Hell @@ -37,6 +39,7 @@ Zu viele Versuche Du hast die biometrische Authentifizierung auf deinem Telefon deaktiviert und kannst keine Wallets in der App speichern. Um Wallets zu speichern, aktiviere bitte die biometrische Authentifizierung in deinen Telefoneinstellungen. Backup-Prozess starten + Von deiner Fiat-Karte oder deinem Bankkonto %d Karte %d Karten @@ -65,6 +68,8 @@ Nicht genug ADA Akzeptieren Zugang verweigert + Alle + Erlauben Anwenden Genehmigung Genehmigen @@ -77,12 +82,17 @@ Gehe zu %1$s D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an Abbrechen + Stakingbelohnungen beanstpruchen Schließen Weitermachen Kopieren Adresse kopieren Erstellen Benutzerdefiniert + + Tag + Tage + Entfernen Deaktiviert Erledigt @@ -100,6 +110,7 @@ Geschwindigkeit und Gebühr Adressen abrufen Zum Anbieter gehen + Zum Token Importieren Später Gesperrt @@ -109,6 +120,7 @@ Weiter Nein Keine Adresse + Jetzt OK Primär Karte Passphrase @@ -139,12 +151,14 @@ Unterstützung Tauschen Allgemeine Geschäftsbedingungen + Heute Transaktion fehlgeschlagen Transaktionen - Überweisen + Überweisung Ich verstehe Es ist ein Fehler aufgetreten. Bitte versuche es erneut. Nicht erreichbar + staking beenden Ja Vertragsadresse kopiert! Verfügbare Netzwerke @@ -212,6 +226,8 @@ Getauscht von %s Besuche die Website des Anbieters, um dein Geld zurückzuerhalten Fehler beim Vorgang durch Anbieter + Der Transaktionsbetrag wurde aufgrund von OKX- oder Bridge-Regeln in %1$s auf deine Wallet zurückerstattet. %2$s + Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet. Besuche die Website des Anbieters zur Überprüfung KYC-Überprüfung durch den Anbieter erforderlich Abgebrochen @@ -232,6 +248,7 @@ Daten stammen vom Anbieter. Der geschätzte Betrag kann sich aufgrund der Marktbedingungen ändern. Status des Tausches Verifizierung erforderlich + Wartet auf Transaktions-Hash Liste aller Token, die deiner Wallet hinzugefügt wurden Beste Preise werden abgerufen … Variabler Zinssatz @@ -245,10 +262,12 @@ Erhältlich bei %s Für dieses Paar nicht verfügbar Erlaubnis erforderlich + Empfohlen Nutzungsbedingungen Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s Transaktions-ID kopiert + Aus einer anderen Währung in deiner Wallet Die folgenden Angaben sind freiwillig. Du kannst diese löschen, wenn du sie nicht weitergeben möchtest. Teile uns mit, welche Funktionen du vermisst, und wir werden versuchen, dir zu helfen. Bitte sag uns, welche Karte du hast @@ -263,6 +282,8 @@ Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Gib das Genehmigungslimit für das ausgewählte Token an Betrag %s + Die Genehmigungsfunktion ist erforderlich, um einer anderen Adresse die Berechtigung zur Verwendung einer bestimmten Menge Ihrer Token zu erteilen. Standardmäßig können Smart Contracts nicht auf deine Token zugreifen, es sei denn, du stimmen zu. Indem du deine Token \"freischaltest\", autorisierst du den StakeKit Smart Contract, sie zu verwenden. Die Miner des Netzwerks erhalten eine Gasgebühr (von dir bezahlt), um diese Aktion in der Blockchain aufzuzeichnen. Du kannst deine Token einsetzen, nachdem du die Genehmigung erteilt hast. + Um fortzufahren, musst du StakeKit Smart Contract erlauben, deine %s zu verwenden Um fortzufahren, erteile %1s Smart Contracts die Berechtigung, dein zu %2s verwenden. Erlaubnis erteilen Unbegrenzt @@ -306,11 +327,85 @@ %1$d von %2$d Wallet %1$d von %2$d Wallets + Entfernen z.B. BTC vertraue ich, hodl muss ich + Dein Portfolio wurde aktualisiert Der ausgewählte Token ist derzeit nicht für Aktionen innerhalb der Krypto-Wallet verfügbar. Aber keine Sorge, du kannst dein Interesse bekunden, indem du den Token hochstufen. Hochstimmen Wallet auswählen Das Wallet unterstützt nicht mehr als ein Netzwerk + Um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen, füge diesen Token zu mindestens 1 Netzwerk hinzu + Dieses Asset ist nicht verfügbar + Zum Portfolio hinzufügen + Token hinzufügen + Verfügbare Netzwerke + Mein Portfolio + Markt + Um Adressen für ausgewählte Netzwerke zu generieren, musst du eine Tangem-Karte scannen/ einsetzen + Die Daten konnten nicht geladen werden... + Schnelle Aktionen + Ergebnis + Token unter 100k Marktkapitalisierung anzeigen + Token anzeigen + Kein Ergebnis + Netzwerk auswählen + Wallet auswählen + 1M + 1 Y + 24H + 3M + 6M + 7T + Alle + Erfahrene Käufer + Bewertung + Sortieren nach + Top-Gewinner + Top-Verlierer + Beliebt + Über %s + + Bewertung, basierend auf %d + Bewertungen, basierend auf %d + + Blockchain-Site + Kaufdruck + Die Differenz zwischen Käufer- und Verkäufervolumen + Umlaufmenge + Die Gesamtzahl der Coins, die für den Handel verfügbar sind und auf dem Markt zirkulieren + Erfahrene Käufer + Nettokäufer mit der zusätzlichen Anforderung, mindestens 100 ausgehende Transaktionen zu haben + Vollständig verwässerte Bewertung + Der theoretische Gesamtwert einer Kryptowährung, wenn alle Coins, die existieren könnten, im Umlauf sind, einschließlich derjenigen, die derzeit nicht im Umlauf sind + Entstehungsdatum + Leer + Hoch + Inhaber/ Halter + Die Änderung der Anzahl der Token-Inhaber innerhalb eines bestimmten Zeitraums + Einblicke + Links + Liquidität + Die Änderung der Liquidität, die dem Token während des angegebenen Zeitraums zur Verfügung steht + Liquiditätsindex + Leer + Niedrig + Marktkapitalisierung + Der Gesamtmarktwert einer Kryptowährung, berechnet durch Multiplikation des aktuellen Preises der Münze mit der Gesamtzahl der im Umlauf befindlichen Münzen + Marktbewertung + Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung + Maximale Versorgung + Leer + Metriken + Offizielle Links + Preisleistung + Aufbewahrungsort + Sicherheitsbewertung + Leer + Soziales + Gesamtangebot + Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können + Handelsvolumen (24h) + Der Gesamtbetrag einer Kryptowährung, der innerhalb der letzten 24 Stunden gehandelt wurde, wobei das Aktivitäts- und Liquiditätsniveau auf dem Markt angegeben wird Du musst einen einzigen Zugangscode einrichten, um alle deine Karten zu schützen Schützen Du kannst später auf jeder Karte einen individuellen Zugangscode einrichten @@ -334,7 +429,7 @@ Jetzt sichern Scannen der Hauptkarte Weiter zu meiner Wallet - Abschließen der Sicherung + Backup abschließen Krypto empfangen Primärkarte scannen Für später überspringen @@ -344,7 +439,7 @@ Erstelle eine Wallet Andere Optionen Deine Schlüssel(private-keys) werden sicher im Inneren der Karte generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen. - Schlüssel privat generieren + Schlüssel anonym generieren Deine Karte ist aktiviert und einsatzbereit Erfolgreich! In diesem Fall musst du ganz von vorne anfangen. @@ -369,19 +464,20 @@ Seed-Phrase verwenden Ungültige Seed-Phrase. Bitte überprüfe die Wortreihenfolge. Ungültige Seed-Phrase. Bitte überprüfe die Rechtschreibung. - Altbestand + veralteter Standard Um zu überprüfen, ob du deine Seed-Phrase richtig aufgeschrieben hast, gib bitte das 2., 7. und 11 Wort ein. - Also, lass uns das überprüfen + Eine letzte Prüfung! Um den Sicherungsvorgang zu starten, füge bis zu zwei Sicherungskarten hinzu. Du kannst eine weitere Karte hinzufügen oder den Sicherungsvorgang abschließen Bereite die Sicherungskarte mit der Nummer %s vor. Scanne die primär-Karte, um den Sicherungsvorgang zu starten. Bereite die primäre Karte mit der Nummer %s vor. - Deine Wallet-Karte ist konfiguriert und einsatzbereit. + Deine Tangem-Karte ist konfiguriert und einsatzbereit. Maximale Anzahl an Karten hinzugefügt. Schließe den Sicherungsvorgang ab. Karte aktivieren Sicherungskarte Nr. %d Keine Sicherungskarten + Benachrichtigungen Eine Backup-Karte hinzugefügt Bereite deine Karte vor Zwei Backup-Karten hinzugefügt @@ -410,6 +506,7 @@ Kamerazugriff verweigert %1$s ( %2$s ) im %3$s Netzwerk Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. + QR-Code anzeigen oder Adresse teilen Teilnehmen Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. Die Informationen über das Empfehlungsprogramm konnten nicht geladen werden. Fehlercode: %s. Bitte versuche es später noch einmal. @@ -470,6 +567,8 @@ Dies sind die Kosten, die du für jede Gaseinheit zu zahlen bereit bist. Je höher der Gaspreis ist, desto schneller wird deine Transaktion bearbeitet. (Vorzugsgebühr inbegriffen) Prioritätsgebühr Die Gebühr, die ein Nutzer an Miner oder Validierer zahlen kann, um die Aufnahme seiner Transaktion in einen Block zu beschleunigen. + Die Gebühr, die für die Nutzung jeder nicht ausgegebenen Transaktionsausgabe (UTXO) im Kaspa-Netzwerk erforderlich ist. Je mehr UTXOs du in einer Transaktion verwenden, desto höher ist die Gebühr. + KAS per UTXO %1$s, %2$s Adresse Ziel-Tag @@ -530,31 +629,53 @@ Berühre an beliebiger Stelle für Änderungen Versende %s Du sendest **%1$s** inklusive der Netzwerkgebühr %2$s - Du sendest ** %1$s ** und %2$s + Du sendest **%1$s** und %2$s Senden %s Gesamt %1$s und %2$s werden gesendet ≈ %1$s (inkl. Gebühr: %2$s) %s wird gesendet Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert + %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. Ungültige Adresse %1$s (%2$s) Transaktion gesendet + Bereite das Scannen der Karte vor, die du einrichten möchtest. Entferne diese Wallet Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name + Aktiv + Um deine Kryptos zu unstaken, klick hier. + Die Anzahl der zu stakenden Krypros muss mindesten %s betragen + Effektiver Jahreszins + Jährliche prozentuale Rendite + Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Verfügbar Durchschnittliche Belohnungsquote %s geschätzter Profit + Marktbewertung + Metriken Mindestanforderungen Keine Belohnungen zu beanspruchen. Gestaked Belohnungen beanspruchen + Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden. Belohnungszeitplan + Dabei handelt es sich um einen Zeitplan, der festlegt, wann die Teilnehmer am Staking ihre Belohnungen erhalten. + Belohnungen, die du beanspruchen kannst: %s Staking %s Entbindungsdauer + Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase + Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Natives Staking + Mit Staking kannst du %1s verdienen. Deine Staking-Belohnungen kommen alle ~%2s Tage. + Verdiene Staking-Belohnungen Belohnungen + Mehr staken + unstaken + Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen + Validator/ Prüfer Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben Revolutionäre Hardware-Wallet Bis zu 3 physische Karten pro Wallet @@ -566,11 +687,14 @@ Lerne Tangem kennen Tausche, kaufen Sie NFTs, vergebe Kredite und tätige Einlagen bei mehr als 100 verschiedenen dezentralen Diensten Web 3.0-kompatibel + Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. + Neuer Swap-Anbieter verfügbar! Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. Der Betrag enthält die Gebühren des Dienstleisters. Gebühren Alle dezentralen Börsen benötigen Genehmigungen, um zu verhindern, dass intelligente Verträge ohne Ihre Erlaubnis auf Ihre Geldbörse zugreifen. Smart Contracts können nicht auf Ihre Token zugreifen, wenn Sie nicht zustimmen. Indem Sie Ihre Token \"freischalten\", ermächtigen Sie den 1-Zoll-Smart-Contract, sie auszugeben. Die Miner des Netzwerks erhalten eine (von Ihnen bezahlte) Gasgebühr, um diese Aktion in der Blockchain aufzuzeichnen. Sie können Ihre Token tauschen, nachdem Sie Ihre Zustimmung gegeben haben. Genehmigen + Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. Du wechselst Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. Unzureichende Mittel @@ -614,6 +738,7 @@ Operation von: %s zu: %s + Versuche es erneut Du hast dieselbe Karte gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte mit der Nummer %d scannen. Du hast die falsche Doppelkarte gescannt. Bitte versuche eine andere Karte Die, die du in den Hand hältst, und die andere mit der Nummer %s.\n\nBeide Karten können verwendet werden, um Geld aus dieser Wallet zu versenden. @@ -626,6 +751,9 @@ Diese Aktion ist unumkehrbar. Du hast keinen Zugriff mehr auf die alte Wallet. Tippe auf die Doppelkarte mit der Nummer %s und entferne sie erst am Ende des Vorgangs. Verwende %s oder scanne eine Karte, um Zugriff auf deine Wallet zu erhalten. + Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten + Sei der Erste, der von neuen Aktionen erfährt + Möchtest du Push-Benachrichtigungen verwenden? Neues Wallet hinzufügen Möchtest du diese Wallet wirklich löschen? Es ist ein Fehler aufgetreten, bitte scanne deine Karte, um sich anzumelden @@ -667,6 +795,7 @@ WalletConnect-Sitzungen Mit dApps verbinden WalletConnect + Das Herstellen der Verbindung kann einige Sekunden dauern %s Marktpreis letzte 24h %s Netzwerk @@ -675,12 +804,13 @@ Wallet-Einstellungen Tangem Verwende %s oder scanne eine Karte, um den Zugriff auf deine Wallet freizuschalten. + Es scheint, dass die Aktivierung der Karte nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. BNB Beacon Chain wird abgeschaltet Könnte besser sein Gefällt mir - OK, ich hab\'s! + OK, habe ich verstanden! Echt toll! Aktualisieren Du befindest sich derzeit im Demo-Modus diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a07a86a3ab..8a104e2052 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -87,6 +87,10 @@ Copier l\'adresse Créer Personnalisé + + %d jour + %d jours + Supprimer Désactivé Exécuté @@ -272,8 +276,8 @@ Pour continuer, accordez aux smart contracts de %1s l\'autorisation d\'utiliser votre %2s Donner l\'autorisation Illimité - Commander une carte - Scannez la carte + Commandez + Scannez Touchez, pour modifier le code d\'accès Touchez, pour modifier le mot de passe Pour créer le portefeuille, appuyez sur la carte comme indiqué ci-dessus et ne la retirez pas jusqu\'à la fin de l\'opération @@ -324,6 +328,7 @@ Ajouter au portfolio Mon portfolio Marché + Pour générer des adresses pour les réseaux sélectionnés, vous devez scanner votre carte Tangem. Sélectionnez un portefeuille Trier par Idées @@ -490,6 +495,8 @@ Il s\'agit du coût que vous êtes prêt à payer pour chaque unité de gaz. Plus le prix du gaz est élevé, plus votre transaction sera traitée rapidement. (Frais de priorité inclus) Frais de priorité Les frais qu\'un utilisateur peut payer aux mineurs ou aux validateurs pour accélérer l\'inclusion de leur transaction dans un bloc. + Les frais requis pour l\'utilisation de chaque sortie de transaction non dépensée (UTXO) dans le réseau Kaspa. Plus vous utilisez d’UTXO dans une transaction, plus les frais seront élevés. + KAS pour UTXO %1$s, %2$s Adresse Destination Tag @@ -581,7 +588,7 @@ Un moyen de recevoir des récompenses de staking. Il peut être réclamé automatiquement ou manuellement. Calendrier de récompenses Il s\'agit d\'un calendrier qui détermine le moment où les participants au staking reçoivent leurs récompenses. - Récompenses à réclamer : %s + Récompenses à réclamer: %s Staking %s Période de détachement La période que vous devez attendre après avoir demandé le retrait des fonds du staking avant que les jetons ne soient disponibles. @@ -667,6 +674,7 @@ Appuyez sur la carte jumelle avec le numéro %s et ne la retirez pas jusqu\'à la fin de l\'opération Utilisez %s ou scannez une carte pour avoir accès à votre portefeuille Restez à jour avec les dernières fonctionnalités et actualités + Soyez le premier informé des nouvelles promotions Souhaitez-vous utiliser les notifications push? Ajouter un nouveau portefeuille Êtes-vous sûr de vouloir supprimer ce portefeuille ? @@ -718,6 +726,7 @@ Paramètres du portefeuille Tangem Utilisez %s ou scannez une carte pour déverrouiller l\'accès à votre portefeuille + Il semble que l\'activation de la carte ne se soit pas déroulée correctement. Cela peut être dû à un problème avec le module NFC de votre appareil ou à une mauvaise connexion de la carte sur votre appareil. Veuillez contacter notre équipe de support pour obtenir de l’aide. Erreur d\'activation Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. BNB Beacon Chain va s\'arrêter de fonctionner diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e004bcbd28..a9ecf96c93 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -108,6 +108,7 @@ 速度と料金 アドレスを取得する プロバイダーへ移動 + トークンへ移動 インポート 後で ロックされています @@ -223,6 +224,8 @@ %sによる交換 返金を受けるには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる操作が失敗しました。 + OKXまたはブリッジのルールにより、取引金額は%1$sでウォレットに返金されました。%2$s + 金額は %1$s(%2$sネットワーク)で返金されました 確認するには、プロバイダーのウェブサイトにアクセスしてください。 プロバイダーによる本人確認手続きが必要です。 キャンセルされました @@ -613,7 +616,7 @@ 送金中... 変更するには任意の箇所をタップしてください %sを送金する - ** %1$s ** を送金する (ネットワーク手数料%2$sを含む) + **%1$s** を送金する (ネットワーク手数料%2$sを含む) **%1$s** と %2$s を送金しています。 %sを送信しています 合計 @@ -621,6 +624,7 @@ ≈ %1$s (%2$s: 手数料を含む) %sが送信されます 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 + %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 無効なアドレス %1$s ( %2$s ) 取引が送信されました @@ -788,6 +792,7 @@ ウォレット設定 Tangem %sを使用するか、カードをスキャンしてウォレットにアクセスしてください + カードのアクティベーションが正しく完了しませんでした。デバイスの NFCモジュールに問題があるか、カードをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 BNBビーコンチェーンは閉鎖されます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e67145ee33..8fef85b5aa 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -110,6 +110,7 @@ Скорость и комиссия Получить адреса К провайдеру + Перейти в токен Импортировать Позже Заблокирован @@ -124,6 +125,7 @@ Основная карта Парольная фраза Вставить + %1$s-%2$s Подробнее Получить Отклонить @@ -222,6 +224,8 @@ Обмен через %s Чтобы вернуть ваши деньги, посетите сайт провайдера Операция не выполнена провайдером + Отправленные средства были возвращены в %1$s на ваш кошелек в соответствии с правилами OKX или моста обмена. %2$s + Сумма была возвращена в %1$s (%2$s сети) Посетите сайт провайдера для проверки Провайдер запрашивает прохождение верификации Отменен @@ -326,6 +330,7 @@ Голосовать Выберите кошелек Кошелёк не поддерживает более одной сети + Чтобы создать адреса для выбранных сетей, необходимо отсканировать свой кошелек Tangem. Ссылки Метрики Вам необходимо установить единый код доступа для защиты всех ваших карт @@ -571,9 +576,11 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + Сумма для стейкинга должна быть не менее %s APY Годовой процентный доход, который вы можете получить от участия в стейкинге. Доступно + %s Способ возраграждения Способ получения вознаграждений за стейкинг. Он может быть автоматическим или ручным. Период возрагражения diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index c6e93b561a..320017ffd8 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -39,6 +39,7 @@ Забагато спроб Ви вимкнули біометричну автентифікацію на своєму телефоні і не зможете зберігати гаманці в додатку. Щоб зберегти гаманці, будь ласка, увімкніть функцію біометричної автентифікації в налаштуваннях телефону. Почніть процес резервного копіювання + З вашої фіатної картки або банківського рахунку %d картка %d картки @@ -83,6 +84,7 @@ Перейдіть до %1$s Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності Скасувати + Отримати винагороди Закрити Продовжити Копіювати @@ -112,6 +114,7 @@ Швидкість та комісія Отримати адреси Перейти до провайдера + Перейти до токену Імпортувати Пізніше Заблокований @@ -159,6 +162,7 @@ Я зрозумів Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно + Скасувати стейкінг Так Адреса контракту скопійована! Доступні мережі @@ -226,6 +230,8 @@ Обмін через %s Щоб повернути ваші кошти, відвідайте сайт провайдера Операція не виконана провайдером + Сума транзакції була повернута в %1$s на ваш гаманець відповідно до правил OKX або мосту обміну. %2$s + Сума була повернута в %1$s (%2$s мережі) Відвідайте сайт провайдера для перевірки Провайдер вимагає проходження KYC верифікації Скасовано @@ -246,6 +252,7 @@ Дані провайдера. Орієнтовна сума може бути змінена через ринкові умови. Статус обміну Потрібна верифікація + Очікування хешу транзакції Список токенів у вашому гаманцю Шукаємо найвигідніший курс... Плаваюча ставка @@ -259,10 +266,12 @@ Доступно від %s Недоступно для цієї пари Потрібен дозвіл + Рекомендовано Умовами використання Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s ID транзакції скопійовано + З іншої валюти у вашому гаманці Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. Розкажіть, яких функцій вам не вистачає, і ми спробуємо вам допомогти. Розкажіть, будь ласка, яку картку ви маєте? @@ -277,6 +286,8 @@ Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Вкажіть ліміт доступу для обраного токена Кількість %s + Функція підтвердження необхідна для надання дозволу іншій адресі на використання певної кількості ваших токенів. За задумом, смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту StakeKit використовувати їх. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви зможете застейкати свій токен після того, як дасте дозвіл. + Щоб продовжити, вам потрібно дозволити смарт-контракту StakeKit використовувати ваш %s Щоб продовжити, вам потрібно надати дозвіл смарт-контракту %1s використовувати ваш %2s Надати дозвіл Необмежено @@ -332,20 +343,73 @@ Щоб почати купувати, обмінювати або отримувати цей актив, додайте цей токен принаймні в 1 мережу Цей актив недоступний Додати в портфоліо + Додати токен + Доступні мережі Моє портфоліо Маркет + Щоб згенерувати адреси для обраних мереж, потрібно прикласти картку Tangem Не вдалося завантажити дані... + Швидкі дії Результат Переглянути токени до 100к ринкової капіталізації Показати токени Жодного результату + Виберіть мережу Оберіть гаманець + 1 міс. + 1 рік + 24 год. + 3 міс. + 6 міс. + 7 днів + Увесь + Досвідчені покупці + За рейтингом Сортувати за + Лідери росту + Лідери падіння + В тренді + Про %s + + На основі %d оцінки + На основі %d оцінок + На основі %d оцінок + На основі %d оцінок + + Блокчейн сайт + Давлення покупця + Різниця між обсягом покупців та обсягом продавців + Циркуляційний запас + Загальна кількість монет, які доступні для торгівлі та перебувають в обігу на ринку + Досвідчені покупці + Мережеві покупці з додатковою вимогою мати не менше 100 вихідних транзакцій + Повністю розведена ринкова капіталізація + Загальна теоретична вартість криптовалюти, якщо всі монети, які могли б існувати, перебувають в обігу, включаючи ті, що не перебувають в обігу в даний час + Дата створення + Високий + Тримачі + Зміна кількості власників токенів протягом певного періоду часу Інсайти Посилання + Ліквідність + Зміна того, скільки ліквідності доступно для токена протягом зазначеного періоду часу + Індекс ліквідності + Низький + Ринкова капіталізація + Загальна ринкова вартість криптовалюти, що розраховується шляхом множення поточної ціни монети на загальну кількість монет в обігу + Рейтинг ринку + Позиція в крипторейтингу між усіма монетами на основі ринкової капіталізації + Максимальна пропозиція Метрики + Офіційні посилання Цінова ефективність + Репозиторій Оцінка безпеки + Соцмережі + Загальна пропозиція + Максимальна кількість монет або токенів, яка може коли-небудь існувати для певної криптовалюти + Обсяг торгів (24г) + Загальна сума криптовалюти, якою торгували протягом останніх 24 годин, що вказує на рівень активності та ліквідності на ринку Вам потрібно встановити єдиний код доступу для захисту всіх ваших карток Захист Пізніше ви зможете налаштувати індивідуальний код доступу до кожної картки @@ -450,6 +514,7 @@ Доступ до камери заборонено %1$s (%2$s) у мережі %3$s Надсилайте лише %s на цю адресу. Надсилання будь-якої іншої валюти призведе до її незворотної втрати. + Покажіть QR-код або поділіться своєю адресою Взяти участь Не вдалося завантажити інформацію по реферальній програмі. Будь ласка, спробуйте пізніше. Не вдалося завантажити інформацію по реферальній програмі. Код помилки: %s. Будь ласка, спробуйте пізніше. @@ -575,14 +640,15 @@ Надсилання... Торкніться будь-якого поля, щоб змінити його Надіслати %s - Ви надсилаєте ** %1$s **, включно з комісію мережі %2$s - Ви надсилаєте ** %1$s ** і %2$s + Ви надсилаєте **%1$s**, включно з комісію мережі %2$s + Ви надсилаєте **%1$s** і %2$s Надсилання %s Всього %1$s та %2$s буде надіслано ≈ %1$s (вкл. комісію: %2$s ) %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час + %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron(TRX) на свій рахунок. Недійсна адреса %1$s (%2$s) Трансакцію надіслано @@ -614,6 +680,7 @@ Період, який ви повинні чекати після запиту на виведення коштів зі стейкінгу, перш ніж токени стануть доступними. Період блокування Відведений час для активації участі в стейкінгу. + Стейкінг %s Нативний стейкінг Стейкінг дозволяє заробляти %1s. Ваші винагороди за стейкінг надходять кожні ~%2s днів. Отримуйте винагороду за стейкінг @@ -640,6 +707,7 @@ Комісії Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл. Підтвердити + Помилка при розрахунку комісії. Будь ласка, надішліть відгук до служби підтримки. Ви обмінюєте Обмін цієї кількості обраних токенів призведе до значного впливу на ціну і зменшить вашу кінцеву суму. Недостатньо коштів @@ -697,6 +765,7 @@ Прикладіть twin-картку з номером %s та не прибирайте її до завершення операції Використовуйте %s або відскануйте картку, щоб отримати доступ до свого гаманця Будьте в курсі останніх функцій та новин + Дізнавайтеся першими про нові акції Бажаєте використовувати Push-повідомлення? Додати новий гаманець Ви впевнені, що хочете видалити цей гаманець? @@ -748,6 +817,7 @@ Налаштування гаманця Tangem Використовуйте %s або відскануйте картку, щоб розблокувати доступ до гаманця + Схоже, що активація картки була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки до пристрою. Зверніться за допомогою до нашої служби підтримки. Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. Відключення мережі BNB Beacon Chain diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 28f111ab8b..ae1e771935 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -4,6 +4,7 @@ 管理代幣 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 請求支持 + 再試一次 此功能不在展示模式中提供 原因:%s 無法發送交易 @@ -36,6 +37,7 @@ 安全模式 卡片設置 接受 + 允許 注意 餘額: %s 餘額 @@ -127,6 +129,9 @@ 反饋 Tangem反饋 無法發送交易 + 數量 %s + 要繼續,您需要允許 %1s 智能合約使用您的 %2s + 賦予權限 掃描卡片 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 要更改密碼,請完全按照上圖所示連接手機和卡 @@ -171,7 +176,6 @@ 這此情況,您必須要重新開始 您想要離開啟用程序嗎? 開始 - 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? 創建備份 閱讀更多關於助記詞的訊息 @@ -288,7 +292,7 @@ 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 隱藏 %s 隱藏代幣 - %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 + %1$s (%2$s) 代幣是 %3$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 無法隱藏 %s 您還沒有任何交易 無法加載交易 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 276eb46615..87ea9c3841 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -82,6 +82,7 @@ Go to %1$s You have not given access to your camera, please adjust your privacy settings Cancel + Claim rewards Close Continue Copy @@ -109,6 +110,7 @@ Speed and fee Get addresses Go to provider + Go to token Import Later Locked @@ -156,6 +158,7 @@ I understand There was an error. Please try again. Unreachable + Unstake Yes Contract address copied! Available networks @@ -223,6 +226,8 @@ Exchange by %s Visit provider’s website to refund your money Operation failed by provider + The transaction amount was refunded in %1$s to your wallet due to OKX or bridge rules. %2$s + The amount was refunded in %1$s (%2$s network) Visit provider’s website for verification KYC verification required by provider Canceled @@ -336,7 +341,7 @@ Available networks My portfolio Market - To generate addresses for selected networks, you need to attach a Tangem card + To generate addresses for selected networks, you must scan your Tangem Wallet card. Unable to load the data… Quick actions Result @@ -497,7 +502,7 @@ Camera access denied %1$s (%2$s) on %3$s network Send only %s to this address. Sending any other currency will result in its irreversible loss. - Show QR-code or share your address + Show a QR-code or share your address Participate Failed to load the information about the referral program. Please try again later. Failed to load the information about the referral program. Error code: %s. Please try again later. @@ -637,6 +642,7 @@ Name Active To unstake your assets, click here. + The amount to stake must be at least %s APR APY The annual percentage return you can earn from participating in staking. @@ -658,13 +664,15 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + Stake %s Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards Rewards Stake more - Unstacked + Unstaked Check unstaked to claim your assets + Unstaking Validator Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 7461386ac3..f14c204327 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -87,7 +87,7 @@ internal class ItemsBuilder @Inject constructor( DetailsItemUM.Basic.Item( id = "send_feedback", block = BlockUM( - text = resourceReference(R.string.details_send_feedback), + text = resourceReference(R.string.details_row_title_contact_to_support), iconRes = R.drawable.ic_comment_24, onClick = onClick, ), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index deb049021a..0e065183b2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -51,7 +51,7 @@ internal class ItemsBuilder @Inject constructor( if (isReferralAvailable) { BlockUM( - text = resourceReference(R.string.referral_title), + text = resourceReference(R.string.details_referral_title), iconRes = R.drawable.ic_add_friends_24, onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) }, ).let(::add) From d4a1d04023ad5a29ea24b69f88169bfbce8afd89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 10:46:36 +0500 Subject: [PATCH 201/223] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 13 +++++++++++++ core/res/src/main/res/values-fr/strings.xml | 11 +++++++++++ core/res/src/main/res/values-it/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 11 +++++++++++ core/res/src/main/res/values-ru/strings.xml | 11 +++++++++++ core/res/src/main/res/values-uk-rUA/strings.xml | 13 ++++++++++++- core/res/src/main/res/values-zh-rTW/strings.xml | 7 +++++++ core/res/src/main/res/values/strings.xml | 17 ++++++++++++++--- 8 files changed, 80 insertions(+), 4 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 12e30ed235..67cef4ccd7 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -13,6 +13,9 @@ Das gewählte unterstützt nicht das %1$s Netzwerk Um die kryptografische Verschlüsselung der %1$s Blockchain zu aktivieren, musst Du die Wallet auf die Werkseinstellungen zurücksetzen. Bitte heb vorher dein Guthaben ab, um sicherzustellen, dass du nichts verlierst, und führe dann den Reset-Vorgang durch. Nach dem Zurücksetzen ist der Zugriff auf die aktuelle Wallet nicht mehr möglich. Tokens im %1$s -Netzwerk werden von dieser Karte aufgrund einer Firmware-Einschränkung nicht unterstützt. + Vielen Dank für dein Feedback. Wir werden so schnell wie möglich antworten + Deine Vorschläge wurden übermittelt + Bitte versuche, die Karte genau wie in der Animation gezeigt anzutippen, oder lese unsere einfache Anleitung, oder forder Support an. Wenn das Problem weiterhin besteht, wende dich bitte an den Support. Hast du Probleme beim Scannen deiner Karte? Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet Standardgebühr @@ -28,6 +31,8 @@ Dunkel Hell Systemstandard + Wenn das System ausgewählt ist, passt sich die App automatisch an die Systemeinstellungen deines Geräts an + System Thema App Einstellungen Um deine Kontostände ein- oder auszublenden, flippe einfach das Display deines Geräts nach unten, oder schalte es in den Einstellungen aus. @@ -95,6 +100,7 @@ Entfernen Deaktiviert + Trennen Erledigt Aktivieren Aktiviert @@ -131,6 +137,7 @@ Ablehnen Neu laden Umbenennen + Wiederholen Speichern Änderungen speichern Suchen @@ -159,6 +166,7 @@ Es ist ein Fehler aufgetreten. Bitte versuche es erneut. Nicht erreichbar staking beenden + Warnung Ja Vertragsadresse kopiert! Verfügbare Netzwerke @@ -206,6 +214,7 @@ Flipp um Guthaben auszublenden Aussteller Signiert + Wenn du den Code vergisst, verlierst du den Zugriff auf dein Geld. Eine Codewiederherstellung ist nicht möglich. Gib uns eine Rückmeldung Details Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk @@ -572,6 +581,7 @@ %1$s, %2$s Adresse Ziel-Tag + Möchtest du den Sendebildschirm wirklich schließen? Adresse eingeben Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein Ungültiges Tag. Es wird der Transaktion nicht hinzugefügt. @@ -668,6 +678,7 @@ Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Stake %s Natives Staking Mit Staking kannst du %1s verdienen. Deine Staking-Belohnungen kommen alle ~%2s Tage. Verdiene Staking-Belohnungen @@ -675,6 +686,7 @@ Mehr staken unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen + Staking beenden Validator/ Prüfer Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben Revolutionäre Hardware-Wallet @@ -716,6 +728,7 @@ Die Geldsendung wird verfügbar, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. Der Verkauf von %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. Staking %s ist aktuell nicht verfügbar. Prüfe bitte ob es ein neues Update gibt. + Adresse wählen Generiere XPUB Ausblenden Du bist dabei, dieses Token vom Hauptbildschirm auszublenden. Du kannst es jederzeit über die Seite „Token verwalten“ wieder hinzufügen. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 8a104e2052..30baffe42f 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -13,6 +13,9 @@ Le sélectionné ne prend pas en charge le réseau %1$s Pour activer le cryptage de la blockchain %1$s, vous devrez réinitialiser le portefeuille aux paramètres d\'usine. Veuillez retirer vos fonds avant de le faire pour vous assurer de ne pas les perdre, puis terminez le processus de réinitialisation. L\'accès au portefeuille actuel ne sera pas possible après la réinitialisation. Les jetons du réseau %1$s ne sont pas pris en charge par cette carte en raison d\'une limitation du micrologiciel. + Merci pour votre retour. Nous vous répondrons dès que possible + Vos suggestions ont été envoyées + Veuillez essayer d\'appuyer sur la carte exactement comme indiqué dans l\'animation ou lire notre guide simple, ou demander de l\'aide. Si le problème persiste, veuillez demander de l\'aide. Avez-vous des difficultés à scanner votre carte ? Cette carte n\'est pas conçue pour fonctionner avec Tangem Frais par défaut @@ -28,6 +31,8 @@ Sombre Clair Par défaut du système + Si le système est sélectionné, l\'application s\'ajustera automatiquement en fonction des paramètres système de votre appareil + Système Thème Paramètres de l\'application Pour masquer ou afficher vos soldes, il suffit de retourner l\'écran de votre appareil vers le bas ou de le désactiver dans les paramètres @@ -93,6 +98,7 @@ Supprimer Désactivé + Se déconnecter Exécuté Activer Activé @@ -128,6 +134,7 @@ Rejeter Recharger Renommer + Réessayer Enregistrer Sauvegarder les modifications Rechercher @@ -155,6 +162,7 @@ Je comprends Il y avait une erreur. Veuillez réessayer. Inaccessible + Alerte Oui Adresse du contrat copiée ! Réseaux disponibles @@ -202,6 +210,7 @@ Retourner pour masquer les soldes Emetteur Signé + Si vous oubliez le code, vous perdrez l\'accès à vos fonds. La récupération du code n\'est pas possible. Envoyer un commentaire Référénces Vérifiez votre connexion Internet ou passez à un réseau différent @@ -500,6 +509,7 @@ %1$s, %2$s Adresse Destination Tag + Êtes-vous sûr de vouloir fermer l\'écran d\'envoi ? Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille Tag invalide. Il ne sera pas ajouté à la transaction. @@ -639,6 +649,7 @@ L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées. La vente de %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour. Le staking %s n’est pas disponible pour le moment. Veuillez consulter nos mises à jour. + Choisissez une adresse Générer XPUB Masquer Vous êtes sur le point de masquer ce jeton de l\'écran principal. Vous pouvez le rajouter à tout moment via la page de gestion des jetons. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index e7caa7829e..b8860bd7d8 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -14,6 +14,7 @@ Mantieni le modifiche Invia Con successo + Avviso Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a9ecf96c93..c0913ede01 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -13,6 +13,9 @@ 選択したものは%1$sネットワークをサポートしていません。 %1$sブロックチェーンの暗号化を有効にするには、ウォレットを工場出荷時の設定にリセットする必要があります。リセットする前に出金して、資金が失われないようにしてから、リセット処理を完了してください。リセット後は、現在のウォレットにアクセスできなくなります。 %1$s ネットワークのトークンは、ファームウェアの制限により、このカードではサポートされていません。 + ご意見ありがとうございます。できるだけ早くご返信いたします。 + あなたの提案が送信されました + アニメーションに表示されているとおりにカードをタップするか、簡単なガイドをお読みください。それでも問題が解決しない場合は、サポートをご依頼ください。 カードのスキャンに問題がありますか? このカードはこのアプリでは使用できません。 デフォルト手数料 @@ -28,6 +31,8 @@ ダーク ライト システムのデフォルト + システムを選択した場合、アプリはデバイスのシステム設定に基づいて自動調整されます。 + システム テーマ アプリ設定 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 @@ -93,6 +98,7 @@ 削除 無効 + 切断 完了 有効にする 有効 @@ -129,6 +135,7 @@ 拒否 リロード 名前を変更 + リトライ 保存 変更内容を保存 検索 @@ -157,6 +164,7 @@ エラーが発生しました。もう一度お試しください。 アクセスできません ステーキング解除 + 警告 はい コントラクトアドレスをコピーしました! 利用可能なネットワーク @@ -204,6 +212,7 @@ フリップして残高を非表示にする 発行者 署名済み + コードを忘れた場合、資金にアクセスできなくなります。コードの回復は不可能です。 フィードバックを送信 詳細 インターネット接続を確認するか、別のネットワークに切り替えてください。 @@ -560,6 +569,7 @@ %1$s 、 %2$s アドレス 宛先タグ + 送金画面を閉じてもよろしいですか? アドレスを入力 アドレスはウォレットアドレスと同じです 無効なタグです。取引には追加されません。 @@ -704,6 +714,7 @@ ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 現在、 %sの売却はご利用いただけません。アップデート情報をご確認ください。 %s のステーキングは現在ご利用いただけません。最新情報をご確認ください。 + アドレスを選択 XPUBを生成する 非表示 このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8fef85b5aa..741704103a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -13,6 +13,9 @@ Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. + Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки. + Ваши предложения отправлены + Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, либо прочтите руководство по сканированию. Если проблема осталась, то запросите поддержку. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. @@ -27,6 +30,8 @@ Тёмная Светлая Как в системе + При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства + Системная Тема Настройки приложения Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" @@ -95,6 +100,7 @@ Удалить Отключено + Отключить Готово Включить Включено @@ -131,6 +137,7 @@ Отклонить Перезагрузить Переименовать + Повторить Сохранить Сохранить изменения Искать @@ -157,6 +164,7 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно + Предупреждение Да Адрес контракта скопирован! Доступные сети @@ -204,6 +212,7 @@ Скрывать балансы жестом переворота Эмитент Подписано + Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно. Отправить отзыв Подробности Проверьте подключение с интернетом или переключитесь на другую сеть @@ -505,6 +514,7 @@ %1$s, %2$s Адрес Код назначения + Вы действительно хотите закрыть экран отправки транзакции? Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. @@ -629,6 +639,7 @@ Отправка средств станет доступной после завершения транзакции(-ий) в сети %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями. + Выберите адрес Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 320017ffd8..1c150d0e88 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -13,6 +13,9 @@ Обраний гаманець не підтримує мережу %1$s Щоб активувати криптографічне шифрування блокчейну %1$s, вам потрібно скинути налаштування гаманця до заводських. Зніміть свої кошти перед цим, щоб переконатися, що ви їх не втратите, а потім завершіть процес скидання. Вхід до поточного гаманця буде неможливий після скидання. Токени в мережі %1$s не підтримуються цією карткою через обмеження прошивки. + Дякуємо за ваш відгук. Ми відповімо якнайшвидше. + Ваші пропозиції надіслано + Будь ласка, спробуйте прикласти картку точно так, як показано на анімації, або прочитайте наш простий посібник. Якщо проблема залишилася, зверніться до служби підтримки. У вас виникли труднощі зі скануванням картки? Ця картка не призначена для роботи з цим додатком Комісія за замовчуванням @@ -28,6 +31,8 @@ Темна Світла Системна + При виборі \"Системна\" застосунок буде використовувати тему відповідно до налаштувань вашого пристрою + Системна Тема Налаштування застосунку Щоб приховати або показати свій баланс, просто переверніть екран пристрою вниз або вимкніть його в налаштуваннях @@ -99,6 +104,7 @@ Видалити Вимкнуто + Від\'єднати Готово Увімкнути Увімкнено @@ -135,6 +141,7 @@ Відхилити Перезавантажити Перейменувати + Повторити Зберегти Зберегти зміни Шукати @@ -163,6 +170,7 @@ Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно Скасувати стейкінг + Увага Так Адреса контракту скопійована! Доступні мережі @@ -210,6 +218,7 @@ Приховувати баланси жестом перевороту Емітент Підписано + Якщо ви забудете код, ви втратите доступ до своїх коштів. Відновлення коду неможливе. Надіслати відгук Деталі Перевірте підключення до інтернету або змініть мережу @@ -584,6 +593,7 @@ %1$s, %2$s Адреса Тег призначення + Ви дійсно хочете закрити екран надсилання транзакції? Введіть адресу Адреса збігається з адресою гаманця Недопустимий Tag. Він не буде доданий у транзакцію. @@ -648,7 +658,7 @@ ≈ %1$s (вкл. комісію: %2$s ) %s буде надіслано Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час - %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron(TRX) на свій рахунок. + %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. Недійсна адреса %1$s (%2$s) Трансакцію надіслано @@ -729,6 +739,7 @@ Надсилання коштів стане доступним після завершення транзакції(-ій) в мережі %s Наразі продаж %s недоступний. Слідкуйте за нашими оновленнями. Стейкінг %s зараз недоступний. Будь ласка, слідкуйте за нашими оновленнями. + Оберіть адресу Згенерувати XPUB Приховати Ви збираєтеся приховати цей токен з головного екрану. Ви можете додати його назад будь-коли на сторінці керування токенами. diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index ae1e771935..b198f3eee7 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -9,6 +9,9 @@ 原因:%s 無法發送交易 此卡不支持%1$s網路上的代幣因為韌體限制 + 感謝您的反饋。我們會盡快回复 + 你的建議已送出 + 請嘗試完全按照動畫中顯示的方式點擊卡片或請求支持 有困難在掃描卡上嗎? 此卡不適用於此app 轉到設置以在 Tangem App 中啟用生物識別身份驗證 @@ -53,6 +56,7 @@ 創造 刪除 禁用 + 斷開連接 完成 允許 啟用 @@ -65,6 +69,7 @@ 主卡片 拒絕 重新命名 + 重試 保存設置 搜索 搜尋代幣 @@ -83,6 +88,7 @@ 交易 我了解 無法觸達 + 警告 已複製代幣地址 支持的網路 @@ -115,6 +121,7 @@ App Currency 發行人 簽署 + 如果您忘記密碼,您將無法使用您的資金。無法恢復代碼 更多 檢查您的網路連接或切換到其他網絡 服務條款 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 87ea9c3841..46299409f1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -13,6 +13,9 @@ The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. + Thank you for your feedback. We will respond as soon as possible + Your suggestions were sent + Please try to tap the card exactly as shown in the animation or read our simple guide, or request support. If the problem persists, please request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -28,6 +31,8 @@ Dark Light System default + If system is selected, the app will auto-adjust based on your device\'s system settings + System Theme App settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -39,7 +44,7 @@ Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. Start backup process - From your fiat card or bank account + With your bank card or bank account %d card %d cards @@ -95,6 +100,7 @@ Delete Disabled + Disconnect Done Enable Enabled @@ -131,6 +137,7 @@ Reject Reload Rename + Retry Save Save changes Search @@ -159,6 +166,7 @@ There was an error. Please try again. Unreachable Unstake + Warning Yes Contract address copied! Available networks @@ -206,6 +214,7 @@ Flip-to-Hide Balances Issuer Signed + If you forget the code you will lose access to your funds. Code recovery is not possible. Send feedback Details Check your internet connection or switch to a different network @@ -267,7 +276,7 @@ No tokens found. Please try another request ID: %s Transaction ID copied - From another currency in your wallet + With another currency in your wallet The following information is optional. You can erase it if you don\'t want to share it. Tell us what functions you are missing, and we will try to help you. Please tell us what card do you have @@ -568,6 +577,7 @@ %1$s, %2$s Address Destination Tag + Are you sure you want to close the send screen? Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. @@ -632,7 +642,7 @@ ≈ %1$s (inc. fee: %2$s) %s will be sent Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while - %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron(TRX) in your account. + %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. Invalid address %1$s (%2$s) Transaction sent @@ -714,6 +724,7 @@ Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. Staking %s is not available at the moment. Please check our updates. + Choose address Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. From 33764123ba248cdbef8c353a4105f29aa9ebffb1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 10:47:34 +0500 Subject: [PATCH 202/223] Updated on 2026-08-14 --- .../tangem/core/ui/utils/RequestPushPermission.kt | 9 ++++----- .../wallet/viewmodels/WalletViewModel.kt | 14 ++++++++------ .../intents/WalletPushPermissionClickIntents.kt | 14 ++++++++++---- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt index 575485aae9..cac63e19f7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt @@ -26,11 +26,10 @@ fun requestPushPermission( val permissionState = pushPermission?.let { permission -> val tempPermissionState = rememberPermissionState(permission = permission) rememberPermissionState(permission = permission) { - when { - it -> onAllow() - !tempPermissionState.status.shouldShowRationale && !isFirstTimeAsking -> onOpenSettings() - else -> onDeny() - } + val isGranted = tempPermissionState.status.isGranted + val shouldShowRationale = tempPermissionState.status.shouldShowRationale + + if (isGranted && !shouldShowRationale && !isFirstTimeAsking) onOpenSettings() } } 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 3a9681b918..ed7f027de5 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 @@ -159,22 +159,24 @@ internal class WalletViewModel @Inject constructor( val isFirstTimeRequested = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } - val onRequestLater: () -> Unit = if (wasInitiallyAsk) { - clickIntents::onDelayAskPushPermission - } else { - clickIntents::onNeverAskPushPermission + val onRequestLater: (Boolean) -> Unit = { isUserDismissed -> + if (wasInitiallyAsk) { + clickIntents.onDelayAskPushPermission(isUserDismissed) + } else { + clickIntents.onNeverAskPushPermission(isUserDismissed) + } } stateHolder.showBottomSheet( content = PushNotificationsBottomSheetConfig( isFirstTimeRequested = isFirstTimeRequested, wasInitiallyAsk = wasInitiallyAsk, onRequest = clickIntents::onRequestPushPermission, - onRequestLater = onRequestLater, + onRequestLater = { onRequestLater(false) }, onAllow = clickIntents::onAllowPushPermission, onDeny = clickIntents::onDenyPushPermission, openSettings = settingsManager::openSettings, ), - onDismiss = onRequestLater, + onDismiss = { onRequestLater(true) }, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index fe8848daf5..62fec7ce4f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -15,9 +15,9 @@ internal interface WalletPushPermissionClickIntents { fun onRequestPushPermission() - fun onDelayAskPushPermission() + fun onDelayAskPushPermission(isUserDismissed: Boolean) - fun onNeverAskPushPermission() + fun onNeverAskPushPermission(isUserDismissed: Boolean) fun onDenyPushPermission() @@ -32,7 +32,9 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { + private var isUserDismissedDialog: Boolean = true override fun onRequestPushPermission() { + isUserDismissedDialog = false analyticsEventHandler.send( PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Main), ) @@ -41,7 +43,9 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( } } - override fun onDelayAskPushPermission() { + override fun onDelayAskPushPermission(isUserDismissed: Boolean) { + if (!isUserDismissedDialog) return + isUserDismissedDialog = isUserDismissed viewModelScope.launch { analyticsEventHandler.send( PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Main), @@ -50,7 +54,9 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( } } - override fun onNeverAskPushPermission() { + override fun onNeverAskPushPermission(isUserDismissed: Boolean) { + if (!isUserDismissedDialog) return + isUserDismissedDialog = isUserDismissed viewModelScope.launch { analyticsEventHandler.send(PushNotificationAnalyticEvents.ButtonCancel) neverRequestPermissionUseCase(PUSH_PERMISSION) From e877fabd933f09d1991d44054bedc5396a3ebc45 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 10:48:08 +0500 Subject: [PATCH 203/223] Updated on 2026-08-14 --- .../main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 778d4f08d8..e5d94f85b7 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 @@ -37,7 +37,7 @@ object BigDecimalFormatter { maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) minimumFractionDigits = 2 isGroupingUsed = true - roundingMode = RoundingMode.DOWN + roundingMode = RoundingMode.HALF_UP } return formatter.format(cryptoAmount).let { From 6c6ce3d05efe8394c10c49de9122f1f27b907803 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 13:49:32 +0500 Subject: [PATCH 204/223] Updated on 2026-08-14 --- .../repository/DefaultCurrenciesRepository.kt | 15 ++------------- .../repository/paging/TxHistoryPagingSource.kt | 1 + gradle/dependencies.toml | 2 +- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index dcfd66e7a7..d1a7d70fcf 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -61,18 +61,6 @@ internal class DefaultCurrenciesRepository( private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( value = emptyMap(), ) - private val parallelTransactionsEnabledBlockchains = setOf( - Blockchain.Ethereum, - Blockchain.EthereumTestnet, - Blockchain.Polygon, - Blockchain.PolygonTestnet, - Blockchain.Arbitrum, - Blockchain.ArbitrumTestnet, - Blockchain.Binance, - Blockchain.BinanceTestnet, - Blockchain.Tron, - Blockchain.TronTestnet, - ) override suspend fun saveTokens( userWalletId: UserWalletId, @@ -391,7 +379,8 @@ internal class DefaultCurrenciesRepository( val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } outgoingTransactions.isNotEmpty() } - parallelTransactionsEnabledBlockchains.contains(blockchain) -> false + blockchain.isEvm() -> false + blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet -> false else -> coinStatus?.value?.hasCurrentNetworkTransactions == true } } 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 index d534e238ec..87d2138b87 100644 --- 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 @@ -85,6 +85,7 @@ internal class TxHistoryPagingSource( currency = sourceParams.currency, ) .filterUnconfirmedTransaction() + .sortedByDescending { it.timestampInMillis } .filterIfTxAlreadyAdded(apiItems = items) return if (recentItems.isEmpty()) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 74fab9ccbd..061ea006d3 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.13-707" +tangemBlockchainSdk = "release-app_5.13-714" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.13-376" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 8c8c0ada458c7697809ebc9227414562e1bfbd49 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jul 2024 11:37:27 +0300 Subject: [PATCH 205/223] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 11 +++++--- .../wallets/usecase/RenameWalletUseCase.kt | 25 ++++++++++++------- .../impl/DefaultRenameWalletComponent.kt | 10 ++++---- .../preview/PreviewRenameWalletComponent.kt | 2 +- .../walletsettings/entity/RenameWalletUM.kt | 2 +- .../walletsettings/ui/RenameWalletDialog.kt | 2 +- 6 files changed, 31 insertions(+), 21 deletions(-) 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 3dfc085c10..cc9b89e775 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,12 +1,12 @@ package com.tangem.tap.di.domain import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* @@ -95,8 +95,11 @@ internal object WalletsDomainModule { @Provides @Singleton - fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase { - return RenameWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesRenameWalletUseCase( + userWalletsListManager: UserWalletsListManager, + dispatchers: CoroutineDispatcherProvider, + ): RenameWalletUseCase { + return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers) } @Provides diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt index 4206d4854f..ff568c6794 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/RenameWalletUseCase.kt @@ -8,25 +8,32 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext /** * Use case for rename user wallet * * @property userWalletsListManager user wallets list manager */ -class RenameWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class RenameWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val dispatchers: CoroutineDispatcherProvider, +) { suspend operator fun invoke(userWalletId: UserWalletId, name: String): Either = - either { - val existingNames = userWalletsListManager.userWalletsSync + withContext(dispatchers.io) { + either { + val existingNames = userWalletsListManager.userWalletsSync - ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { - UpdateWalletError.NameAlreadyExists - } + ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } - when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data + when (val result = userWalletsListManager.update(userWalletId) { it.copy(name = name) }) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt index 15ab597e72..902d26f1ef 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -34,8 +34,8 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( private val stateFlow: MutableStateFlow = MutableStateFlow( value = RenameWalletUM( walletNameValue = TextFieldValue(text = params.currentName), - isNameCorrect = false, updateValue = ::updateValue, + isConfirmEnabled = false, onConfirm = { renameWallet(params.userWalletId) }, ), ) @@ -58,12 +58,14 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( stateFlow.update { it.copy( walletNameValue = value, - isNameCorrect = value.text.isNotBlank() && value.text != currentWalletName, + isConfirmEnabled = value.text.isNotBlank() && value.text != currentWalletName, ) } } private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { + stateFlow.update { it.copy(isConfirmEnabled = false) } + val newName = stateFlow.value.walletNameValue val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() @@ -71,9 +73,7 @@ internal class DefaultRenameWalletComponent @AssistedInject constructor( Timber.e("Unable to rename wallet: $maybeError") val message = when (maybeError) { - is UpdateWalletError.DataError -> resourceReference( - id = R.string.common_unknown_error, - ) + is UpdateWalletError.DataError -> resourceReference(id = R.string.common_unknown_error) is UpdateWalletError.NameAlreadyExists -> resourceReference( id = R.string.user_wallet_list_rename_popup_error_already_exists, formatArgs = wrappedList(newName), diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt index ff42b59689..fbcb3d6389 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt @@ -10,7 +10,7 @@ internal class PreviewRenameWalletComponent : RenameWalletComponent { private val previewState = RenameWalletUM( walletNameValue = TextFieldValue(text = "My Wallet"), - isNameCorrect = false, + isConfirmEnabled = false, updateValue = {}, onConfirm = {}, ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt index 4dd5b8c7a7..730cc1ee60 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt @@ -6,7 +6,7 @@ import androidx.compose.ui.text.input.TextFieldValue @Immutable internal data class RenameWalletUM( val walletNameValue: TextFieldValue, - val isNameCorrect: Boolean, val updateValue: (value: TextFieldValue) -> Unit, + val isConfirmEnabled: Boolean, val onConfirm: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt index fa02de24c9..5ff04fe2e7 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -23,7 +23,7 @@ internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { fieldValue = value, confirmButton = DialogButton( title = stringResource(id = R.string.common_ok), - enabled = model.isNameCorrect, + enabled = model.isConfirmEnabled, onClick = model.onConfirm, ), dismissButton = DialogButton( From af822f9d7a16eb05baa83233a7ec64bbcf4dbc17 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 12:57:35 +0300 Subject: [PATCH 206/223] Updated on 2026-08-14 --- .../analytics/DefaultAnalyticsContextProxy.kt | 31 +++++++++++ .../tap/di/analytics/AnalyticsModule.kt | 6 +++ .../details/ui/details/DetailsScreenState.kt | 2 +- app/src/main/res/drawable/ic_more_cards.xml | 5 -- core/analytics/build.gradle.kts | 1 + .../analytics/utils/AnalyticsContextProxy.kt | 17 +++++++ .../main/res/drawable/ic_more_cards_24.xml | 11 ++++ .../wallet-settings/impl/build.gradle.kts | 3 ++ .../walletsettings/analytics/Settings.kt | 13 +++++ .../preview/PreviewWalletSettingsComponent.kt | 2 + .../model/WalletSettingsModel.kt | 29 +++++++++++ .../walletsettings/utils/ItemsBuilder.kt | 51 ++++++++++++------- 12 files changed, 147 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt delete mode 100644 app/src/main/res/drawable/ic_more_cards.xml create mode 100644 core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt create mode 100644 core/ui/src/main/res/drawable/ic_more_cards_24.xml create mode 100644 features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt new file mode 100644 index 0000000000..d1aecc29d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.common.analytics + +import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.utils.AnalyticsContextProxy +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.addContext +import com.tangem.tap.common.extensions.eraseContext +import com.tangem.tap.common.extensions.removeContext +import com.tangem.tap.common.extensions.setContext + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy { + + override fun setContext(scanResponse: ScanResponse) { + Analytics.setContext(scanResponse) + } + + override fun eraseContext() { + Analytics.eraseContext() + } + + override fun addContext(scanResponse: ScanResponse) { + Analytics.addContext(scanResponse) + } + + override fun removeContext() { + Analytics.removeContext() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt index 34dbbd2e11..c687d63c10 100644 --- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt @@ -1,6 +1,8 @@ package com.tangem.tap.di.analytics +import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase +import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase import dagger.Module import dagger.Provides @@ -17,4 +19,8 @@ internal object AnalyticsModule { fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase { return DefaultChangeCardAnalyticsContextUseCase() } + + @Provides + @Singleton + fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index b35c17a5fd..acafefe8e7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -60,7 +60,7 @@ internal sealed class SettingsItem( data class LinkMoreCards( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_more_cards, + iconResId = R.drawable.ic_more_cards_24, title = resourceReference(R.string.details_row_title_create_backup), ) diff --git a/app/src/main/res/drawable/ic_more_cards.xml b/app/src/main/res/drawable/ic_more_cards.xml deleted file mode 100644 index 0cc559021b..0000000000 --- a/app/src/main/res/drawable/ic_more_cards.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index 9d05970191..c61c8e7af2 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Domain */ implementation(projects.domain.analytics) + implementation(projects.domain.models) /** Other */ implementation(deps.kotlin.coroutines) diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt b/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt new file mode 100644 index 0000000000..cab4da6a7b --- /dev/null +++ b/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt @@ -0,0 +1,17 @@ +package com.tangem.core.analytics.utils + +import com.tangem.domain.models.scan.ScanResponse + +/** +[REDACTED_AUTHOR] + */ +interface AnalyticsContextProxy { + + fun setContext(scanResponse: ScanResponse) + + fun eraseContext() + + fun addContext(scanResponse: ScanResponse) + + fun removeContext() +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_more_cards_24.xml b/core/ui/src/main/res/drawable/ic_more_cards_24.xml new file mode 100644 index 0000000000..1fd80fa554 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_more_cards_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 8c61fc867a..14885a740b 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -21,11 +21,13 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.common.routing) /* Project - Domain */ implementation(projects.domain.legacy) + implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) @@ -50,4 +52,5 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.reKotlin) } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt new file mode 100644 index 0000000000..9a655d973a --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.walletsettings.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal sealed class Settings( + category: String = "Settings", + event: String, + params: Map = mapOf(), + error: Throwable? = null, +) : AnalyticsEvent(category, event, params, error) { + + class ButtonCreateBackup : Settings(event = "Button - Create Backup") +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt index 7649ef9bd7..4dba365556 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -19,8 +19,10 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent { userWalletId = UserWalletId("011"), userWalletName = "My Wallet", isReferralAvailable = true, + isLinkMoreCardsAvailable = true, renameWallet = {}, forgetWallet = {}, + onLinkMoreCardsClick = {}, ), ) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index e066301748..a9e5b42932 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -5,6 +5,8 @@ import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.utils.AnalyticsContextProxy import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -16,9 +18,14 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.ContentMessage import com.tangem.core.ui.message.SnackbarMessage 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.redux.LegacyAction +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.component.WalletSettingsComponent import com.tangem.feature.walletsettings.entity.DialogConfig import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM @@ -43,6 +50,9 @@ internal class WalletSettingsModel @Inject constructor( private val deleteWalletUseCase: DeleteWalletUseCase, private val itemsBuilder: ItemsBuilder, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsContextProxy: AnalyticsContextProxy, + private val reduxStateHolder: ReduxStateHolder, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -75,6 +85,7 @@ internal class WalletSettingsModel @Inject constructor( userWalletId = userWallet.walletId, userWalletName = userWallet.name, isReferralAvailable = userWallet.cardTypesResolver.isTangemWallet(), + isLinkMoreCardsAvailable = userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, forgetWallet = { messageSender.send( @@ -98,6 +109,9 @@ internal class WalletSettingsModel @Inject constructor( }, ) }, + onLinkMoreCardsClick = { + onLinkMoreCardsClick(scanResponse = userWallet.scanResponse) + }, ) private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { @@ -126,4 +140,19 @@ internal class WalletSettingsModel @Inject constructor( router.replaceAll(AppRoute.Home) } } + + private fun onLinkMoreCardsClick(scanResponse: ScanResponse) { + analyticsEventHandler.send(Settings.ButtonCreateBackup()) + + analyticsContextProxy.addContext(scanResponse) + + reduxStateHolder.dispatch( + LegacyAction.StartOnboardingProcess( + scanResponse = scanResponse, + canSkipBackup = false, + ), + ) + + router.push(AppRoute.OnboardingWallet()) + } } \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 0e065183b2..941f1c53f2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -19,15 +19,18 @@ internal class ItemsBuilder @Inject constructor( private val router: Router, ) { + @Suppress("LongParameterList") fun buildItems( userWalletId: UserWalletId, userWalletName: String, + isLinkMoreCardsAvailable: Boolean, isReferralAvailable: Boolean, forgetWallet: () -> Unit, renameWallet: () -> Unit, + onLinkMoreCardsClick: () -> Unit, ): PersistentList = persistentListOf( buildNameItem(userWalletName, renameWallet), - buildCardItem(userWalletId, isReferralAvailable), + buildCardItem(userWalletId, isLinkMoreCardsAvailable, isReferralAvailable, onLinkMoreCardsClick), buildForgetItem(forgetWallet), ) @@ -38,26 +41,38 @@ internal class ItemsBuilder @Inject constructor( onClick = renameWallet, ) - private fun buildCardItem(userWalletId: UserWalletId, isReferralAvailable: Boolean) = - WalletSettingsItemUM.WithItems( - id = "card", - description = resourceReference(R.string.settings_card_settings_footer), - blocks = buildList { + private fun buildCardItem( + userWalletId: UserWalletId, + isLinkMoreCardsAvailable: Boolean, + isReferralAvailable: Boolean, + onLinkMoreCardsClick: () -> Unit, + ) = WalletSettingsItemUM.WithItems( + id = "card", + description = resourceReference(R.string.settings_card_settings_footer), + blocks = buildList { + if (isLinkMoreCardsAvailable) { BlockUM( - text = resourceReference(R.string.card_settings_title), - iconRes = R.drawable.ic_card_settings_24, - onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, + text = resourceReference(R.string.details_row_title_create_backup), + iconRes = R.drawable.ic_more_cards_24, + onClick = onLinkMoreCardsClick, ).let(::add) + } - if (isReferralAvailable) { - BlockUM( - text = resourceReference(R.string.details_referral_title), - iconRes = R.drawable.ic_add_friends_24, - onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) }, - ).let(::add) - } - }.toImmutableList(), - ) + BlockUM( + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings(userWalletId)) }, + ).let(::add) + + if (isReferralAvailable) { + BlockUM( + text = resourceReference(R.string.details_referral_title), + iconRes = R.drawable.ic_add_friends_24, + onClick = { router.push(AppRoute.ReferralProgram(userWalletId)) }, + ).let(::add) + } + }.toImmutableList(), + ) private fun buildForgetItem(forgetWallet: () -> Unit) = WalletSettingsItemUM.WithItems( id = "forget", From ac29785733af4af7fd11083477ee6850082654ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jul 2024 23:56:55 +0400 Subject: [PATCH 207/223] Updated on 2026-08-14 --- .../domain/scanCard/LegacyScanProcessor.kt | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) 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 0bbdc3f5f4..ea7c4ac652 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 @@ -32,6 +32,7 @@ import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext internal object LegacyScanProcessor { @@ -115,24 +116,28 @@ internal object LegacyScanProcessor { } else { scope.launch { delay(DELAY_SDK_DIALOG_CLOSE) - disclaimerWillShow() - store.dispatchWithMain( - DisclaimerAction.Show( - from = DisclaimerSource.Home, - callback = DisclaimerCallback( - onAccept = { - scope.launch(Dispatchers.Main) { - nextHandler(scanResponse) - } - }, - onDismiss = { - scope.launch(Dispatchers.Main) { - onFailure(TangemSdkError.UserCancelled()) - } - }, + + withContext(Dispatchers.Main.immediate) { + disclaimerWillShow() + + store.dispatch( + DisclaimerAction.Show( + from = DisclaimerSource.Home, + callback = DisclaimerCallback( + onAccept = { + scope.launch(Dispatchers.Main.immediate) { + nextHandler(scanResponse) + } + }, + onDismiss = { + scope.launch(Dispatchers.Main.immediate) { + onFailure(TangemSdkError.UserCancelled()) + } + }, + ), ), - ), - ) + ) + } } } } From 01b9ceda966ac290fb16ac1051ff90fac8eb9774 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jul 2024 23:58:54 +0400 Subject: [PATCH 208/223] Updated on 2026-08-14 --- .../tangem/features/details/utils/UserWalletMappers.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt index eab5ed50e3..a9782fc1d9 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -21,7 +21,8 @@ internal fun List.toUiModels( isBalancesHidden: Boolean = false, ): ImmutableList = this.map { model -> val balance = balances[model.walletId] - model.mapToUiModel( + + model.toUiModel( balance = balance, appCurrency = appCurrency, isLoading = isLoading, @@ -30,7 +31,7 @@ internal fun List.toUiModels( ) }.toImmutableList() -private fun UserWallet.mapToUiModel( +private fun UserWallet.toUiModel( balance: TotalFiatBalance?, appCurrency: AppCurrency?, isLoading: Boolean, @@ -66,9 +67,9 @@ private fun UserWallet.getInfo( ) return when { + isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked)) isLoading -> cardCountRef - isBalanceHidden -> combinedReference(cardCountRef, dividerRef, stringReference(STARS)) else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef) } } @@ -80,7 +81,7 @@ private fun getBalanceInfo( dividerRef: TextReference, ): TextReference { val amount = when (balance) { - is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized } + is TotalFiatBalance.Loaded -> balance.amount is TotalFiatBalance.Failed, is TotalFiatBalance.Loading, null, From 3b5f51c639af99c02e18bed3d7567f481a80b205 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 31 Jul 2024 00:26:59 +0400 Subject: [PATCH 209/223] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsViewModel.kt | 54 ++++++++++++------- .../twins/redux/TwinCardsMiddleware.kt | 24 ++++----- .../products/twins/redux/TwinCardsState.kt | 12 +++-- .../twins/ui/OnboardingTwinsFragment.kt | 8 +-- 4 files changed, 59 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index d8b9f44ca9..47425804ff 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -24,6 +24,8 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.features.details.ui.common.utils.* +import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode +import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.store import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel @@ -129,27 +131,8 @@ internal class CardSettingsViewModel @Inject constructor( changeAccessCode() } is CardInfo.ResetToFactorySettings -> { - val card = requireNotNull(scannedScanResponse.value) { - "Impossible to reset card if ScanResponse is null" - }.card - Analytics.send(Settings.CardSettings.ButtonFactoryReset()) - store.dispatchNavigationAction { - push( - route = AppRoute.ResetToFactory( - userWalletId = userWalletId, - cardId = card.cardId, - isActiveBackupStatus = card.backupStatus?.isActive == true, - backupCardsCount = when (val status = card.backupStatus) { - is CardDTO.BackupStatus.Active -> status.cardCount - is CardDTO.BackupStatus.CardLinked, - CardDTO.BackupStatus.NoBackup, - null, - -> 0 - }, - ), - ) - } + resetWalletToFactorySettings() } is CardInfo.SecurityMode -> { Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode()) @@ -166,6 +149,37 @@ internal class CardSettingsViewModel @Inject constructor( } } + private fun resetWalletToFactorySettings() { + val scanResponse = requireNotNull(scannedScanResponse.value) { + "Impossible to reset card if ScanResponse is null" + } + + if (scanResponse.cardTypesResolver.isTangemTwins()) { + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) + + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + } else { + val card = scanResponse.card + + store.dispatchNavigationAction { + push( + route = AppRoute.ResetToFactory( + userWalletId = userWalletId, + cardId = card.cardId, + isActiveBackupStatus = card.backupStatus?.isActive == true, + backupCardsCount = when (val status = card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + is CardDTO.BackupStatus.CardLinked, + CardDTO.BackupStatus.NoBackup, + null, + -> 0 + }, + ), + ) + } + } + } + private fun changeAccessCode() = viewModelScope.launch { val scanResponse = requireNotNull(scannedScanResponse.value) { "Scan response is null" } 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 2bc6f07ce1..a53d4f51a7 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 @@ -46,7 +46,7 @@ object TwinCardsMiddleware { val handler = twinsWalletMiddleware } -private val twinsWalletMiddleware: Middleware = { dispatch, state -> +private val twinsWalletMiddleware: Middleware = { dispatch, _ -> { next -> { action -> handle(action, dispatch) @@ -65,16 +65,16 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) fun getScanResponse(): ScanResponse { - return when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse - CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse + return when (val mode = twinCardsState.mode) { + is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse + is CreateTwinWalletMode.RecreateWallet -> mode.scanResponse } ?: throw NullPointerException("ScanResponse can't be NULL") } fun updateScanResponse(response: ScanResponse) { when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response - CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response)) + is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response + is CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response)) } } @@ -113,7 +113,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { mainScope.launch { val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) .invokeSync() @@ -132,7 +132,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { store.dispatch(dispatchAction) } } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning)) } } @@ -226,10 +226,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { delay(DELAY_SDK_DIALOG_CLOSE) withMainContext { when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet)) } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) } } @@ -319,11 +319,11 @@ private fun handle(action: Action, dispatch: DispatchFunction) { TwinCardsAction.Done -> { val scanResponse = getScanResponse() when (twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { store.dispatchOnMain(GlobalAction.Onboarding.Stop) OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { scope.launch { val walletsRepository = store.inject(DaggerGraphState::walletsRepository) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index a8867a1a2c..83e641bc8a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -32,7 +32,7 @@ data class TwinCardsState( val steps: List get() = when (mode) { - CreateTwinWalletMode.CreateWallet -> listOf( + is CreateTwinWalletMode.CreateWallet -> listOf( TwinCardsStep.None, TwinCardsStep.CreateFirstWallet, TwinCardsStep.CreateSecondWallet, @@ -40,7 +40,7 @@ data class TwinCardsState( TwinCardsStep.TopUpWallet, TwinCardsStep.Done, ) - CreateTwinWalletMode.RecreateWallet -> listOf( + is CreateTwinWalletMode.RecreateWallet -> listOf( TwinCardsStep.None, TwinCardsStep.CreateFirstWallet, TwinCardsStep.CreateSecondWallet, @@ -56,7 +56,13 @@ data class TwinCardsState( get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet } -enum class CreateTwinWalletMode { CreateWallet, RecreateWallet } +sealed class CreateTwinWalletMode { + data object CreateWallet : CreateTwinWalletMode() + + data class RecreateWallet( + val scanResponse: ScanResponse, + ) : CreateTwinWalletMode() +} sealed class TwinCardsStep { object None : TwinCardsStep() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index 69db8e0120..eda118c3dc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -56,10 +56,10 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( override fun configureTransitions() { when (store.state.twinCardsState.mode) { - CreateTwinWalletMode.CreateWallet -> { + is CreateTwinWalletMode.CreateWallet -> { super.configureTransitions() } - CreateTwinWalletMode.RecreateWallet -> { + is CreateTwinWalletMode.RecreateWallet -> { configureDefaultTransactions() } } @@ -391,8 +391,8 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( tvBody.setText(R.string.onboarding_done_body) val layout = when (state.mode) { - CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins - CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done + is CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins + is CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done } updateConstraints(state.currentStep, layout) } From 58cab30bd392ecc74dc12b1a753f74743e92163d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 17:52:39 +0500 Subject: [PATCH 210/223] Updated on 2026-08-14 --- .../usecase/SendTransactionUseCase.kt | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index faccbea7e4..96b5db01fa 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -14,6 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.DemoTransactionSender import com.tangem.domain.tokens.model.Network @@ -37,7 +38,10 @@ class SendTransactionUseCase( userWallet: UserWallet, network: Network, ): Either { - val signer = cardSdkConfigRepository.getCommonSigner(cardId = null) + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner(cardId = card.cardId.takeIf { isCardNotBackedUp }) val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() if (userWallet.scanResponse.card.isStart2Coin) { @@ -105,22 +109,7 @@ class SendTransactionUseCase( } val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() return when (error) { - is BlockchainSdkError.WrappedTangemError -> { - if (error.code == USER_CANCELLED_ERROR_CODE) { - SendTransactionError.UserCancelledError - } else { - val tangemError = error.tangemError - if (tangemError is TangemSdkError) { - val resource = tangemError.localizedDescriptionRes() - val resId = resource.resId ?: R.string.common_unknown_error - val resArgs = resource.args.map { it.value } - val textReference = resourceReference(resId, wrappedList(resArgs)) - SendTransactionError.TangemSdkError(tangemError.code, textReference) - } else { - SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) - } - } - } + is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) is BlockchainSdkError.CreateAccountUnderfunded -> { val minAmount = error.minReserve val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty() @@ -134,4 +123,26 @@ class SendTransactionUseCase( } } } + + private fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { + return if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + when (val tangemError = error.tangemError) { + is TangemSdkError -> { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + val textReference = resourceReference(resId, wrappedList(resArgs)) + SendTransactionError.TangemSdkError(tangemError.code, textReference) + } + is BlockchainSdkError.WrappedTangemError -> { + parseWrappedError(tangemError) // todo remove when sdk errors are revised + } + else -> { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } + } + } + } } \ No newline at end of file From 186f8f77c49c4990cb75e0de911272b4a2a6f987 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 16:44:47 +0300 Subject: [PATCH 211/223] Updated on 2026-08-14 --- features/referral/domain/build.gradle.kts | 1 + .../referral/domain/ReferralInteractorImpl.kt | 13 ++++++++++++- .../feature/referral/domain/errors/ReferralError.kt | 8 ++++++++ .../referral/viewmodels/ReferralViewModel.kt | 4 ++-- .../crypto/models/errors/UserCancelledException.kt | 3 --- 5 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt delete mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 0ae67f2d5f..4206513db7 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.tangem.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 36782f4e1d..04e3bb3028 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -1,10 +1,12 @@ package com.tangem.feature.referral.domain import arrow.core.getOrElse +import com.tangem.common.core.TangemSdkError import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData import com.tangem.lib.crypto.UserWalletManager @@ -42,7 +44,7 @@ internal class ReferralInteractorImpl( val cryptoCurrency = repository.getCryptoCurrency(userWalletId = userWallet.walletId, tokenData = tokenData) derivePublicKeysUseCase(userWallet.walletId, listOfNotNull(cryptoCurrency)).getOrElse { Timber.e("Failed to derive public keys: $it") - throw it + throw it.mapToDomainError() } addCryptoCurrenciesUseCase( @@ -67,4 +69,13 @@ internal class ReferralInteractorImpl( tokensForReferral.clear() tokensForReferral.addAll(tokens) } + + private fun Throwable.mapToDomainError(): ReferralError { + if (this !is TangemSdkError) return ReferralError.DataError(this) + return if (this is TangemSdkError.UserCancelled) { + ReferralError.UserCancelledException + } else { + ReferralError.SdkError + } + } } \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt new file mode 100644 index 0000000000..8d78d31fdd --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/errors/ReferralError.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.referral.domain.errors + +sealed class ReferralError : Exception() { + data object UserCancelledException : ReferralError() + data object SdkError : ReferralError() + + data class DataError(val throwable: Throwable) : ReferralError() +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index 6565947793..236d0c93db 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.referral.analytics.ReferralEvents import com.tangem.feature.referral.domain.ReferralInteractor +import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.DiscountType import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.ReferralInfo @@ -21,7 +22,6 @@ import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState import com.tangem.feature.referral.router.ReferralRouter -import com.tangem.lib.crypto.models.errors.UserCancelledException import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel @@ -95,7 +95,7 @@ internal class ReferralViewModel @Inject constructor( runCatching(dispatchers.io) { referralInteractor.startReferral(userWalletId) } .onSuccess(::showContent) .onFailure { throwable -> - if (throwable is UserCancelledException) { + if (throwable is ReferralError.UserCancelledException) { lastReferralData?.let { referralData -> showContent(referralData) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt deleted file mode 100644 index bdaf360494..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/errors/UserCancelledException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.lib.crypto.models.errors - -class UserCancelledException : Exception() \ No newline at end of file From e8301388dbeba0290d394c7349a834df9423a7aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 13:33:39 +0300 Subject: [PATCH 212/223] Updated on 2026-08-14 --- .../ui/cardsettings/CardSettingsFragment.kt | 11 +---- .../ui/cardsettings/CardSettingsScreen.kt | 17 ++++---- .../cardsettings/CardSettingsScreenState.kt | 1 + .../ui/cardsettings/CardSettingsViewModel.kt | 33 ++++++++------ .../AccessCodeRecoveryViewModel.kt | 43 ++++++++----------- .../domain/CardSettingsInteractor.kt | 35 +++++++++++++++ .../ui/resetcard/ResetCardViewModel.kt | 4 ++ .../com/tangem/common/routing/AppRoute.kt | 8 +--- 8 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 6054581056..328815f377 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -5,11 +5,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -25,12 +22,6 @@ internal class CardSettingsFragment : ComposeFragment() { override fun ScreenContent(modifier: Modifier) { val state by viewModel.screenState.collectAsStateWithLifecycle() - CardSettingsScreen( - modifier = modifier, - state = state, - onBackClick = { - store.dispatchNavigationAction(AppRouter::pop) - }, - ) + CardSettingsScreen(modifier = modifier, state = state) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index b0e2785b32..57f1103703 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -1,10 +1,13 @@ package com.tangem.tap.features.details.ui.cardsettings import android.content.res.Configuration -import androidx.compose.foundation.* +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,18 +16,14 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -internal fun CardSettingsScreen( - state: CardSettingsScreenState, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) { val needReadCard = state.cardDetails == null SettingsScreensScaffold( @@ -37,7 +36,7 @@ internal fun CardSettingsScreen( } }, titleRes = R.string.card_settings_title, - onBackClick = onBackClick, + onBackClick = state.onBackClick, ) } @@ -180,7 +179,7 @@ private fun CardSettings(state: CardSettingsScreenState) { // region Preview @Composable private fun CardSettingsScreenStateSample() { - CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}, onElementClick = {}), {}) + CardSettingsScreen(state = CardSettingsScreenState(onBackClick = {}, onScanCardClick = {}, onElementClick = {})) } @Preview(showBackground = true, widthDp = 360) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index 46425706b4..bbde2c8579 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -12,6 +12,7 @@ internal data class CardSettingsScreenState( val cardDetails: List? = null, val onScanCardClick: () -> Unit, val onElementClick: (CardInfo) -> Unit, + val onBackClick: () -> Unit, ) internal sealed class CardInfo( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 47425804ff..e3fe0469f9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.tangem.common.CompletionResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardProcessor @@ -23,14 +24,14 @@ import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.* import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.store import com.tangem.wallet.R import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -39,6 +40,7 @@ import javax.inject.Inject internal class CardSettingsViewModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val tangemSdkManager: TangemSdkManager, + private val cardSettingsInteractor: CardSettingsInteractor, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -46,24 +48,28 @@ internal class CardSettingsViewModel @Inject constructor( ?.unbundle(UserWalletId.serializer()) ?: error("User wallet ID is required for CardSettingsViewModel") - private val scannedScanResponse = MutableStateFlow(value = null) - val screenState: MutableStateFlow = MutableStateFlow(getInitialState()) + init { + cardSettingsInteractor.scannedScanResponse + .filterNotNull() + .onEach(::updateCardDetails) + .launchIn(viewModelScope) + } + private fun getInitialState() = CardSettingsScreenState( cardDetails = null, onElementClick = ::handleClickingItem, onScanCardClick = ::scanCard, + onBackClick = ::onBackClick, ) private fun scanCard() = viewModelScope.launch { scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> - scannedScanResponse.value = scanResponse - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) { - updateCardDetails(scanResponse) + cardSettingsInteractor.initialize(scanResponse) } else { store.dispatchDialogShow( AppDialog.SimpleOkDialogRes( @@ -141,16 +147,14 @@ internal class CardSettingsViewModel @Inject constructor( } } is CardInfo.AccessCodeRecovery -> { - store.dispatchNavigationAction { - push(route = AppRoute.AccessCodeRecovery(userWalletId)) - } + store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) } } else -> {} } } private fun resetWalletToFactorySettings() { - val scanResponse = requireNotNull(scannedScanResponse.value) { + val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { "Impossible to reset card if ScanResponse is null" } @@ -181,7 +185,7 @@ internal class CardSettingsViewModel @Inject constructor( } private fun changeAccessCode() = viewModelScope.launch { - val scanResponse = requireNotNull(scannedScanResponse.value) { "Scan response is null" } + val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { "Scan response is null" } when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) { is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) @@ -196,4 +200,9 @@ internal class CardSettingsViewModel @Inject constructor( val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin() return !isNotAllowed } + + private fun onBackClick() { + cardSettingsInteractor.clear() + store.dispatchNavigationAction(AppRouter::pop) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt index 8404af2beb..d65fb74a54 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt @@ -1,23 +1,16 @@ package com.tangem.tap.features.details.ui.cardsettings.coderecovery -import android.os.Bundle -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse import com.tangem.common.doOnSuccess -import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.Analytics import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled import com.tangem.tap.store import dagger.hilt.android.lifecycle.HiltViewModel @@ -28,25 +21,21 @@ import javax.inject.Inject @HiltViewModel internal class AccessCodeRecoveryViewModel @Inject constructor( - private val getUserWalletUseCase: GetUserWalletUseCase, private val tangemSdkManager: TangemSdkManager, - savedStateHandle: SavedStateHandle, + private val cardSettingsInteractor: CardSettingsInteractor, ) : ViewModel() { - private val userWalletId = savedStateHandle.get(AppRoute.AccessCodeRecovery.USER_WALLET_ID_KEY) - ?.unbundle(UserWalletId.serializer()) - ?: error("UserWalletId is required for AccessCodeRecoveryViewModel") + private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value + ?: error("Scan response is null") val screenState = MutableStateFlow( value = getInitialState(), ) private fun getInitialState(): AccessCodeRecoveryScreenState { - val userWallet = getUserWallet() - val isEnabled = isAccessCodeRecoveryEnabled( - typeResolver = userWallet.scanResponse.cardTypesResolver, - card = userWallet.scanResponse.card, + typeResolver = scannedScanResponse.cardTypesResolver, + card = scannedScanResponse.card, ) return AccessCodeRecoveryScreenState( @@ -59,11 +48,10 @@ internal class AccessCodeRecoveryViewModel @Inject constructor( } private fun saveChanges() = viewModelScope.launch { - val userWallet = getUserWallet() val isEnabled = screenState.value.enabledSelection tangemSdkManager - .setAccessCodeRecoveryEnabled(userWallet.cardId, isEnabled) + .setAccessCodeRecoveryEnabled(scannedScanResponse.card.cardId, isEnabled) .doOnSuccess { Analytics.send( Settings.CardSettings.AccessCodeRecoveryChanged( @@ -71,6 +59,16 @@ internal class AccessCodeRecoveryViewModel @Inject constructor( ), ) + cardSettingsInteractor.update { scanResponse -> + scanResponse.copy( + card = scanResponse.card.copy( + userSettings = scanResponse.card.userSettings?.copy( + isUserCodeRecoveryAllowed = isEnabled, + ), + ), + ) + } + store.dispatchNavigationAction(AppRouter::pop) } } @@ -78,14 +76,9 @@ internal class AccessCodeRecoveryViewModel @Inject constructor( private fun selectOption(isEnabled: Boolean) { screenState.update { it.copy( + enabledSelection = isEnabled, isSaveChangesEnabled = isEnabled != it.enabledOnCard, ) } } - - private fun getUserWallet(): UserWallet { - return getUserWalletUseCase(userWalletId).getOrElse { - error("Unable to get user wallet $userWalletId: $it") - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt new file mode 100644 index 0000000000..73bbf10991 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.features.details.ui.cardsettings.domain + +import com.tangem.domain.models.scan.ScanResponse +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Interactor for sharing logic and data between all card settings screens + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class CardSettingsInteractor @Inject constructor() { + + private val _scannedScanResponse = MutableStateFlow(value = null) + val scannedScanResponse: StateFlow = _scannedScanResponse + + fun initialize(scanResponse: ScanResponse) { + _scannedScanResponse.value = scanResponse + } + + fun update(transform: (ScanResponse) -> ScanResponse) { + _scannedScanResponse.update { + requireNotNull(it) + transform(it) + } + } + + fun clear() { + _scannedScanResponse.value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 41d0535147..fefbdf122e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -23,6 +23,7 @@ import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.features.details.redux.ResetCardDialog +import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription import com.tangem.tap.store import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE @@ -44,6 +45,7 @@ internal class ResetCardViewModel @Inject constructor( private val deleteWalletUseCase: DeleteWalletUseCase, private val userWalletsListManager: UserWalletsListManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val cardSettingsInteractor: CardSettingsInteractor, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -255,6 +257,8 @@ internal class ResetCardViewModel @Inject constructor( } private fun finishFullReset() { + cardSettingsInteractor.clear() + val newSelectedWallet = userWalletsListManager.selectedUserWalletSync if (newSelectedWallet != null) { diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 8bb8395a7d..20d08dcf6a 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -177,15 +177,9 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data class AccessCodeRecovery( - val userWalletId: UserWalletId, - ) : AppRoute(path = "/access_code_recovery/${userWalletId.stringValue}"), RouteBundleParams { + data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val USER_WALLET_ID_KEY = "userWalletId" - } } @Serializable From 07d5ede58cdc82df4e700ecbf28e5a2a62ade340 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 18:08:35 +0300 Subject: [PATCH 213/223] Updated on 2026-08-14 --- .../redux/OnboardingWalletMiddleware.kt | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) 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 c86ed79d1d..0e32a392c4 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 @@ -35,6 +35,7 @@ import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.R +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware @@ -44,6 +45,8 @@ object OnboardingWalletMiddleware { val handler = onboardingWalletMiddleware } +private const val HIDE_PROGRESS_DELAY = 400L + private val onboardingWalletMiddleware: Middleware = { dispatch, state -> { next -> { action -> @@ -161,17 +164,13 @@ private fun handleWalletAction(action: Action) { store.dispatch(GlobalAction.Onboarding.Stop) if (scanResponse == null) { - store.dispatchNavigationAction(AppRouter::pop) - store.dispatch(HomeAction.ReadCard(scope = action.scope)) + action.scope.launch { + readCard { newScanResponse -> + handleFinishOnboardind(newScanResponse) + } + } } else { - val backupState = store.state.onboardingWalletState.backupState - val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen( - scanResponse = updatedScanResponse, - accessCode = backupState.accessCode, - backupCardsIds = backupState.backupCardIds, - hasBackupError = backupState.hasBackupError, - ) + handleFinishOnboardind(scanResponse) } } is OnboardingWalletAction.ResumeBackup -> { @@ -196,6 +195,43 @@ private fun handleWalletAction(action: Action) { } } +private fun handleFinishOnboardind(scanResponse: ScanResponse) { + val backupState = store.state.onboardingWalletState.backupState + val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) + OnboardingHelper.trySaveWalletAndNavigateToWalletScreen( + scanResponse = updatedScanResponse, + accessCode = backupState.accessCode, + backupCardsIds = backupState.backupCardIds, + hasBackupError = backupState.hasBackupError, + ) +} + +private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) { + val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() + + store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + store.inject(DaggerGraphState::scanCardProcessor).scan( + analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Intro, + onProgressStateChange = { showProgress -> + if (showProgress) { + store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) + } else { + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + } + }, + onFailure = { + Timber.e(it, "Unable to scan card") + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) + }, + onSuccess = onSuccess, + ) +} + private suspend fun loadArtworkForUnfinishedBackup( cardId: String, cardPublicKey: ByteArray, From e1b50efbe0e64fdb1970d566eea464028ae8e5e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 1 Aug 2024 15:40:30 +0500 Subject: [PATCH 214/223] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 24 ----------- .../core/ui/utils/RequestPushPermission.kt | 13 +----- .../settings/DefaultPermissionRepository.kt | 43 +------------------ .../settings/DelayPermissionRequestUseCase.kt | 13 ------ .../IsFirstTimeAskingPermissionUseCase.kt | 11 ----- .../SetFirstTimeAskingPermissionUseCase.kt | 11 ----- .../repositories/PermissionRepository.kt | 17 -------- .../PushNotificationAnalyticEvents.kt | 8 +--- .../impl/PushNotificationsFragment.kt | 3 +- .../ui/PushNotificationsScreen.kt | 9 ++-- .../viewmodel/PushNotificationViewModel.kt | 25 ++--------- .../PushNotificationsClickIntents.kt | 4 +- .../PushNotificationsBottomSheetConfig.kt | 5 +-- .../PushNotificationsBottomSheet.kt | 15 ++----- .../wallet/viewmodels/WalletViewModel.kt | 20 +-------- .../WalletPushPermissionClickIntents.kt | 22 +--------- 16 files changed, 21 insertions(+), 222 deletions(-) delete mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt delete mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt delete mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt 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 index 7ff162a0d1..c05129c777 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -184,30 +184,6 @@ internal object SettingsDomainModule { return NeverToInitiallyAskPermissionUseCase(repository = permissionRepository) } - @Provides - @Singleton - fun provideIsFirstTimeAskingPermissionUseCase( - permissionRepository: PermissionRepository, - ): IsFirstTimeAskingPermissionUseCase { - return IsFirstTimeAskingPermissionUseCase(repository = permissionRepository) - } - - @Provides - @Singleton - fun provideSetFirstTimeAskingPushPermissionUseCase( - permissionRepository: PermissionRepository, - ): SetFirstTimeAskingPermissionUseCase { - return SetFirstTimeAskingPermissionUseCase(repository = permissionRepository) - } - - @Provides - @Singleton - fun provideDelayPermissionRequestUseCase( - permissionRepository: PermissionRepository, - ): DelayPermissionRequestUseCase { - return DelayPermissionRequestUseCase(repository = permissionRepository) - } - @Provides @Singleton fun provideShouldAskPermissionUseCase(permissionRepository: PermissionRepository): ShouldAskPermissionUseCase { diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt index cac63e19f7..1b21fff1ef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt @@ -6,7 +6,6 @@ import androidx.compose.runtime.MutableState import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.google.accompanist.permissions.shouldShowRationale /** * Returns push permission requester. @@ -16,21 +15,13 @@ import com.google.accompanist.permissions.shouldShowRationale @OptIn(ExperimentalPermissionsApi::class) @Composable fun requestPushPermission( - isFirstTimeAsking: Boolean, pushPermission: String?, isClicked: MutableState, onAllow: () -> Unit, onDeny: () -> Unit, - onOpenSettings: () -> Unit, ): () -> Unit { val permissionState = pushPermission?.let { permission -> - val tempPermissionState = rememberPermissionState(permission = permission) - rememberPermissionState(permission = permission) { - val isGranted = tempPermissionState.status.isGranted - val shouldShowRationale = tempPermissionState.status.shouldShowRationale - - if (isGranted && !shouldShowRationale && !isFirstTimeAsking) onOpenSettings() - } + rememberPermissionState(permission = permission) } // Check if user granted permission and close bottom sheet @@ -44,7 +35,7 @@ fun requestPushPermission( } return if (permissionState == null) { - onOpenSettings + {} } else { permissionState::launchPermissionRequest } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt index 6497ea59b4..e2528964fe 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt @@ -1,11 +1,6 @@ package com.tangem.data.settings -import android.os.SystemClock import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.PreferencesKeys.getIsFirstTimeAskingPermission -import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionDaysCount -import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionLaunchCount import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowInitialPermissionScreen import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowPermission import com.tangem.datasource.local.preferences.utils.getSyncOrDefault @@ -30,47 +25,11 @@ internal class DefaultPermissionRepository( ) } - override suspend fun isFirstTimeAskingPermission(permission: String): Boolean = - appPreferencesStore.getSyncOrDefault( - key = getIsFirstTimeAskingPermission(permission), - default = true, - ) - - override suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) { - appPreferencesStore.store( - key = getIsFirstTimeAskingPermission(permission), - value = value, - ) - } - override suspend fun shouldAskPermission(permission: String): Boolean { - val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) - val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0) - val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0) - val currentLaunchCounter = appPreferencesStore.getSyncOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) - - val nowMillis = SystemClock.elapsedRealtime() - val isDaysDelayed = delayedDays < nowMillis - val isLaunchesDelayed = delayedLaunches < currentLaunchCounter - return shouldAskPermission && isDaysDelayed && isLaunchesDelayed + return appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) } override suspend fun neverAskPermission(permission: String) { appPreferencesStore.store(key = getShouldShowPermission(permission), value = false) } - - override suspend fun delayPermissionAsking(permission: String) { - appPreferencesStore.editData { - val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) - val nowMillis = SystemClock.elapsedRealtime() - - it[getPermissionLaunchCount(permission)] = appLaunchCounter + DELAY_LAUNCH_COUNT - it[getPermissionDaysCount(permission)] = nowMillis + DELAY_DAYS_COUNT - } - } - - private companion object { - const val DELAY_LAUNCH_COUNT = 5 - const val DELAY_DAYS_COUNT = 3L * 24 * 3600 * 1000 // 3 days in millis - } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt deleted file mode 100644 index bf300cb779..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.settings - -import arrow.core.Either -import com.tangem.domain.settings.repositories.PermissionRepository - -class DelayPermissionRequestUseCase( - private val repository: PermissionRepository, -) { - - suspend operator fun invoke(permission: String): Either = Either.catch { - repository.delayPermissionAsking(permission) - } -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt deleted file mode 100644 index 000bfd7ed1..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import arrow.core.Either -import com.tangem.domain.settings.repositories.PermissionRepository - -class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { - - suspend operator fun invoke(permission: String): Either = Either.catch { - repository.isFirstTimeAskingPermission(permission) - } -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt deleted file mode 100644 index fa5d057e77..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.settings - -import arrow.core.Either -import com.tangem.domain.settings.repositories.PermissionRepository - -class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { - - suspend operator fun invoke(permission: String): Either = Either.catch { - repository.setFirstTimeAskingPermission(permission, false) - } -} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt index 721bfe90be..21b476b233 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt @@ -13,18 +13,6 @@ interface PermissionRepository { */ suspend fun neverInitiallyShowPermissionScreen(permission: String) - /** - * Indicates which time [permission] was asked via platform dialog. - * NOTE: Use this method to indicate either reroute to settings or display platform dialog. - */ - suspend fun isFirstTimeAskingPermission(permission: String): Boolean - - /** - * Sets value indicating that [permission] was asked via platform dialog. - * NOTE: Use this method to indicate either reroute to settings or display platform dialog. - */ - suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) - /** * Is clear to ask [permission]. * User could already granted or permanently denied permission @@ -36,9 +24,4 @@ interface PermissionRepository { * Permanently deny [permission] and never request again */ suspend fun neverAskPermission(permission: String) - - /** - * Delay next [permission] request for some time or active sessions - */ - suspend fun delayPermissionAsking(permission: String) } \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt index c35b196ec8..ae6f90b7a1 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -17,19 +17,15 @@ sealed class PushNotificationAnalyticEvents( ), ) - data class ButtonLater( + data class ButtonCancel( val source: AnalyticsParam.ScreensSources, ) : PushNotificationAnalyticEvents( - event = "Button - Later", + event = "Button - Cancel", params = mapOf( AnalyticsParam.SOURCE to source.value, ), ) - data object ButtonCancel : PushNotificationAnalyticEvents( - event = "Button - Cancel", - ) - data class PermissionStatus( val isAllowed: Boolean, ) : PushNotificationAnalyticEvents( diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt index 1d6fb08e71..c7110e411f 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt @@ -26,10 +26,9 @@ internal class PushNotificationsFragment : ComposeFragment() { NavigationBar3ButtonsScrim() PushNotificationsScreen( onRequest = viewModel::onRequest, - onRequestLater = viewModel::onRequestLater, + onNeverRequest = viewModel::onNeverRequest, onAllowPermission = viewModel::onAllowPermission, onDenyPermission = viewModel::onDenyPermission, - onOpenSettings = viewModel::openSettings, ) } diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt index c039d9109d..a73e852c64 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -17,18 +17,15 @@ import kotlinx.collections.immutable.persistentListOf @Composable internal fun PushNotificationsScreen( onRequest: () -> Unit, - onRequestLater: () -> Unit, + onNeverRequest: () -> Unit, onAllowPermission: () -> Unit, onDenyPermission: () -> Unit, - onOpenSettings: () -> Unit, ) { val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( - isFirstTimeAsking = true, isClicked = isClicked, onAllow = onAllowPermission, onDeny = onDenyPermission, - onOpenSettings = onOpenSettings, pushPermission = getPushPermissionOrNull(), ) @@ -54,8 +51,8 @@ internal fun PushNotificationsScreen( }, ), secondaryButton = ShowcaseButtonModel( - buttonText = resourceReference(R.string.common_later), - onClick = onRequestLater, + buttonText = resourceReference(R.string.common_cancel), + onClick = onNeverRequest, ), modifier = Modifier.systemBarsPadding(), ) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt index b87d3f4f88..4a0f3ed9f8 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -4,11 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.settings.SettingsManager -import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase -import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase -import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter @@ -19,12 +15,8 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel internal class PushNotificationViewModel @Inject constructor( - private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, - private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, - private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val router: DefaultPushNotificationsRouter, - private val settingsManager: SettingsManager, private val analyticHandler: AnalyticsEventHandler, ) : ViewModel(), PushNotificationsClickIntents { @@ -32,19 +24,14 @@ internal class PushNotificationViewModel @Inject constructor( analyticHandler.send( PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), ) - viewModelScope.launch { - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) - } } - override fun onRequestLater() { + override fun onNeverRequest() { analyticHandler.send( - PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories), + PushNotificationAnalyticEvents.ButtonCancel(AnalyticsParam.ScreensSources.Stories), ) viewModelScope.launch { - delayPermissionRequestUseCase(PUSH_PERMISSION) - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) - neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + neverRequestPermissionUseCase(PUSH_PERMISSION) router.openHome() } } @@ -55,7 +42,6 @@ internal class PushNotificationViewModel @Inject constructor( ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) - neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) router.openHome() } } @@ -65,11 +51,8 @@ internal class PushNotificationViewModel @Inject constructor( PushNotificationAnalyticEvents.PermissionStatus(isAllowed = false), ) viewModelScope.launch { - delayPermissionRequestUseCase(PUSH_PERMISSION) - neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + neverRequestPermissionUseCase(PUSH_PERMISSION) router.openHome() } } - - override fun openSettings() = settingsManager.openSettings() } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt index 2d4fe34217..63100190b7 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt @@ -3,11 +3,9 @@ package com.tangem.features.pushnotifications.impl.presentation.viewmodel internal interface PushNotificationsClickIntents { fun onRequest() - fun onRequestLater() + fun onNeverRequest() fun onAllowPermission() fun onDenyPermission() - - fun openSettings() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt index c659d29d7c..6fd16546b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt @@ -3,11 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent data class PushNotificationsBottomSheetConfig( - val isFirstTimeRequested: Boolean, - val wasInitiallyAsk: Boolean, val onRequest: () -> Unit, - val onRequestLater: () -> Unit, + val onNeverRequest: () -> Unit, val onAllow: () -> Unit, val onDeny: () -> Unit, - val openSettings: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt index dcc56be741..aeaad6ef3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -39,7 +39,6 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC val isClicked = remember { mutableStateOf(false) } val requestPushPermission = requestPushPermission( pushPermission = getPushPermissionOrNull(), - isFirstTimeAsking = content.isFirstTimeRequested, isClicked = isClicked, onAllow = { content.onAllow() @@ -49,7 +48,6 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC content.onDeny() onDismiss() }, - onOpenSettings = content.openSettings, ) Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { @@ -78,13 +76,9 @@ private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetC ), ) { SecondaryButton( - text = if (content.wasInitiallyAsk) { - stringResource(R.string.common_later) - } else { - stringResource(R.string.common_cancel) - }, + text = stringResource(R.string.common_cancel), onClick = { - content.onRequestLater() + content.onNeverRequest() onDismiss() }, modifier = Modifier.weight(1f), @@ -110,13 +104,10 @@ private fun PushNotificationsSheetContent_Preview() { TangemThemePreview { PushNotificationsSheetContent( PushNotificationsBottomSheetConfig( - isFirstTimeRequested = false, - wasInitiallyAsk = false, onRequest = {}, - onRequestLater = {}, + onNeverRequest = {}, onAllow = {}, onDeny = {}, - openSettings = {}, ), onDismiss = {}, ) 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 ed7f027de5..06bb5b53a9 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,7 +5,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase @@ -63,11 +62,8 @@ internal class WalletViewModel @Inject constructor( private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, - private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, - private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, - private val settingsManager: SettingsManager, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -157,26 +153,14 @@ internal class WalletViewModel @Inject constructor( delay(timeMillis = 1_800) - val isFirstTimeRequested = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } - val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } - val onRequestLater: (Boolean) -> Unit = { isUserDismissed -> - if (wasInitiallyAsk) { - clickIntents.onDelayAskPushPermission(isUserDismissed) - } else { - clickIntents.onNeverAskPushPermission(isUserDismissed) - } - } stateHolder.showBottomSheet( content = PushNotificationsBottomSheetConfig( - isFirstTimeRequested = isFirstTimeRequested, - wasInitiallyAsk = wasInitiallyAsk, onRequest = clickIntents::onRequestPushPermission, - onRequestLater = { onRequestLater(false) }, + onNeverRequest = { clickIntents.onNeverAskPushPermission(false) }, onAllow = clickIntents::onAllowPushPermission, onDeny = clickIntents::onDenyPushPermission, - openSettings = settingsManager::openSettings, ), - onDismiss = { onRequestLater(true) }, + onDismiss = { clickIntents.onNeverAskPushPermission(true) }, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt index 62fec7ce4f..13a301f372 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -2,9 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.settings.DelayPermissionRequestUseCase import com.tangem.domain.settings.NeverRequestPermissionUseCase -import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import dagger.hilt.android.scopes.ViewModelScoped @@ -15,8 +13,6 @@ internal interface WalletPushPermissionClickIntents { fun onRequestPushPermission() - fun onDelayAskPushPermission(isUserDismissed: Boolean) - fun onNeverAskPushPermission(isUserDismissed: Boolean) fun onDenyPushPermission() @@ -26,9 +22,7 @@ internal interface WalletPushPermissionClickIntents { @ViewModelScoped internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( - private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, - private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { @@ -38,27 +32,13 @@ internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( analyticsEventHandler.send( PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Main), ) - viewModelScope.launch { - setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) - } - } - - override fun onDelayAskPushPermission(isUserDismissed: Boolean) { - if (!isUserDismissedDialog) return - isUserDismissedDialog = isUserDismissed - viewModelScope.launch { - analyticsEventHandler.send( - PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Main), - ) - delayPermissionRequestUseCase(PUSH_PERMISSION) - } } override fun onNeverAskPushPermission(isUserDismissed: Boolean) { if (!isUserDismissedDialog) return isUserDismissedDialog = isUserDismissed viewModelScope.launch { - analyticsEventHandler.send(PushNotificationAnalyticEvents.ButtonCancel) + PushNotificationAnalyticEvents.ButtonCancel(AnalyticsParam.ScreensSources.Main) neverRequestPermissionUseCase(PUSH_PERMISSION) } } From 6c9eb4860f4426131bed75e1b19fbf9eb45e48fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 2 Aug 2024 15:53:00 +0300 Subject: [PATCH 215/223] 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 35af912b96..b5726a45bf 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 35af912b96ce0f871906061bd483d9cf3af7451d +Subproject commit b5726a45bf51b7763c5967afae4d3d687676240b From 5f96138f4f8621aa16a1518453266b1765223d15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 5 Aug 2024 16:53:06 +0300 Subject: [PATCH 216/223] Updated on 2026-08-14 --- .../java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index 549f514b01..7fe3ef0081 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,8 +1,6 @@ package com.tangem.tap.common.url import android.content.Context -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.content.Intent.FLAG_ACTIVITY_NO_HISTORY import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent @@ -36,9 +34,6 @@ internal class CustomTabsUrlOpener : UrlOpener { ) .build() - // Open CustomTabsActivity as new task without saving into the stack - customTabsIntent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_NO_HISTORY) - customTabsIntent.launchUrl(context, Uri.parse(url)) } } \ No newline at end of file From b350717cd0a1caa20dc46b4f367c824dcb22b6ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Aug 2024 13:28:40 +0300 Subject: [PATCH 217/223] Updated on 2026-08-14 --- .../tokens/impl/presentation/ui/BriefNetworksList.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt index e122327ff8..4091e53c06 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text @@ -123,14 +126,14 @@ internal fun HasMoreItem(moreCount: Int) { ) { Text( modifier = Modifier - .padding(TangemTheme.dimens.spacing4) .align(Alignment.Center) .drawWithContent { if (readyToDraw) drawContent() }, text = "+$count", style = textStyle, + color = TangemTheme.colors.text.tertiary, overflow = TextOverflow.Clip, onTextLayout = { textLayoutResult -> - if (textLayoutResult.didOverflowHeight) { + if (textLayoutResult.hasVisualOverflow) { textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9) } else { readyToDraw = true From 3d0a434423a44d4919d2638e473434cc3d06e1a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Aug 2024 16:37:09 +0500 Subject: [PATCH 218/223] Updated on 2026-08-14 --- .../impl/presentation/viewmodel/PushNotificationViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt index 4a0f3ed9f8..5b62923bbb 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter @@ -16,6 +17,7 @@ import javax.inject.Inject @HiltViewModel internal class PushNotificationViewModel @Inject constructor( private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val router: DefaultPushNotificationsRouter, private val analyticHandler: AnalyticsEventHandler, ) : ViewModel(), PushNotificationsClickIntents { @@ -32,6 +34,7 @@ internal class PushNotificationViewModel @Inject constructor( ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) router.openHome() } } @@ -42,6 +45,7 @@ internal class PushNotificationViewModel @Inject constructor( ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) router.openHome() } } @@ -52,6 +56,7 @@ internal class PushNotificationViewModel @Inject constructor( ) viewModelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) router.openHome() } } From 83772083833522de12c50d497966b66f93b80299 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Aug 2024 15:24:14 +0400 Subject: [PATCH 219/223] Updated on 2026-08-14 --- .../tap/features/disclaimer/Disclaimer.kt | 38 +---------------- .../tap/features/disclaimer/DisclaimerType.kt | 42 +++---------------- .../local/preferences/PreferencesKeys.kt | 2 - .../tangem/data/card/DefaultCardRepository.kt | 25 ----------- .../domain/card/repository/CardRepository.kt | 5 --- 5 files changed, 7 insertions(+), 105 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt index f92bc4be2f..9cda07b754 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt @@ -6,14 +6,13 @@ import android.net.Uri [REDACTED_AUTHOR] */ interface Disclaimer { - fun type(): DisclaimerType fun getUri(): Uri suspend fun accept() suspend fun isAccepted(): Boolean } abstract class BaseDisclaimer( - protected val dataProvider: DisclaimerDataProvider, + private val dataProvider: DisclaimerDataProvider, ) : Disclaimer { val baseUrl = "https://tangem.com" @@ -26,46 +25,11 @@ abstract class BaseDisclaimer( } class DummyDisclaimer : Disclaimer { - override fun type(): DisclaimerType = DisclaimerType.Tangem override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html") override suspend fun accept() {} override suspend fun isAccepted(): Boolean = false } class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) { - override fun type(): DisclaimerType = DisclaimerType.Tangem override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html") -} - -class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) { - override fun type(): DisclaimerType = DisclaimerType.Start2Coin - override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion())) - - @Suppress("ComplexMethod") - private fun filename(languageCode: String, regionCode: String?): String { - return when { - languageCode == "fr" && regionCode == "ch" -> "start2coin-fr-ch-tangem.html" - languageCode == "de" && regionCode == "ch" -> "start2coin-de-ch-tangem.html" - languageCode == "en" && regionCode == "ch" -> "start2coin-en-ch-tangem.html" - languageCode == "it" && regionCode == "ch" -> "start2coin-it-ch-tangem.html" - languageCode == "fr" && regionCode == "fr" -> "start2coin-fr-fr-tangem.html" - languageCode == "de" && regionCode == "at" -> "start2coin-de-at-tangem.html" - regionCode == "fr" -> "start2coin-fr-fr-tangem.html" - regionCode == "ch" -> "start2coin-en-ch-tangem.html" - regionCode == "at" -> "start2coin-de-at-tangem.html" - else -> "start2coin-fr-fr-tangem.html" - } - } - - private fun getRegion(): String? { - val cardId = dataProvider.getCardId() - if (cardId.isEmpty()) return null - - return when (cardId[1]) { - '0' -> "fr" - '1' -> "ch" - '2' -> "at" - else -> null - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt index 796452dcab..90f73cb4ad 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt @@ -1,58 +1,28 @@ package com.tangem.tap.features.disclaimer -import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import java.util.Locale -/** -[REDACTED_AUTHOR] - */ -enum class DisclaimerType { - Tangem, - Start2Coin, - ; - - companion object { - fun get(cardDTO: CardDTO): DisclaimerType { - return when { - cardDTO.isStart2Coin -> Start2Coin - else -> Tangem - } - } - } +fun CardDTO.createDisclaimer(): Disclaimer { + val dataProvider = provideDisclaimerDataProvider(cardId) + return TangemDisclaimer(dataProvider) } -fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer { - val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId, this) - return when (this) { - DisclaimerType.Tangem -> TangemDisclaimer(dataProvider) - DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider) - } -} - -fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this) - -private fun provideDisclaimerDataProvider(cardId: String, disclaimerType: DisclaimerType): DisclaimerDataProvider { +private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider { val cardRepository = store.inject(DaggerGraphState::cardRepository) return object : DisclaimerDataProvider { override fun getLanguage(): String = Locale.getDefault().language override fun getCardId(): String = cardId override suspend fun accept() { - when (disclaimerType) { - DisclaimerType.Tangem -> cardRepository.acceptTangemTOS() - DisclaimerType.Start2Coin -> cardRepository.acceptStart2CoinTOS(cardId) - } + cardRepository.acceptTangemTOS() } override suspend fun isAccepted(): Boolean { - return when (disclaimerType) { - DisclaimerType.Tangem -> cardRepository.isTangemTOSAccepted() - DisclaimerType.Start2Coin -> cardRepository.isStart2CoinTOSAccepted(cardId) - } + return cardRepository.isTangemTOSAccepted() } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index c517d9fdec..bb2c5fa1b0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -95,8 +95,6 @@ object PreferencesKeys { booleanPreferencesKey(name = "isTokenSwapPromoOkxShown") } - fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") - // region Permission fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") 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 index d0250fd7c3..5241c8562b 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -80,24 +80,10 @@ internal class DefaultCardRepository( return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false) } - override suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean { - return appPreferencesStore.getSyncOrDefault( - key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)), - default = false, - ) - } - override suspend fun acceptTangemTOS() { return appPreferencesStore.store(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, true) } - override suspend fun acceptStart2CoinTOS(cardId: String) { - appPreferencesStore.store( - key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)), - value = true, - ) - } - private suspend fun AppPreferencesStore.editUsedCards(cardId: String, update: (UsedCardInfo) -> UsedCardInfo) { editData { mutablePreferences -> val usedCards = mutablePreferences.getUsedCards() @@ -127,16 +113,5 @@ internal class DefaultCardRepository( .firstOrNull { it.cardId == cardId } } - private fun getRegion(cardId: String): String? { - if (cardId.isEmpty()) return null - - return when (cardId[1]) { - '0' -> "fr" - '1' -> "ch" - '2' -> "at" - else -> null - } - } - private fun createDefaultUsedCardInfo(cardId: String) = UsedCardInfo(cardId = 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 index 68e476443e..5e01929fa1 100644 --- 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 @@ -26,10 +26,5 @@ interface CardRepository { @Throws suspend fun isTangemTOSAccepted(): Boolean - @Throws - suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean - suspend fun acceptTangemTOS() - - suspend fun acceptStart2CoinTOS(cardId: String) } \ No newline at end of file From 3013aad882052541d297d85085da4b2252511d80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Aug 2024 16:14:34 +0400 Subject: [PATCH 220/223] Updated on 2026-08-14 --- .../com/tangem/common/routing/AppRoute.kt | 9 +------ .../impl/DefaultDisclaimerComponent.kt | 10 +------- .../disclaimer/impl/model/DisclaimerModel.kt | 24 +++++++++++++++---- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 20d08dcf6a..6cd73dfcb0 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -35,14 +35,7 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Disclaimer( val isTosAccepted: Boolean, - ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}"), RouteBundleParams { - - override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val IS_TOS_ACCEPTED_KEY = "isTosAccepted" - } - } + ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}") @Serializable data object OnboardingNote : AppRoute(path = "/onboarding/note") diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt index f281084c55..5fbd86f488 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt @@ -7,7 +7,6 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.disclaimer.impl.model.DisclaimerModel import com.tangem.features.disclaimer.impl.ui.DisclaimerScreen @@ -18,7 +17,6 @@ import dagger.assisted.AssistedInject internal class DefaultDisclaimerComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: DisclaimerComponent.Params, - private val appFinisher: AppFinisher, ) : DisclaimerComponent, AppComponentContext by context { private val model: DisclaimerModel = getOrCreateModel(params) @@ -27,13 +25,7 @@ internal class DefaultDisclaimerComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() - BackHandler { - if (params.isTosAccepted) { - state.popBack() - } else { - appFinisher.finish() - } - } + BackHandler(onBack = state.popBack) DisclaimerScreen(state = state) } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index f1be4240e0..e2dffb37f3 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase @@ -17,12 +18,14 @@ import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped +@Suppress("LongParameterList") internal class DisclaimerModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, private val cardRepository: CardRepository, private val router: Router, - override val dispatchers: CoroutineDispatcherProvider, private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val appFinisher: AppFinisher, paramsContainer: ParamsContainer, ) : Model() { @@ -33,23 +36,34 @@ internal class DisclaimerModel @Inject constructor( onAccept = ::onAccept, url = DISCLAIMER_URL, isTosAccepted = params.isTosAccepted, - popBack = router::pop, + popBack = ::popBack, ), ) - private fun onAccept(shouldAskPushPermission: Boolean) { - modelScope.launch { + private fun onAccept(shouldAskPushPermission: Boolean) = modelScope.launch { + if (params.isTosAccepted) { + router.pop() + } else { cardRepository.acceptTangemTOS() + if (shouldAskPushPermission) { router.push(AppRoute.PushNotification) } else { neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION) - router.push(AppRoute.Home) + router.replaceAll(AppRoute.Home) } } } + private fun popBack() { + if (params.isTosAccepted) { + router.pop() + } else { + appFinisher.finish() + } + } + private companion object { const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" } From 25abb8e2fc0ae491edc24176dd41e5f62b5c85aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 6 Aug 2024 13:43:21 +0300 Subject: [PATCH 221/223] Updated on 2026-08-14 --- .../details/ui/cardsettings/CardSettingsViewModel.kt | 2 ++ .../products/twins/redux/TwinCardsMiddleware.kt | 11 +++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index e3fe0469f9..7f972b948d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -161,6 +161,8 @@ internal class CardSettingsViewModel @Inject constructor( if (scanResponse.cardTypesResolver.isTangemTwins()) { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) + cardSettingsInteractor.clear() + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } } else { val card = scanResponse.card 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 a53d4f51a7..19d77c509e 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 @@ -65,9 +65,9 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) fun getScanResponse(): ScanResponse { - return when (val mode = twinCardsState.mode) { + return when (twinCardsState.mode) { is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse - is CreateTwinWalletMode.RecreateWallet -> mode.scanResponse + is CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse } ?: throw NullPointerException("ScanResponse can't be NULL") } @@ -105,6 +105,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) { mainScope.launch { if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch + if (twinCardsState.mode is CreateTwinWalletMode.RecreateWallet) { + store.dispatch(GlobalAction.SaveScanResponse(twinCardsState.mode.scanResponse)) + } + val scanResponse = getScanResponse() onboardingManager?.apply { if (!isActivationStarted(scanResponse.card.cardId)) { @@ -368,8 +372,7 @@ private fun getPopBackScreen(): KClass { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) return if (userWalletsListManager.hasUserWallets) { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } - .fold(onSuccess = { true }, onFailure = { false }) + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } if (isLocked) { AppRoute.Welcome::class From dcc3286a085df353772998ae2f707b68c0fc403d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 10:38:08 +0300 Subject: [PATCH 222/223] 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 b5726a45bf..095b8f4ea0 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b5726a45bf51b7763c5967afae4d3d687676240b +Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef From 5e3fcd146a2ef0ab290bf0043224e5e34835bf28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 7 Aug 2024 12:02:38 +0300 Subject: [PATCH 223/223] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 51 +++++++++--------- core/res/src/main/res/values-fr/strings.xml | 24 +++------ core/res/src/main/res/values-ja/strings.xml | 40 +++++++------- core/res/src/main/res/values-ru/strings.xml | 54 +++++++++++++------ .../src/main/res/values-uk-rUA/strings.xml | 21 ++------ core/res/src/main/res/values/strings.xml | 30 ++++++----- .../previewdata/InitialStakingStatePreview.kt | 9 +--- .../SetInitialDataStateTransformer.kt | 8 +-- .../ShowInfoBottomSheetStateTransformer.kt | 4 +- 9 files changed, 120 insertions(+), 121 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 67cef4ccd7..0123a1dfdd 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,7 +1,7 @@ Netzwerk wählen - Benutzerdefiniertes Token hinzufügen + Benutzerdef. Token hinzuf. Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. So scannt man @@ -13,9 +13,6 @@ Das gewählte unterstützt nicht das %1$s Netzwerk Um die kryptografische Verschlüsselung der %1$s Blockchain zu aktivieren, musst Du die Wallet auf die Werkseinstellungen zurücksetzen. Bitte heb vorher dein Guthaben ab, um sicherzustellen, dass du nichts verlierst, und führe dann den Reset-Vorgang durch. Nach dem Zurücksetzen ist der Zugriff auf die aktuelle Wallet nicht mehr möglich. Tokens im %1$s -Netzwerk werden von dieser Karte aufgrund einer Firmware-Einschränkung nicht unterstützt. - Vielen Dank für dein Feedback. Wir werden so schnell wie möglich antworten - Deine Vorschläge wurden übermittelt - Bitte versuche, die Karte genau wie in der Animation gezeigt anzutippen, oder lese unsere einfache Anleitung, oder forder Support an. Wenn das Problem weiterhin besteht, wende dich bitte an den Support. Hast du Probleme beim Scannen deiner Karte? Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet Standardgebühr @@ -31,8 +28,6 @@ Dunkel Hell Systemstandard - Wenn das System ausgewählt ist, passt sich die App automatisch an die Systemeinstellungen deines Geräts an - System Thema App Einstellungen Um deine Kontostände ein- oder auszublenden, flippe einfach das Display deines Geräts nach unten, oder schalte es in den Einstellungen aus. @@ -51,7 +46,7 @@ Deaktiviere diese Option, wenn du nicht möchtest, dass diese Karte zum Zurücksetzen von Zugangscodes auf anderen Karten in dieser Wallets verwendet wird. Bitte beachte, dass du dann auch den Zugangscode auf dieser Karte nicht zurücksetzen kannst. Ermöglicht die Verwendung dieser Karte zum Zurücksetzen des Zugangscodes auf anderen Karten in dieser Brieftasche - Wiederherstellung des Zugangscodes + Zugangscodes wiederherstellen Zurücksetzen Möchtest du das wirklich tun? Zugangscode ändern @@ -89,18 +84,17 @@ Abbrechen Stakingbelohnungen beanstpruchen Schließen - Weitermachen + Weiter Kopieren Adresse kopieren Erstellen - Benutzerdefiniert + Benutzerdef. - Tag - Tage + %d tag + %d tage Entfernen Deaktiviert - Trennen Erledigt Aktivieren Aktiviert @@ -113,7 +107,7 @@ Schnell Markt Langsam - Geschwindigkeit und Gebühr + Gebühren Adressen abrufen Zum Anbieter gehen Zum Token @@ -137,7 +131,6 @@ Ablehnen Neu laden Umbenennen - Wiederholen Speichern Änderungen speichern Suchen @@ -166,7 +159,6 @@ Es ist ein Fehler aufgetreten. Bitte versuche es erneut. Nicht erreichbar staking beenden - Warnung Ja Vertragsadresse kopiert! Verfügbare Netzwerke @@ -214,7 +206,6 @@ Flipp um Guthaben auszublenden Aussteller Signiert - Wenn du den Code vergisst, verlierst du den Zugriff auf dein Geld. Eine Codewiederherstellung ist nicht möglich. Gib uns eine Rückmeldung Details Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk @@ -466,7 +457,7 @@ leer %d Wörter - Um deine Wallets zu importieren, gib bitte deineSeed-Phrase in das folgende Feld ein + Um deine Wallets zu importieren, gib bitte deine Seed-Phrase in das folgende Feld ein Seed-Phrase generieren Wallet importieren Eine Seed-Phrase ist eine Reihe von Wörtern, mit denen du deine Wallet wiederherstellen kannst. Im Gegensatz zu den von der Karte generierten Schlüsseln sind Seed-Phrasen ungeschützt und können kopiert und gestohlen werden. Die Verwendung dieser Option erfolgt auf eigene Gefahr. @@ -508,7 +499,7 @@ Gruppe erstellen Nach Guthaben Token organisieren - Gruppierung aufheben + Gruppierung aufh. Wählen aus der Galerie aus Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt @@ -581,7 +572,6 @@ %1$s, %2$s Adresse Ziel-Tag - Möchtest du den Sendebildschirm wirklich schließen? Adresse eingeben Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein Ungültiges Tag. Es wird der Transaktion nicht hinzugefügt. @@ -657,9 +647,10 @@ Aktiv Um deine Kryptos zu unstaken, klick hier. Die Anzahl der zu stakenden Krypros muss mindesten %s betragen + nicht gestakte beanspruche + Jährliche prozentuale Rendite + Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Effektiver Jahreszins - Jährliche prozentuale Rendite - Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. Verfügbar Durchschnittliche Belohnungsquote %s geschätzter Profit @@ -667,7 +658,6 @@ Metriken Mindestanforderungen Keine Belohnungen zu beanspruchen. - Gestaked Belohnungen beanspruchen Eine Möglichkeit, Staking-Belohnungen zu erhalten. Es kann automatisch oder manuell beansprucht werden. Belohnungszeitplan @@ -679,15 +669,27 @@ Aufwärmphase Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. Stake %s + Migrieren Natives Staking Mit Staking kannst du %1s verdienen. Deine Staking-Belohnungen kommen alle ~%2s Tage. Verdiene Staking-Belohnungen + Die Belohnungen werden sofort nach dem unstaken gestoppt. Der unstakingprozess dauert %s. + Erneut binden + Erneut staken + Belohnungen erneut staken + Widerrufen + Neuwahl Belohnungen + Stake gesperrt Mehr staken - unstaken + gelocktes unlocken + Unstaken Prüfe, was nicht eingesetzt wurde, um dein Vermögen zu beanspruchen Staking beenden Validator/ Prüfer + Abstimmung + Abstimmung gesperrt + Zurückziehen Bewahre deine Krypto-Assets sicher auf, während die privaten Schlüssel auf deiner Karte bleiben Revolutionäre Hardware-Wallet Bis zu 3 physische Karten pro Wallet @@ -728,7 +730,6 @@ Die Geldsendung wird verfügbar, sobald die ausstehende(n) Transaktion(en) im Netzwerk %s abgeschlossen ist/sind. Der Verkauf von %s ist im Moment nicht verfügbar. Bitte prüfe später ob es Updates gibt. Staking %s ist aktuell nicht verfügbar. Prüfe bitte ob es ein neues Update gibt. - Adresse wählen Generiere XPUB Ausblenden Du bist dabei, dieses Token vom Hauptbildschirm auszublenden. Du kannst es jederzeit über die Seite „Token verwalten“ wieder hinzufügen. @@ -821,7 +822,7 @@ Aktivierungsfehler Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. BNB Beacon Chain wird abgeschaltet - Könnte besser sein + verbesserungswürdig Gefällt mir OK, habe ich verstanden! Echt toll! diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 30baffe42f..071924cd18 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -13,9 +13,6 @@ Le sélectionné ne prend pas en charge le réseau %1$s Pour activer le cryptage de la blockchain %1$s, vous devrez réinitialiser le portefeuille aux paramètres d\'usine. Veuillez retirer vos fonds avant de le faire pour vous assurer de ne pas les perdre, puis terminez le processus de réinitialisation. L\'accès au portefeuille actuel ne sera pas possible après la réinitialisation. Les jetons du réseau %1$s ne sont pas pris en charge par cette carte en raison d\'une limitation du micrologiciel. - Merci pour votre retour. Nous vous répondrons dès que possible - Vos suggestions ont été envoyées - Veuillez essayer d\'appuyer sur la carte exactement comme indiqué dans l\'animation ou lire notre guide simple, ou demander de l\'aide. Si le problème persiste, veuillez demander de l\'aide. Avez-vous des difficultés à scanner votre carte ? Cette carte n\'est pas conçue pour fonctionner avec Tangem Frais par défaut @@ -31,8 +28,6 @@ Sombre Clair Par défaut du système - Si le système est sélectionné, l\'application s\'ajustera automatiquement en fonction des paramètres système de votre appareil - Système Thème Paramètres de l\'application Pour masquer ou afficher vos soldes, il suffit de retourner l\'écran de votre appareil vers le bas ou de le désactiver dans les paramètres @@ -98,7 +93,6 @@ Supprimer Désactivé - Se déconnecter Exécuté Activer Activé @@ -134,7 +128,6 @@ Rejeter Recharger Renommer - Réessayer Enregistrer Sauvegarder les modifications Rechercher @@ -162,7 +155,6 @@ Je comprends Il y avait une erreur. Veuillez réessayer. Inaccessible - Alerte Oui Adresse du contrat copiée ! Réseaux disponibles @@ -210,7 +202,6 @@ Retourner pour masquer les soldes Emetteur Signé - Si vous oubliez le code, vous perdrez l\'accès à vos fonds. La récupération du code n\'est pas possible. Envoyer un commentaire Référénces Vérifiez votre connexion Internet ou passez à un réseau différent @@ -385,7 +376,7 @@ Voulez-vous quitter le processus d\'activation ? Initialiser Un autre portefeuille a déjà été créé sur la carte que vous essayez d\'ajouter. Si vous avez des fonds dans ce portefeuille, veuillez les retirer, puis réinitialiser cette carte et l\'ajouter comme sauvegarde. - Création d\'une sauvegarde + Sauvegarde en cours En savoir plus sur les seed phrases Empty @@ -398,7 +389,7 @@ Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous Générer une seed phrase - Importer un portefeuille + Importez Une seed phrase est une série de mots qui vous permet de récupérer votre portefeuille. Contrairement aux clés générées par la carte, les seed phrases ne sont pas protégées et peuvent être copiées et volées. Utilisez cette option à vos propres risques. Utiliser une seed phrase Seed phrase invalide. Veuillez vérifier l\'ordre des mots. @@ -509,7 +500,6 @@ %1$s, %2$s Adresse Destination Tag - Êtes-vous sûr de vouloir fermer l\'écran d\'envoi ? Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille Tag invalide. Il ne sera pas ajouté à la transaction. @@ -574,6 +564,7 @@ ≈ %1$s (incl. les commissions : %2$s) Sera envoyé %s La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps + %1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte. Adresse incorrecte %1$s (%2$s) Transaction envoyée @@ -583,9 +574,8 @@ Nom Actif Afin d\'unstaker vos actifs, cliquez ici. + Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. APR - APY - Le pourcentage de rendement annuel que vous pouvez gagner en participant au staking. Disponible Taux de récompense moyen %s profit estimatif @@ -593,7 +583,6 @@ Métriques Minimum requis Aucune récompense à réclamer - En jeu Réclamation de récompense Un moyen de recevoir des récompenses de staking. Il peut être réclamé automatiquement ou manuellement. Calendrier de récompenses @@ -649,7 +638,6 @@ L\'envoi de fonds sera disponible une fois la ou les transactions en attente dans le réseau %s terminées. La vente de %s n\'est pas disponible pour le moment. Veuillez consulter nos mises à jour. Le staking %s n’est pas disponible pour le moment. Veuillez consulter nos mises à jour. - Choisissez une adresse Générer XPUB Masquer Vous êtes sur le point de masquer ce jeton de l\'écran principal. Vous pouvez le rajouter à tout moment via la page de gestion des jetons. @@ -657,7 +645,7 @@ Masquer le jeton Le Staking vous permet d\'en gagner %1$s et d\'obtenir des récompenses tous les %2$s jours Gagnez jusqu\'à %s récompense de mise par an - %1$s jeton dans %%image%% le réseau %2$s + %1$s jeton dans %%image%% %2$s le réseau Jeton dans le %%image%% %1$s réseau Le jeton %1$s (%2$s) est la principale devise du réseau %3$s et ne peut pas être masqué tant que vous avez d\'autres jetons de ce réseau dans la liste Impossible de masquer %s @@ -741,7 +729,7 @@ Erreur d\'activation Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. BNB Beacon Chain va s\'arrêter de fonctionner - Pourrait être mieux + Pas terrible J\'aime Ok, je l\'ai! Vraiment cool ! diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index c0913ede01..d9c7168c7a 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -13,9 +13,6 @@ 選択したものは%1$sネットワークをサポートしていません。 %1$sブロックチェーンの暗号化を有効にするには、ウォレットを工場出荷時の設定にリセットする必要があります。リセットする前に出金して、資金が失われないようにしてから、リセット処理を完了してください。リセット後は、現在のウォレットにアクセスできなくなります。 %1$s ネットワークのトークンは、ファームウェアの制限により、このカードではサポートされていません。 - ご意見ありがとうございます。できるだけ早くご返信いたします。 - あなたの提案が送信されました - アニメーションに表示されているとおりにカードをタップするか、簡単なガイドをお読みください。それでも問題が解決しない場合は、サポートをご依頼ください。 カードのスキャンに問題がありますか? このカードはこのアプリでは使用できません。 デフォルト手数料 @@ -31,8 +28,6 @@ ダーク ライト システムのデフォルト - システムを選択した場合、アプリはデバイスのシステム設定に基づいて自動調整されます。 - システム テーマ アプリ設定 残高を表示または非表示にするには、デバイスの画面を下向きにするか、設定でオフにしてください。 @@ -94,11 +89,10 @@ 作成 設定 - + %d 日 削除 無効 - 切断 完了 有効にする 有効 @@ -135,7 +129,6 @@ 拒否 リロード 名前を変更 - リトライ 保存 変更内容を保存 検索 @@ -151,7 +144,7 @@ ステーキング ステーキング 始める - 提出する + 送信 成功 サポート スワップ @@ -164,7 +157,6 @@ エラーが発生しました。もう一度お試しください。 アクセスできません ステーキング解除 - 警告 はい コントラクトアドレスをコピーしました! 利用可能なネットワーク @@ -212,7 +204,6 @@ フリップして残高を非表示にする 発行者 署名済み - コードを忘れた場合、資金にアクセスできなくなります。コードの回復は不可能です。 フィードバックを送信 詳細 インターネット接続を確認するか、別のネットワークに切り替えてください。 @@ -347,7 +338,7 @@ 利用可能なネットワーク 私のポートフォリオ マーケット - 選択したネットワークのアドレスを生成するには、Tangemカードをタップする必要があります + 選択したネットワークのアドレスを生成するには、Tangemカードをスキャンする必要があります。 データを読み込めません… クイックアクション 結果 @@ -458,7 +449,7 @@ ウォレットをインポートするには、下のフィールドにシードフレーズを入力してください。 シードフレーズを生成する - ウォレットをインポートする + ウォレットをインポート シードフレーズは、ウォレットを復元できる一連の単語です。カードによって生成される秘密鍵とは異なり、シードフレーズは保護されていないため、コピーされて盗まれる可能性があります。このオプションは自己責任で使用してください。 シードフレーズを使用する 無効なシードフレーズです。語順を確認してください。 @@ -569,7 +560,6 @@ %1$s 、 %2$s アドレス 宛先タグ - 送金画面を閉じてもよろしいですか? アドレスを入力 アドレスはウォレットアドレスと同じです 無効なタグです。取引には追加されません。 @@ -645,17 +635,18 @@ アクティブ 資産のステーキングを解除するには、ここをクリックしてください。 ステーキング金額は %s 以上である必要があります + ステーキング解除分を請求する + APY + ステーキングに参加することで得られる年間収益率。 APR - APY - ステーキングに参加することで得られる年間収益率。 利用可能 平均報酬率 + ステーキングとは? %s 推定利益 市場評価 指標 最低要件 請求できる報酬はありません - ステーキング中 請求中の報酬 ステーキング報酬を受け取る方法。自動または手動で請求できます。 報酬スケジュール @@ -666,14 +657,28 @@ ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 ウォームアップ期間 ステーキングへの参加を有効にするために割り当てられた時間。 + %sをステーキングする + 移行 ネイティブステーキング ステーキングにより%1sを獲得できます。ステーキング報酬は ~ %2s日ごとに届きます。 ステーキング報酬を獲得 + ステーキング解除後、報酬の獲得はすぐに停止します。ステーキング解除プロセスには%sかかります。 + 再結束 + 再度ステーキングする + 報酬をステーキングする + 取り消す + 再投票 報酬 + ステーキングはロックされています もっとステーキングする + ステーキング解除はロックされています スタックされていない 資産を請求するために、unstakedを確認してください + ステーキング解除 バリデーター + 投票する + 投票はロックされています + 引き出す カード内に秘密鍵を保管しながら暗号資産を安全に保管します 革新的なハードウェアウォレット 1つのウォレットに最大3枚のカード @@ -714,7 +719,6 @@ ネットワーク%s内の保留中の取引が完了すると、送金が可能になります。 現在、 %sの売却はご利用いただけません。アップデート情報をご確認ください。 %s のステーキングは現在ご利用いただけません。最新情報をご確認ください。 - アドレスを選択 XPUBを生成する 非表示 このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 741704103a..f9f2f33750 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -13,9 +13,6 @@ Выбранный кошелёк не поддерживает сеть %1$s Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. - Спасибо за ваш отзыв. Мы ответим в кратчайшие сроки. - Ваши предложения отправлены - Пожалуйста, попробуйте приложить карту в точности, как показано на анимации, либо прочтите руководство по сканированию. Если проблема осталась, то запросите поддержку. У вас возникли трудности со сканированием карты? Эта карта не предназначена для работы с этим приложением Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. @@ -30,8 +27,6 @@ Тёмная Светлая Как в системе - При выборе настройки как в системе приложение будет использовать тему в соответствии с настройками вашего устройства - Системная Тема Настройки приложения Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" @@ -86,6 +81,7 @@ Перейти на %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена + Вывести награду Закрыть Продолжить Копировать @@ -100,7 +96,6 @@ Удалить Отключено - Отключить Готово Включить Включено @@ -137,7 +132,6 @@ Отклонить Перезагрузить Переименовать - Повторить Сохранить Сохранить изменения Искать @@ -150,6 +144,7 @@ Поделиться Подписать Подписать и отправить + Застейкать Стейкинг Начать Отправить @@ -164,7 +159,7 @@ Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно - Предупреждение + Завершить стейкинг Да Адрес контракта скопирован! Доступные сети @@ -212,7 +207,6 @@ Скрывать балансы жестом переворота Эмитент Подписано - Если вы забудете код, то потеряете доступ к своим средствам. Восстановление кода невозможно. Отправить отзыв Подробности Проверьте подключение с интернетом или переключитесь на другую сеть @@ -289,6 +283,7 @@ Укажите лимит доступа к выбранному токену Количество %s Функция подтверждения необходима для предоставления другому адресу разрешения на использование определенного количества ваших токенов.По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту StakeKit разрешение использовать ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете осуществить стейкинг токена. + Чтобы продолжить, вам необходимо разрешить смарт контракту StakeKit использовать ваш %s Чтобы продолжить, вам нужно разрешить смарт-контракту %1s использовать ваш %2s Дать разрешение Безлимитно @@ -514,7 +509,6 @@ %1$s, %2$s Адрес Код назначения - Вы действительно хотите закрыть экран отправки транзакции? Введите адрес Адрес совпадает с адресом кошелька Недопустимый Tag. Он не будет добавлен в транзакцию. @@ -586,20 +580,49 @@ Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + Активно + Для завершения стейкинга нажмите сюда Сумма для стейкинга должна быть не менее %s - APY - Годовой процентный доход, который вы можете получить от участия в стейкинге. + Забрать средства + APY + Годовой процентный доход, который вы можете получить от участия в стейкинге. + APR Доступно - %s + Средння ставка вознаграждения + %s оценка доходности + Позиция в рынке + Метрики + Минимальное количество + Нет вознаграждений к получению Способ возраграждения - Способ получения вознаграждений за стейкинг. Он может быть автоматическим или ручным. + Способ получения вознаграждений за стейкинг. Он может быть автоматическим, при котором вознаграждение само зачисляется вам на адрес или в ручную, когда вознаграждение нужно вывести, создав транзакцию на её получение. Период возрагражения Это период, определяющий, когда участники стейкинга получат свои вознаграждения. + Вознаграждение для получения: %s Стейкинг %s Период вывода Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. Период прогрева - Время, необходимое для начала процесса стейкинга. + Время, необходимое для начала процесса стейкинга и активации процесса начисления наград + Застейкать %s + Переместить + Нативный стейкинг + Стейкинг дает возможность вам получать %1s. Награда будет зачисляться каждый %2s + Получите награду за стейкинг + Награда перестанет начисляться сразу после завершения стейкинга. Процесс завершения длится %s. + Повторный стейкинг + Застейкать вознаграждения + Отозвать + Переголосовать + Вознаграждения + Застейкать еще + Разблокировать + Выведено из стейкинга + Проверьте процесс завершения стейкинга, чтобы вывести свои средства. + Завершение стейкинга + Валидатор + Проголосовать + Вывод Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -639,7 +662,6 @@ Отправка средств станет доступной после завершения транзакции(-ий) в сети %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями. - Выберите адрес Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 1c150d0e88..14a8373849 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -13,9 +13,6 @@ Обраний гаманець не підтримує мережу %1$s Щоб активувати криптографічне шифрування блокчейну %1$s, вам потрібно скинути налаштування гаманця до заводських. Зніміть свої кошти перед цим, щоб переконатися, що ви їх не втратите, а потім завершіть процес скидання. Вхід до поточного гаманця буде неможливий після скидання. Токени в мережі %1$s не підтримуються цією карткою через обмеження прошивки. - Дякуємо за ваш відгук. Ми відповімо якнайшвидше. - Ваші пропозиції надіслано - Будь ласка, спробуйте прикласти картку точно так, як показано на анімації, або прочитайте наш простий посібник. Якщо проблема залишилася, зверніться до служби підтримки. У вас виникли труднощі зі скануванням картки? Ця картка не призначена для роботи з цим додатком Комісія за замовчуванням @@ -31,8 +28,6 @@ Темна Світла Системна - При виборі \"Системна\" застосунок буде використовувати тему відповідно до налаштувань вашого пристрою - Системна Тема Налаштування застосунку Щоб приховати або показати свій баланс, просто переверніть екран пристрою вниз або вимкніть його в налаштуваннях @@ -104,7 +99,6 @@ Видалити Вимкнуто - Від\'єднати Готово Увімкнути Увімкнено @@ -141,7 +135,6 @@ Відхилити Перезавантажити Перейменувати - Повторити Зберегти Зберегти зміни Шукати @@ -170,7 +163,6 @@ Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно Скасувати стейкінг - Увага Так Адреса контракту скопійована! Доступні мережі @@ -218,7 +210,6 @@ Приховувати баланси жестом перевороту Емітент Підписано - Якщо ви забудете код, ви втратите доступ до своїх коштів. Відновлення коду неможливе. Надіслати відгук Деталі Перевірте підключення до інтернету або змініть мережу @@ -356,7 +347,7 @@ Доступні мережі Моє портфоліо Маркет - Щоб згенерувати адреси для обраних мереж, потрібно прикласти картку Tangem + Щоб згенерувати адреси для обраних мереж, потрібно відсканувати свою картку Tangem. Не вдалося завантажити дані... Швидкі дії Результат @@ -476,7 +467,7 @@ Щоб імпортувати гаманець, введіть seed-фразу в поле нижче Згенерувати seed-фразу - Імпортувати гаманець + Імпорт гаманця Seed-фраза — це набір слів, який дозволяє відновити ваш гаманець. На відміну від ключів, що генеруються карткою, seed-фраза не захищена і може бути скопійована та викрадена. Використовуйте цю опцію на свій власний ризик. Використовувати seed-фразу Невірна seed-фраза. Будь ласка, перевірте порядок слів. @@ -593,7 +584,6 @@ %1$s, %2$s Адреса Тег призначення - Ви дійсно хочете закрити екран надсилання транзакції? Введіть адресу Адреса збігається з адресою гаманця Недопустимий Tag. Він не буде доданий у транзакцію. @@ -669,9 +659,8 @@ Активний Щоб вивести активи зі стейкінгу, натисніть тут. Сума для стейкінгу має бути не менше %s + Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. APR - APY - Річний відсоток, який ви можете отримати, беручи участь у стейкінгу. Доступно Середня ставка винагороди ~ прибуток за %s @@ -679,7 +668,6 @@ Метрики Мінімальні вимоги Немає винагород, щоб отримати - В стейкінгу Отримати винагороду Спосіб отримання винагороди за стейкінг. Його можна отримати автоматично або вручну. Розклад винагород @@ -739,7 +727,6 @@ Надсилання коштів стане доступним після завершення транзакції(-ій) в мережі %s Наразі продаж %s недоступний. Слідкуйте за нашими оновленнями. Стейкінг %s зараз недоступний. Будь ласка, слідкуйте за нашими оновленнями. - Оберіть адресу Згенерувати XPUB Приховати Ви збираєтеся приховати цей токен з головного екрану. Ви можете додати його назад будь-коли на сторінці керування токенами. @@ -832,7 +819,7 @@ Помилка активації За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. Відключення мережі BNB Beacon Chain - Могло б бути краще + Можна краще Вподобати Зрозуміло! Дуже круто! diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 46299409f1..b277ef22a8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -13,9 +13,6 @@ The selected does not support the %1$s network To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. Tokens in %1$s network are not supported by this card due to firmware limitation. - Thank you for your feedback. We will respond as soon as possible - Your suggestions were sent - Please try to tap the card exactly as shown in the animation or read our simple guide, or request support. If the problem persists, please request support. Are you having difficulty scanning your card? This card is not designed to work with this app Default Fee @@ -31,8 +28,6 @@ Dark Light System default - If system is selected, the app will auto-adjust based on your device\'s system settings - System Theme App settings To hide or show your balances, simply flip your device screen down, or switch it off in Settings @@ -100,7 +95,6 @@ Delete Disabled - Disconnect Done Enable Enabled @@ -137,7 +131,6 @@ Reject Reload Rename - Retry Save Save changes Search @@ -166,7 +159,6 @@ There was an error. Please try again. Unreachable Unstake - Warning Yes Contract address copied! Available networks @@ -214,7 +206,6 @@ Flip-to-Hide Balances Issuer Signed - If you forget the code you will lose access to your funds. Code recovery is not possible. Send feedback Details Check your internet connection or switch to a different network @@ -577,7 +568,6 @@ %1$s, %2$s Address Destination Tag - Are you sure you want to close the send screen? Enter address Address is the same as wallet address Invalid Tag. It won\'t be added to the transaction. @@ -653,17 +643,18 @@ Active To unstake your assets, click here. The amount to stake must be at least %s + Claim unstaked + Annual percentage rate + The annual percentage return you can earn from participating in staking. APR - APY - The annual percentage return you can earn from participating in staking. Available Average Reward Rate + What is Staking? %s est. profit Market rating Metrics Minimum Requirement No rewards to claim - On stake Reward claiming A way to receive staking rewards. It can be claimed automatically or manually. Reward schedule @@ -675,15 +666,27 @@ Warmup period The allocated time for activating participation in staking. Stake %s + Migrate Native staking Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. Earn staking rewards + Rewards stop accruing immediately after you unstake. The unstaking process takes %s. + Rebond + Restake + Restake rewards + Revoke + Revote Rewards + Stake locked Stake more + Unlock locked Unstaked Check unstaked to claim your assets Unstaking Validator + Vote + Vote locked + Withdraw Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -724,7 +727,6 @@ Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. Staking %s is not available at the moment. Please check our updates. - Choose address Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 2255ba2015..cca20b1416 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -19,15 +19,10 @@ internal object InitialStakingStatePreview { endText = TextReference.Str("15 SOL"), ), RoundedListWithDividersItemData( - id = R.string.staking_details_apy, - startText = TextReference.Res(R.string.staking_details_apy), + id = R.string.staking_details_annual_percentage_rate, + startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), endText = TextReference.Str("2.54-5.12%"), ), - RoundedListWithDividersItemData( - id = R.string.staking_details_on_stake, - startText = TextReference.Res(R.string.staking_details_on_stake), - endText = TextReference.Str("0 SOL"), - ), RoundedListWithDividersItemData( id = R.string.staking_details_unbonding_period, startText = TextReference.Res(R.string.staking_details_unbonding_period), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 7e45dc8557..5dba67759a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -100,14 +100,14 @@ internal class SetInitialDataStateTransformer( ), ), RoundedListWithDividersItemData( - id = R.string.staking_details_apy, - startText = TextReference.Res(R.string.staking_details_apy), + id = R.string.staking_details_annual_percentage_rate, + startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), endText = getAprRange(), iconClick = { clickIntents.onInfoClick(InfoType.APY) }, ), RoundedListWithDividersItemData( - id = R.string.staking_details_on_stake, - startText = TextReference.Res(R.string.staking_details_on_stake), + id = 0, // todo remove in merge + startText = TextReference.Res(0), // todo remove in merge endText = TextReference.Str( value = BigDecimalFormatter.formatCryptoAmount( cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt index ff1701bc9b..05db6fb7ba 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -19,8 +19,8 @@ internal class ShowInfoBottomSheetStateTransformer( isShow = true, content = when (infoType) { InfoType.APY -> StakingInfoBottomSheetConfig( - title = resourceReference(R.string.staking_details_apy), - text = resourceReference(R.string.staking_details_apy_info), + title = resourceReference(R.string.staking_details_annual_percentage_rate), + text = resourceReference(R.string.staking_details_annual_percentage_rate_info), ) InfoType.UNBOUNDING_PERIOD -> StakingInfoBottomSheetConfig( title = resourceReference(R.string.staking_details_unbonding_period),