From 1582c3a8ef8926bae55d614dee3375a4fc1ef316 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 4 Jun 2024 11:53:56 +0500 Subject: [PATCH 01/74] 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 02/74] 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 03/74] 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 04/74] 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 05/74] 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 06/74] 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 07/74] 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 08/74] 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 09/74] 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 10/74] 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 11/74] 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 12/74] 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 13/74] 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 14/74] 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 15/74] 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 16/74] 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 17/74] 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 18/74] 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 19/74] 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 20/74] 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 21/74] 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 22/74] 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 23/74] 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 24/74] 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 25/74] 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 26/74] 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 27/74] 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 28/74] 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 29/74] 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 30/74] 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 31/74] 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 32/74] 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 33/74] 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 34/74] 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 35/74] 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 36/74] 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 37/74] 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 38/74] 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 39/74] 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 40/74] 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 41/74] 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 42/74] 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 43/74] 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 44/74] 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 45/74] 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 46/74] 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 47/74] 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 48/74] 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 49/74] 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 50/74] 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 51/74] 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 52/74] 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 53/74] 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 54/74] 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 55/74] 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 56/74] 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 57/74] 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 58/74] 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 59/74] 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 60/74] 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 61/74] 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 62/74] 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 63/74] 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 64/74] 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 65/74] 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 66/74] 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 67/74] 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 68/74] 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 69/74] 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 70/74] 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 71/74] 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 72/74] 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 73/74] 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 74/74] 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