From 069ff2f4e1287ace294f6c4cb27cc5b54f2c2d2f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 11:39:32 +0500 Subject: [PATCH 01/87] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../network/exchangeServices/DefaultRampManager.kt | 13 +++++++++++++ .../com/tangem/domain/exchange/RampStateManager.kt | 7 +++++++ .../onramp/tokenlist/model/OnrampTokenListModel.kt | 8 ++++---- .../tangem/feature/swap/DefaultSwapRepository.kt | 6 ++++-- .../com/tangem/feature/swap/di/SwapDataModule.kt | 3 +++ .../feature/swap/domain/SwapInteractorImpl.kt | 6 ++++-- 7 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 040b46b637..68ed0730c5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -98,6 +98,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.analytics) implementation(projects.domain.visa) implementation(projects.domain.onboarding) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index be5a5a9705..6b10fa0d44 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -18,6 +18,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching @@ -130,6 +131,18 @@ internal class DefaultRampManager( } } + override fun checkAssetRequirements(requirements: AssetRequirementsCondition?): Boolean { + return when (requirements) { + AssetRequirementsCondition.PaidTransaction, + is AssetRequirementsCondition.PaidTransactionWithFee, + is AssetRequirementsCondition.RequiredTrustline, + -> false + is AssetRequirementsCondition.IncompleteTransaction, + null, + -> true + } + } + private suspend fun getExchangeableState( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index dd4d2ae6d7..f48f6aadaf 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -7,11 +7,13 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.transaction.models.AssetRequirementsCondition import kotlinx.coroutines.flow.Flow /** * Manager that holds info about available actions as Sell and Buy */ +@Deprecated("Move to express domain layer") interface RampStateManager { suspend fun availableForBuy(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): ScenarioUnavailabilityReason @@ -50,4 +52,9 @@ interface RampStateManager { userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): ScenarioUnavailabilityReason + + /** + * Returns whether asset requirements are full filled to be able use express services + */ + fun checkAssetRequirements(requirements: AssetRequirementsCondition?): Boolean } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 8a3bd36ae7..6a5103bcfa 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -22,7 +22,6 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -212,21 +211,22 @@ internal class OnrampTokenListModel @Inject constructor( val isOperationAvailable = checkAvailabilityByOperation(status = status) val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation val isNotLoading = status.value !is CryptoCurrencyStatus.Loading + val requirements = getAssetRequirementsUseCase( userWalletId = userWallet.walletId, currency = status.currency, ).getOrNull() - val isNotTrustlineRequired = requirements !is AssetRequirementsCondition.RequiredTrustline + val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements) val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable val isAvailable = when (params.filterOperation) { OnrampOperation.BUY -> { - isNotTrustlineRequired + isAvailableForBuy } // unreachable state is available for Buy operation OnrampOperation.SELL -> isNotUnreachable OnrampOperation.SWAP -> { - isNotUnreachable && isNotTrustlineRequired + isNotUnreachable && isAvailableForBuy } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index a23bc00f4b..a7faad5bca 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -25,10 +25,10 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.converters.* @@ -55,6 +55,7 @@ internal class DefaultSwapRepository( private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, + private val rampStateManager: RampStateManager, moshi: Moshi, excludedBlockchains: ExcludedBlockchains, ) : SwapRepository { @@ -138,7 +139,8 @@ internal class DefaultSwapRepository( val currenciesList = currencyList .filter { val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it) - requirements !is AssetRequirementsCondition.RequiredTrustline + val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) + isAvailableForSwap } .map { leastTokenInfoConverter.convert(it) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 679015d755..8dbd0c2597 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository @@ -38,6 +39,7 @@ internal class SwapDataModule { @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, appPreferencesStore: AppPreferencesStore, + rampStateManager: RampStateManager, ): SwapRepository { return DefaultSwapRepository( tangemExpressApi = tangemExpressApi, @@ -49,6 +51,7 @@ internal class SwapDataModule { moshi = moshi, excludedBlockchains = excludedBlockchains, appPreferencesStore = appPreferencesStore, + rampStateManager = rampStateManager, ) } 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 712d322bd4..1e0209b1e3 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 @@ -17,6 +17,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.quote.QuoteStatus @@ -31,7 +32,6 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -82,6 +82,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase, private val amountFormatter: AmountFormatter, + private val rampStateManager: RampStateManager, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -179,12 +180,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, ): List? { val requirements = getAssetRequirementsUseCase.invoke(userWalletId, cryptoCurrencyStatuses.currency).getOrNull() + val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) return swapPairsLeastList.firstNotNullOfOrNull { val listTokenInfo = tokenInfoForAvailable(it) if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && - requirements !is AssetRequirementsCondition.RequiredTrustline + isAvailableForSwap ) { it.providers } else { From 25086c03af6316ea404029713ecd7dee298d41b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 17:11:11 +0400 Subject: [PATCH 02/87] Updated on 2026-08-14 --- .../domain/tokens/operations/BaseCurrencyStatusOperations.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 80744461c7..10a27ab90d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -369,7 +369,8 @@ abstract class BaseCurrencyStatusOperations( multiWalletCryptoCurrenciesSupplier.getSyncOrNull( params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), ) - ?.firstOrNull { it.network.id == networkId && it.network.derivationPath == derivationPath } + ?.filterIsInstance() + ?.firstOrNull { it.network.id == networkId } ?: error("Unable to create network coin with ID: $networkId and derivation path: $derivationPath") } else { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) From bc760834966185ea94f5604a815d598501c5ba5b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 12:55:01 +0400 Subject: [PATCH 03/87] Updated on 2026-08-14 --- .../domain/tokens/operations/BaseCurrencyStatusOperations.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 10a27ab90d..6e242d03c6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -371,7 +371,7 @@ abstract class BaseCurrencyStatusOperations( ) ?.filterIsInstance() ?.firstOrNull { it.network.id == networkId } - ?: error("Unable to create network coin with ID: $networkId and derivation path: $derivationPath") + ?: error("Unable to create network coin with ID: $networkId") } else { currenciesRepository.getNetworkCoin(userWalletId, networkId, derivationPath) } From e79bbf29adfa54cce5ddaa31a3030e32baeb9ca0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Aug 2025 17:27:54 +0700 Subject: [PATCH 04/87] Updated on 2026-08-14 --- .../features/walletconnect/connections/model/WcPairModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 9a63c9baad..c1dcb8cf95 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency @@ -69,8 +70,9 @@ internal class WcPairModel @Inject constructor( val stackNavigation = StackNavigation() - private val selectedUserWalletFlow = + private val selectedUserWalletFlow: MutableStateFlow by lazy { MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) + } private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() private var additionallyEnabledNetworks = setOf() From ee69ff5fb31f58936366030d3f6c4f9421d8b13e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Aug 2025 16:52:39 +0500 Subject: [PATCH 05/87] Updated on 2026-08-14 --- .../sign/BlockAidChainNameConverter.kt | 29 +++++----- .../utils/BlockAidVerificationDelegate.kt | 53 +++++++++++-------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt index 3421317947..c8491f5be6 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt @@ -4,31 +4,32 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.models.network.Network import com.tangem.utils.converter.Converter -import javax.inject.Inject -internal class BlockAidChainNameConverter @Inject constructor() : Converter { +internal object BlockAidChainNameConverter : Converter { @Suppress("CyclomaticComplexMethod") - override fun convert(value: Network): String { + override fun convert(value: Network): String? { return when (Blockchain.fromNetworkId(value.backendId)) { Blockchain.Arbitrum -> "arbitrum" Blockchain.Avalanche -> "avalanche" Blockchain.AvalancheTestnet -> "avalanche-fuji" - Blockchain.Binance, Blockchain.BSC -> "bsc" - Blockchain.Ethereum -> "ethereum" - Blockchain.EthereumTestnet -> "ethereum-sepolia" - Blockchain.Polygon -> "polygon" - Blockchain.Solana -> "mainnet" - Blockchain.Gnosis -> "gnosis" - Blockchain.Optimism -> "optimism" - Blockchain.ZkSyncEra -> "zksync" - Blockchain.ZkSyncEraTestnet -> "zksync-sepolia" Blockchain.Base -> "base" Blockchain.BaseTestnet -> "base-sepolia" + Blockchain.Binance, Blockchain.BSC -> "bsc" + Blockchain.Ethereum -> "ethereum" + Blockchain.Optimism -> "optimism" + Blockchain.Polygon -> "polygon" + Blockchain.ZkSyncEra -> "zksync" + Blockchain.ZkSyncEraTestnet -> "zksync-sepolia" Blockchain.Blast, Blockchain.BlastTestnet -> "blast" - Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain" Blockchain.Scroll -> "scroll" - else -> value.name + Blockchain.EthereumTestnet -> "ethereum-sepolia" + Blockchain.Gnosis -> "gnosis" + Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain" + + Blockchain.Solana -> "mainnet" + + else -> null } } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index 4a774ac505..eb03530b0a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -17,7 +17,6 @@ import javax.inject.Inject internal class BlockAidVerificationDelegate @Inject constructor( private val blockAidVerifier: BlockAidVerifier, - private val blockAidChainNameConverter: BlockAidChainNameConverter, ) { fun getSecurityStatus( @@ -27,7 +26,6 @@ internal class BlockAidVerificationDelegate @Inject constructor( session: WcSession, accountAddress: String?, ): LceFlow = flow { - emit(Lce.Loading(partialContent = null)) val failedResult = CheckTransactionResult( validation = ValidationResult.FAILED_TO_VALIDATE, simulation = SimulationResult.FailedToSimulate, @@ -36,7 +34,13 @@ internal class BlockAidVerificationDelegate @Inject constructor( emit(Lce.Content(failedResult)) return@flow } - when (method) { + val chain = BlockAidChainNameConverter.convert(network) + if (chain == null) { + emit(Lce.Content(failedResult)) + return@flow + } + emit(Lce.Loading(partialContent = null)) + val params = when (method) { is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params) is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction) is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction)) @@ -45,25 +49,28 @@ internal class BlockAidVerificationDelegate @Inject constructor( emit(Lce.Content(failedResult)) return@flow } - else -> null - }?.let { params -> - blockAidVerifier.verifyTransaction( - TransactionData( - chain = blockAidChainNameConverter.convert(network), - accountAddress = accountAddress, - method = rawSdkRequest.request.method, - domainUrl = session.sdkModel.appMetaData.url, - params = params, - ), - ).fold( - ifLeft = { - Timber.e("Failed to verify transaction: ${it.localizedMessage}") - emit(Lce.Error(it)) - }, - ifRight = { - emit(Lce.Content(it)) - }, - ) - } ?: emit(Lce.Content(failedResult)) + else -> { + emit(Lce.Content(failedResult)) + return@flow + } + } + + blockAidVerifier.verifyTransaction( + data = TransactionData( + chain = chain, + accountAddress = accountAddress, + method = rawSdkRequest.request.method, + domainUrl = session.sdkModel.appMetaData.url, + params = params, + ), + ).fold( + ifLeft = { + Timber.e("Failed to verify transaction: ${it.localizedMessage}") + emit(Lce.Error(it)) + }, + ifRight = { + emit(Lce.Content(it)) + }, + ) } } \ No newline at end of file From 4f88f3a7b01c2c01f3d3b854fb8df4b4a677e6f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 12:02:05 +0500 Subject: [PATCH 06/87] Updated on 2026-08-14 --- .../domain/walletconnect/WcAnalyticEvents.kt | 7 ++++--- .../transaction/model/WcAddNetworkModel.kt | 16 ++++++++++++++++ .../transaction/model/WcSignTransactionModel.kt | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index d4bff77def..e3f4cf7299 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -11,6 +11,7 @@ import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.extensions.mapNotNullValues sealed class WcAnalyticEvents( event: String, @@ -103,7 +104,7 @@ sealed class WcAnalyticEvents( class SignatureRequestReceived( rawRequest: WcSdkSessionRequest, network: Network, - emulationStatus: EmulationStatus, + emulationStatus: EmulationStatus?, ) : WcAnalyticEvents( event = "Signature Request Received", params = mapOf( @@ -111,8 +112,8 @@ sealed class WcAnalyticEvents( AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url, AnalyticsParam.Key.METHOD_NAME to rawRequest.request.method, AnalyticsParam.Key.BLOCKCHAIN to network.name, - AnalyticsParam.Key.EMULATION_STATUS to emulationStatus.status, - ), + AnalyticsParam.Key.EMULATION_STATUS to emulationStatus?.status, + ).mapNotNullValues { it.value }, ) { enum class EmulationStatus(val status: String) { Emulated("Emulated"), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 1684d836c3..107e983abc 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -49,12 +49,14 @@ internal class WcAddNetworkModel @Inject constructor( private val params = paramsContainer.require() private var useCase by Delegates.notNull() + private val signatureReceivedAnalyticsSendState = MutableStateFlow(false) init { modelScope.launch { useCase = useCaseFactory.createUseCase(params.rawRequest) .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } .getOrNull() ?: return@launch + sendSignatureReceivedAnalytics(useCase) _uiState.emit( wcAddEthereumChainUMConverter.convert( WcAddEthereumChainUMConverter.Input( @@ -114,4 +116,18 @@ internal class WcAddNetworkModel @Inject constructor( private fun copyData(text: String) { clipboardManager.setText(text = text, isSensitive = true) } + + private fun sendSignatureReceivedAnalytics(useCase: WcAddNetworkUseCase) { + if (signatureReceivedAnalyticsSendState.value) return + + analytics.send( + WcAnalyticEvents.SignatureRequestReceived( + rawRequest = useCase.rawSdkRequest, + network = useCase.network, + emulationStatus = null, + ), + ) + + signatureReceivedAnalyticsSendState.value = true + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index 1283f3e84f..ccc4413ef4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -59,12 +59,14 @@ internal class WcSignTransactionModel @Inject constructor( val stackNavigation = StackNavigation() private var useCase by Delegates.notNull() + private val signatureReceivedAnalyticsSendState = MutableStateFlow(false) init { modelScope.launch { useCase = useCaseFactory.createUseCase(params.rawRequest) .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } .getOrNull() ?: return@launch + sendSignatureReceivedAnalytics(useCase) useCase.invoke() .onEach { signState -> if (signingIsDone(signState)) return@onEach @@ -145,4 +147,18 @@ internal class WcSignTransactionModel @Inject constructor( private fun copyData(text: String) { clipboardManager.setText(text = text, isSensitive = true) } + + private fun sendSignatureReceivedAnalytics(useCase: WcMessageSignUseCase) { + if (signatureReceivedAnalyticsSendState.value) return + + analytics.send( + WcAnalyticEvents.SignatureRequestReceived( + rawRequest = useCase.rawSdkRequest, + network = useCase.network, + emulationStatus = null, + ), + ) + + signatureReceivedAnalyticsSendState.value = true + } } \ No newline at end of file From 9b4ac6e5f8d99c884f64b1e500563bbdb6b730fa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 12:31:59 +0500 Subject: [PATCH 07/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 6 +++--- core/res/src/main/res/values-fr/strings.xml | 2 ++ core/res/src/main/res/values-ja/strings.xml | 12 ++++++++++- core/res/src/main/res/values-ru/strings.xml | 20 +++++++++++++++++-- .../src/main/res/values-uk-rUA/strings.xml | 2 ++ core/res/src/main/res/values/strings.xml | 15 ++++++++++---- ...AddEthereumChainModalBottomSheetContent.kt | 2 +- .../send/WcSendTransactionModalBottomSheet.kt | 2 +- ...cSignTransactionModalBottomSheetContent.kt | 2 +- 10 files changed, 51 insertions(+), 14 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index aa10a37bfa..673c284024 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1434,7 +1434,7 @@ Neue Verbindung Verbinde Deine Wallet mit einer anderen dApp Keine Sitzungen - Diese Domain wird von mehreren Sicherheitsanbietern als unsicher eingestuft. Verlasse diese umgehend, um Dein Vermögen zu schützen. + Es wurden potenzielle Risiken oder bösartiges Verhalten erkannt. Das Verbinden oder Signieren von Transaktionen kann zum Verlust von Geldern führen. Bekanntes Sicherheitsrisiko Anfrage von Art der Signatur diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 6f67b2c033..f17262fd95 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1268,8 +1268,8 @@ Actualmente, MATIC está migrando a POL. Sin embargo, no se ha fijado ninguna fecha límite y MATIC aún no está obsoleto. Puede seguir usando el token MATIC de forma segura o utilizar intercambios para cambiarlo por POL. Migración de MATIC a POL - Use su tarjeta o anillo para obtener una dirección para la red - Use su tarjeta o anillo para obtener direcciónes para la red + Use su tarjeta o anillo para obtener una dirección para la red %d + Use su tarjeta o anillo para obtener direcciónes para las redes %d Faltan algunas direcciones La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. @@ -1341,7 +1341,7 @@ Transacción maliciosa Añada la red %s a su perfil para esta billetera La billetera no tiene las redes requeridas - Este dominio está marcado como no seguro por varios proveedores de seguridad. Salga de inmediato para proteger sus activos + Se han detectado riesgos potenciales o comportamiento malicioso. Conectarse o firmar transacciones puede resultar en la pérdida de fondos. Riesgo de seguridad conocido Solicitud de Tipo de firma diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a4140af715..bbf154df4e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1320,6 +1320,7 @@ Adresse Chargement Illimité + Réseaux connectés Connexions Contenu Copier les données @@ -1332,6 +1333,7 @@ Nouvelle connexion Connectez votre portefeuille à différentes dApps Aucune séance + Des risques potentiels ou un comportement malveillant ont été détectés. Se connecter ou signer des transactions peut entraîner une perte de fonds. Demande de Type de signature À diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a785951f19..cf05e40cb9 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -236,6 +236,7 @@ 拒否 リロード 名前を変更 + 必須 保存 変更内容を保存 検索 @@ -392,6 +393,7 @@ プロバイダー ベストレート FCA警告リスト + FCA警告リストに掲載されたプロバイダー 最大 %s まで使用可能 %s 以上で利用可能 このペアは利用できません @@ -1302,9 +1304,11 @@ Tangemウォレットを見る このウォレットを保護するためのシークレットコードです。ログインと署名に使用されます。 アクセスコードの設定 / 変更 + アクセスコードの変更 ウォレットの受信取引とTangemの更新について通知を受け取る。 現在、Huaweiデバイスではプッシュ通知が機能しない可能性があります。現在、解決策の検討に取り組んでおり、今後のアップデートで修正をリリースする予定です。ご理解のほどよろしくお願いいたします。 取引通知 + アクセスコードを設定する ウォレット設定 Tangem %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください @@ -1406,7 +1410,7 @@ WalletConnectを確立できませんでした このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 ブラウザに戻り、WalletConnect経由で再接続してください。 - WalletConnectセッションが接続解除されました + Wallet Connectセッションが接続解除されました とにかくサインする エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 @@ -1421,6 +1425,7 @@ 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています + すべてのdAppが接続解除されました 使用を許可する アドレス 接続する @@ -1431,6 +1436,7 @@ ウォレット 接続されたアプリ 接続されたネットワーク + %1$sへ接続済み ウォレットの残高とアクティビティを表示する 通知なしに取引に署名する 取引の承認をリクエストする @@ -1441,6 +1447,7 @@ 内容 データをコピー 使用可能量の設定 + dAppが接続解除されました すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する @@ -1448,6 +1455,8 @@ 取引をシミュレーションできませんでした。注意して続行してください。 %sでは見積もりはサポートされていません %sによる提案 + ネットワーク手数料をカバーするために残高を補充してください + %1$sが不足しています 悪意のある取引 このウォレットのポートフォリオに%sネットワークを追加します ウォレットに必要なネットワークはありません @@ -1462,6 +1471,7 @@ 署名タイプ dApp接続には、少なくとも1つのネットワークが必要です 選択したネットワークを指定する + 署名に成功しました 宛先 取引リクエスト 取引リクエスト diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 68950a67cc..10a53f974e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1304,15 +1304,18 @@ Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема + Все dapps отключены Разрешение на использование Адрес Подключение Загрузка Сеть Сети - Безлимитно + Без лимитно Кошелек + Подключенное приложение Подключенные сети + Подключено к %1$s Посмотреть баланс кошелька и его активность Подписать транзакцию без вашего участия Запрос разрешения на транзакцию @@ -1323,25 +1326,38 @@ Вложение Копировать данные Настраиваемый лимит + dApp отключен Отключить все Отключить все dApp Предварительные изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. + Оценка не поддерживается для %s + Предложено %s + Пополните ваш баланс, чтобы покрыть комиссию сети + Недостаточно %1$s Вредоносная транзакция Добавьте сеть %s в ваш портфель для выбранного кошелька В кошельке не добавлены необходимые сети Новое подключение Подключите свой кошелек к различным dApp Нет подключений - Этот домен помечен как небезопасный несколькими поставщиками систем безопасности. Немедленно покиньте его, чтобы защитить свои активы. + Изменения в кошельке не обнаружены. + Обнаружены потенциальные риски или вредоносное поведение. Подключение или подписание транзакций может привести к потере средств. Известный риск безопасности Запрос от + Подписать всё равно Тип подписи + Для подключения к dApp требуется как минимум одна выбранная сеть. + Укажите выбранные сети + Успешно подписано На Запрос транзакции Запрос транзакции Безлимитное количество + Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. + URI уже используется Подключение кошелька + Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? Да, возобновить diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index bc736e28ba..9c820fac97 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1286,10 +1286,12 @@ Обрана не вірна картка або кільце Адреса Підключення + Підключені мережі Переглянути баланс гаманця та активність Запит на підключення Вміст Копіювати дані + Виявлено потенційні ризики або шкідливу активність. Підключення чи підпис транзакцій можуть призвести до втрати коштів. Запит від Тип підпису До diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 039b9d2cdb..4c933aff60 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -58,12 +58,15 @@ Tokens in %1$s network are not supported by this card or ring due to firmware limitation. Are you having difficulty scanning your card or ring? This card is not designed to work with this app + Use %1$s to quickly and securely unlock your wallet and authorize all sensitive actions, such as signing transactions. For hardware wallets, you will still need a card to sign. Default Fee Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + Require Access Code + This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction. Save Access Code Biometric authentication will be requested instead of the access code for interactions with your card or ring. Keep the wallet in the app @@ -188,6 +191,7 @@ days Delete + Disable Disabled Disconnect Done @@ -340,6 +344,7 @@ Details Check your internet connection or switch to a different network Terms of service + Receive assets Sending assets in other networks will result in permanent loss. %s network Send funds using only @@ -400,6 +405,7 @@ Provider Best rate FCA Warning List + Provider in FCA warning list Available up to %s Available from %s Unavailable for this pair @@ -1478,7 +1484,7 @@ Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. Please return to your browser and reconnect via WalletConnect. - WalletConnect session was disconnected + Wallet Connect session was disconnected Sign anyway Error code: %s. If the problem persists — feel free to contact our support. If the problem persists — feel free to contact our support. @@ -1488,7 +1494,7 @@ Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error Tangem does not currently support a required network by %s. - Unsuported networks + Unsupported networks Tangem support a required network by %s Verified domain Wrong card or ring selected in the App @@ -1532,14 +1538,15 @@ Connect your wallet to a different dApps No sessions No wallet changes detected - This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets + Potential risks or malicious behavior have been detected. Connecting or signing transactions may lead to loss of funds. Known security risk Request from - Send anyway + Sign anyway Signature Type At least one network is required for dApp connection Specify selected networks Successfully signed + Wallet connect To Transaction request Transaction request diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt index e835f5ad87..36f2195df9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt @@ -52,7 +52,7 @@ internal fun WcAddEthereumChainModalBottomSheetContent( onBack = onBack, title = { TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = resourceReference(R.string.wc_transaction_flow_title), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index cf5f3fee9f..ec1400b0df 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -71,7 +71,7 @@ internal fun WcSendTransactionModalBottomSheet( containerColor = TangemTheme.colors.background.tertiary, title = { config -> TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = resourceReference(R.string.wc_transaction_flow_title), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 86e7fad3f4..21dd6eb5a9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -50,7 +50,7 @@ internal fun WcSignTransactionModalBottomSheetContent( onBack = onBack, title = { TangemModalBottomSheetTitle( - title = resourceReference(R.string.wc_wallet_connect), + title = resourceReference(R.string.wc_transaction_flow_title), endIconRes = R.drawable.ic_close_24, onEndClick = onDismiss, ) From ae33c00aaeba91afe2dbf8dcc5505c29183ef725 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Aug 2025 18:35:53 +0500 Subject: [PATCH 08/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7ab02a4628..6a3d13307a 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1126" +tangemBlockchainSdk = "releases-5.27.0-1132" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.27.0-510" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 85cff6b0d323533df3e52145b08137465736885f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 14:42:09 +0500 Subject: [PATCH 09/87] Updated on 2026-08-14 --- .../api/common/blockaid/BlockAidApi.kt | 3 +- .../request/SolanaTransactionScanRequest.kt | 2 +- .../response/SolanaTransactionResponse.kt | 58 ++++++++++++++++ .../tangem/data/blockaid/BlockAidMapper.kt | 68 ++++++++++++++++++- .../blockaid/DefaultBlockAidRepository.kt | 25 ++++--- .../blockaid/DefaultBlockAidRepositoryTest.kt | 3 +- .../utils/BlockAidVerificationDelegate.kt | 10 ++- .../walletconnect/model/WcSolanaMethod.kt | 15 +++- .../WcEstimatedWalletChangeUMConverter.kt | 5 +- .../WcSendAndReceiveBlockAidUiConverter.kt | 2 +- 10 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt index 605bc8a391..cdb6ec04e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/BlockAidApi.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse import retrofit2.http.Body import retrofit2.http.POST @@ -17,5 +18,5 @@ interface BlockAidApi { suspend fun scanJsonRpc(@Body request: EvmTransactionScanRequest): TransactionScanResponse @POST("solana/message/scan") - suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): TransactionScanResponse + suspend fun scanSolanaMessage(@Body request: SolanaTransactionScanRequest): SolanaTransactionResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt index 232343cf53..e89428b6a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/request/SolanaTransactionScanRequest.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.common.blockaid.models.response.TransactionMeta @JsonClass(generateAdapter = true) data class SolanaTransactionScanRequest( @Json(name = "encoding") val encoding: String = "base64", - @Json(name = "chain") val chain: String, + @Json(name = "blockchain") val blockchain: String, @Json(name = "method") val method: String, @Json(name = "options") val options: List = listOf("simulation", "validation"), @Json(name = "metadata") val metadata: TransactionMetadata, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt new file mode 100644 index 0000000000..d98f56ee5d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/SolanaTransactionResponse.kt @@ -0,0 +1,58 @@ +package com.tangem.datasource.api.common.blockaid.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SolanaTransactionResponse( + @Json(name = "result") val result: SolanaTransactionResult, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionResult( + @Json(name = "validation") val validation: SolanaTransactionValidation, + @Json(name = "simulation") val simulation: SolanaTransactionSimulation? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionValidation( + @Json(name = "result_type") val resultType: String, + @Json(name = "description") val description: String?, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionSimulation( + @Json(name = "account_summary") val accountSummary: SolanaTransactionAccountSummary, + @Json(name = "error") val error: String? = null, + @Json(name = "error_details") val errorDetails: String? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionAccountSummary( + @Json(name = "account_assets_diff") + val accountAssetsDiff: List, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionAssetDiff( + @Json(name = "asset_type") val assetType: String, + @Json(name = "asset") val asset: SolanaTransactionAsset, + @Json(name = "in") val inTransfer: SolanaTransferDetail? = null, + @Json(name = "out") val outTransfer: SolanaTransferDetail? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransactionAsset( + @Json(name = "address") val address: String? = null, + @Json(name = "symbol") val symbol: String? = null, + @Json(name = "name") val name: String? = null, + @Json(name = "decimals") val decimals: Int? = null, + @Json(name = "type") val type: String? = null, + @Json(name = "logo") val logoUrl: String? = null, +) + +@JsonClass(generateAdapter = true) +data class SolanaTransferDetail( + @Json(name = "value") val amount: String? = null, + @Json(name = "summary") val summary: String? = null, +) \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index 128d1cfd56..e61f8ddd21 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -17,6 +17,9 @@ private const val SUCCESS_STATUS = "Success" private const val DOMAIN_CHECKED_STATUS = "hit" private const val VALIDATION_SAFE_STATUS = "Benign" private const val VALIDATION_WARNING_STATUS = "Warning" +private const val VALIDATION_MALICIOUS_STATUS = "Malicious" + +private const val SOL_ASSET_SYMBOL = "SOL" internal object BlockAidMapper { @@ -28,6 +31,26 @@ internal object BlockAidMapper { } } + fun mapToDomain(from: SolanaTransactionResponse): CheckTransactionResult { + val validation = when (from.result.validation.resultType) { + VALIDATION_SAFE_STATUS -> ValidationResult.SAFE + VALIDATION_WARNING_STATUS -> ValidationResult.WARNING + VALIDATION_MALICIOUS_STATUS -> ValidationResult.UNSAFE + else -> ValidationResult.FAILED_TO_VALIDATE + } + val simulationResponse = from.result.simulation + val simulation = if (simulationResponse == null) { + SimulationResult.FailedToSimulate + } else { + mapToSolanaAssetsDiffs(simulationResponse.accountSummary.accountAssetsDiff) + } + return CheckTransactionResult( + validation = validation, + description = from.result.validation.description, + simulation = simulation, + ) + } + fun mapToDomain(from: TransactionScanResponse): CheckTransactionResult { return CheckTransactionResult( validation = when { @@ -68,7 +91,7 @@ internal object BlockAidMapper { fun mapToSolanaRequest(from: TransactionData): SolanaTransactionScanRequest { return SolanaTransactionScanRequest( - chain = from.chain.lowercase(), + blockchain = from.chain.lowercase(), accountAddress = from.accountAddress, metadata = TransactionMetadata(from.domainUrl), method = from.method, @@ -89,6 +112,49 @@ internal object BlockAidMapper { } } + private fun mapToSolanaAssetsDiffs(assetsDiffs: List): SimulationResult { + val sendInfo = assetsDiffs.mapNotNull { assetDiff -> + val outTransfer = assetDiff.outTransfer ?: return@mapNotNull null + val amount = outTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null + AmountInfo.FungibleTokens( + amount = amount, + token = TokenInfo( + chainId = null, + logoUrl = assetDiff.asset.logoUrl, + symbol = assetDiff.asset.assetSymbol(), + decimals = assetDiff.asset.decimals ?: 0, + ), + ) + } + val receiveInfo = assetsDiffs.mapNotNull { assetDiff -> + val inTransfer = assetDiff.inTransfer ?: return@mapNotNull null + val amount = inTransfer.amount?.toBigDecimalOrNull() ?: return@mapNotNull null + AmountInfo.FungibleTokens( + amount = amount, + token = TokenInfo( + chainId = null, + logoUrl = assetDiff.asset.logoUrl, + symbol = assetDiff.asset.assetSymbol(), + decimals = assetDiff.asset.decimals ?: 0, + ), + ) + } + + return if (sendInfo.isNotEmpty() || receiveInfo.isNotEmpty()) { + SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = receiveInfo)) + } else { + SimulationResult.Success(SimulationData.NoWalletChangesDetected) + } + } + + private fun SolanaTransactionAsset.assetSymbol(): String { + return if (type?.lowercase().equals(SOL_ASSET_SYMBOL, ignoreCase = true)) { + symbol ?: SOL_ASSET_SYMBOL + } else { + symbol.orEmpty() + } + } + private fun mapApproveTransaction(exposures: List?): SimulationResult { val amounts = exposures?.flatMap { exposure -> val tokenInfo = TokenInfo( diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt index 66483ed2df..985e8d7e2e 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -24,16 +24,21 @@ internal class DefaultBlockAidRepository( } override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult { - val response = withContext(dispatchers.io) { - when (data.params) { - is TransactionParams.Evm -> { - api.scanJsonRpc(mapper.mapToEvmRequest(data)) - } - is TransactionParams.Solana -> { - api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) - } - } + return when (data.params) { + is TransactionParams.Evm -> scanEvmTransaction(data = data) + is TransactionParams.Solana -> scanSolanaTransaction(data = data) } - return mapper.mapToDomain(response) } + + private suspend fun scanEvmTransaction(data: TransactionData): CheckTransactionResult = + withContext(dispatchers.io) { + val response = api.scanJsonRpc(mapper.mapToEvmRequest(data)) + mapper.mapToDomain(response) + } + + private suspend fun scanSolanaTransaction(data: TransactionData): CheckTransactionResult = + withContext(dispatchers.io) { + val response = api.scanSolanaMessage(mapper.mapToSolanaRequest(data)) + mapper.mapToDomain(response) + } } \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt index d125246bce..54d8331f1f 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.common.blockaid.models.request.DomainScanReques import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.request.SolanaTransactionScanRequest import com.tangem.datasource.api.common.blockaid.models.response.DomainScanResponse +import com.tangem.datasource.api.common.blockaid.models.response.SolanaTransactionResponse import com.tangem.datasource.api.common.blockaid.models.response.TransactionScanResponse import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -91,7 +92,7 @@ class DefaultBlockAidRepositoryTest { ) val request = mockk() - val response = mockk() + val response = mockk() val expectedResult = mockk() every { mapper.mapToSolanaRequest(data) } returns request diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index eb03530b0a..8bc9a0c492 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -40,6 +40,14 @@ internal class BlockAidVerificationDelegate @Inject constructor( return@flow } emit(Lce.Loading(partialContent = null)) + val methodName = when (method) { + is WcEthMethod -> rawSdkRequest.request.method + is WcSolanaMethod -> method.trimmedPrefixMethodName + is WcMethod.Unsupported -> { + emit(Lce.Content(failedResult)) + return@flow + } + } val params = when (method) { is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params) is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction) @@ -59,7 +67,7 @@ internal class BlockAidVerificationDelegate @Inject constructor( data = TransactionData( chain = chain, accountAddress = accountAddress, - method = rawSdkRequest.request.method, + method = methodName, domainUrl = session.sdkModel.appMetaData.url, params = params, ), diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt index 0042025dfb..d44d98c88f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -2,18 +2,27 @@ package com.tangem.domain.walletconnect.model sealed interface WcSolanaMethod : WcMethod { + val methodName: String + val trimmedPrefixMethodName: String get() = methodName.substringAfter("_") + data class SignMessage( val pubKey: String, val rawMessage: String, val humanMsg: String, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignMessage.raw + } data class SignTransaction( val transaction: String, val address: String?, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SignTransaction.raw + } data class SignAllTransaction( val transaction: List, - ) : WcSolanaMethod + ) : WcSolanaMethod { + override val methodName: String = WcSolanaMethodName.SendAllTransaction.raw + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt index c51cc78fbb..91ab33bb5b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.utils.converter.Converter -import java.math.BigDecimal import javax.inject.Inject internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : @@ -17,7 +16,7 @@ internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : is AmountInfo.FungibleTokens -> WcEstimatedWalletChangeUM( iconRes = value.iconRes, title = resourceReference(value.titleRes), - description = "${value.sign} ${amountInfo.amount.amountText()} ${amountInfo.token.symbol}", + description = "${value.sign} ${amountInfo.amountText()} ${amountInfo.token.symbol}", tokenIconUrl = amountInfo.token.logoUrl, ) is AmountInfo.NonFungibleTokens -> WcEstimatedWalletChangeUM( @@ -29,7 +28,7 @@ internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : } } - private fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } + private fun AmountInfo.FungibleTokens.amountText() = amount.format { crypto("", token.decimals) } data class Input( val amountInfo: AmountInfo, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 1eda478462..3312c609d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -21,7 +21,7 @@ import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import javax.inject.Inject -internal const val DECIMALS_AMOUNT = 2 +private const val DECIMALS_AMOUNT = 2 @Suppress("CyclomaticComplexMethod", "LongMethod") internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( From a69cc2b5e63a00126704bfa01b3c4fa8dc504e50 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Aug 2025 19:14:30 +0700 Subject: [PATCH 10/87] Updated on 2026-08-14 --- .../v2/feeselector/ui/FeeSelectorModalBottomSheet.kt | 5 ++++- .../transaction/model/WcSendTransactionModel.kt | 11 ++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index f9f7905693..1c2ba7badd 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -488,7 +488,10 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< isPrimaryButtonEnabled = false, feeItems = persistentListOf( FeeItem.Suggested( - title = stringReference("Suggested by Tangem"), + title = resourceReference( + id = R.string.wc_fee_suggested, + formatArgs = wrappedList("Tangem"), + ), fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), ), FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 623125db6f..8a7315fe55 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -18,7 +18,8 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2.Icon.Type -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -39,6 +40,7 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfigur import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.send.v2.api.subcomponents.feeSelector.entity.FeeSelectorData import com.tangem.features.walletconnect.connections.routing.WcInnerRoute +import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter import com.tangem.features.walletconnect.transaction.converter.WcSendTransactionUMConverter @@ -57,7 +59,7 @@ import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class WcSendTransactionModel @Inject constructor( @@ -112,7 +114,10 @@ internal class WcSendTransactionModel @Inject constructor( ?.dAppFee() ?.let { dAppFee -> feeStateConfiguration = FeeStateConfiguration.Suggestion( - title = stringReference(useCase.session.sdkModel.appMetaData.name), + title = resourceReference( + id = R.string.wc_fee_suggested, + formatArgs = wrappedList(useCase.session.sdkModel.appMetaData.name), + ), fee = dAppFee, ) } From 502d9e3b40ad07b705747e14fcb4414b36439bd0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 12:10:39 +0500 Subject: [PATCH 11/87] Updated on 2026-08-14 --- .../blockaid/models/response/AssetDiff.kt | 1 + .../blockaid/models/response/Exposure.kt | 1 + .../res/drawable/img_approvale_new_24.xml | 17 +++ .../tangem/data/blockaid/BlockAidMapper.kt | 101 +++++++++++------- .../data/blockaid/BlockAidMapperTest.kt | 9 +- .../network/ethereum/WcEthTxHelper.kt | 8 +- .../transaction/simultation/ApproveInfo.kt | 13 +++ .../transaction/simultation/ApprovedAmount.kt | 9 -- .../transaction/simultation/SimulationData.kt | 4 +- .../blockaid/WcEstimatedWalletChangesItem.kt | 10 +- .../WcSendAndReceiveBlockAidUiConverter.kt | 16 ++- .../send/WcSendTransactionModalBottomSheet.kt | 11 +- 12 files changed, 135 insertions(+), 65 deletions(-) create mode 100644 core/ui/src/main/res/drawable/img_approvale_new_24.xml create mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt delete mode 100644 domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt index be70cd4a52..a3c5d878a6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/AssetDiff.kt @@ -16,6 +16,7 @@ data class Asset( @Json(name = "chain_id") val chainId: Int? = null, @Json(name = "logo_url") val logoUrl: String? = null, @Json(name = "symbol") val symbol: String? = null, + @Json(name = "name") val name: String? = null, @Json(name = "decimals") val decimals: Int? = null, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt index e776056d4c..cfab51b716 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Exposure.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Exposure( + @Json(name = "asset_type") val assetType: String, @Json(name = "asset") val asset: Asset, @Json(name = "spenders") val spenders: Map, ) diff --git a/core/ui/src/main/res/drawable/img_approvale_new_24.xml b/core/ui/src/main/res/drawable/img_approvale_new_24.xml new file mode 100644 index 0000000000..bc1f814f54 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_approvale_new_24.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index e61f8ddd21..5334f1e066 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -3,7 +3,7 @@ package com.tangem.data.blockaid import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.transaction.* import com.domain.blockaid.models.transaction.simultation.AmountInfo -import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.domain.blockaid.models.transaction.simultation.TokenInfo import com.tangem.blockchain.extensions.hexToBigDecimal @@ -101,13 +101,8 @@ internal object BlockAidMapper { private fun mapSimulationSuccessResult(from: AccountSummaryResponse): SimulationResult { return when { - !from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction( - from.assetsDiffs, - ) - !from.exposures.isNullOrEmpty() -> mapApproveTransaction( - from.exposures, - ) - !from.traces.isNullOrEmpty() -> mapNftSendReceiveTransaction(from.traces) + !from.exposures.isNullOrEmpty() -> mapApproveTransaction(from.exposures) + !from.assetsDiffs.isNullOrEmpty() -> mapSendReceiveTransaction(from.assetsDiffs) else -> SimulationResult.Success(data = SimulationData.NoWalletChangesDetected) } } @@ -156,23 +151,11 @@ internal object BlockAidMapper { } private fun mapApproveTransaction(exposures: List?): SimulationResult { - val amounts = exposures?.flatMap { exposure -> - val tokenInfo = TokenInfo( - chainId = exposure.asset.chainId, - logoUrl = exposure.asset.logoUrl, - symbol = exposure.asset.symbol ?: "", - decimals = exposure.asset.decimals ?: 0, - ) - exposure.spenders.flatMap { (_, spender) -> - val isUnlimited = spender.isApprovedForAll == true - val approval = spender.approval?.hexToBigDecimal() - spender.exposure.map { detail -> - ApprovedAmount( - approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(), - isUnlimited = isUnlimited, - tokenInfo = tokenInfo, - ) - } + val amounts: List? = exposures?.flatMap { exposure -> + if (exposure.assetType.isNFT()) { + listOf(mapApproveNftTransaction(exposure)) + } else { + mapTransaction(exposure) } } return if (!amounts.isNullOrEmpty()) { @@ -182,6 +165,34 @@ internal object BlockAidMapper { } } + private fun mapTransaction(exposure: Exposure): List { + val tokenInfo = TokenInfo( + chainId = exposure.asset.chainId, + logoUrl = exposure.asset.logoUrl, + symbol = exposure.asset.symbol ?: "", + decimals = exposure.asset.decimals ?: 0, + ) + return exposure.spenders.flatMap { (_, spender) -> + val isUnlimited = spender.isApprovedForAll == true + val approval = spender.approval?.hexToBigDecimal() + spender.exposure.map { detail -> + ApproveInfo.Amount( + approvedAmount = detail.value?.toBigDecimalOrNull() ?: approval ?: 1.toBigDecimal(), + isUnlimited = isUnlimited, + tokenInfo = tokenInfo, + ) + } + } + } + + private fun mapApproveNftTransaction(exposure: Exposure): ApproveInfo.NonFungibleToken { + return ApproveInfo.NonFungibleToken( + name = exposure.asset.name.orEmpty(), + logoUrl = exposure.spenders.values.firstOrNull()?.exposure?.firstOrNull()?.logoUrl + ?: exposure.asset.logoUrl, + ) + } + private fun mapSendReceiveTransaction(assetDiffs: List?): SimulationResult { val sendInfo = arrayListOf() val receiveInfo = arrayListOf() @@ -194,13 +205,31 @@ internal object BlockAidMapper { decimals = diff.asset.decimals ?: 0, ) diff.outTransfer.orEmpty().forEach { transfer -> - transfer.value?.toBigDecimalOrNull()?.let { amount -> - sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + if (diff.assetType.isNFT()) { + sendInfo.add( + AmountInfo.NonFungibleTokens( + name = diff.asset.name.orEmpty(), + logoUrl = token.logoUrl, + ), + ) + } else { + transfer.value?.toBigDecimalOrNull()?.let { amount -> + sendInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + } } } diff.inTransfer.orEmpty().forEach { transfer -> - transfer.value?.toBigDecimalOrNull()?.let { amount -> - receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + if (diff.assetType.isNFT()) { + receiveInfo.add( + AmountInfo.NonFungibleTokens( + name = diff.asset.name.orEmpty(), + logoUrl = token.logoUrl, + ), + ) + } else { + transfer.value?.toBigDecimalOrNull()?.let { amount -> + receiveInfo.add(AmountInfo.FungibleTokens(amount = amount, token = token)) + } } } } @@ -212,17 +241,7 @@ internal object BlockAidMapper { } } - private fun mapNftSendReceiveTransaction(traces: List?): SimulationResult { - val sendInfo = traces?.mapNotNull { - it.exposed?.let { exposed -> - AmountInfo.NonFungibleTokens(name = "${it.asset.name} #${exposed.tokenId}", logoUrl = exposed.logoUrl) - } - } - - return if (!sendInfo.isNullOrEmpty()) { - SimulationResult.Success(SimulationData.SendAndReceive(send = sendInfo, receive = listOf())) - } else { - SimulationResult.Success(SimulationData.NoWalletChangesDetected) - } + private fun String.isNFT(): Boolean { + return this.lowercase() == "erc721" || this.lowercase() == "erc1155" || this.lowercase() == "nft" } } \ No newline at end of file diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt index 00dd54e198..3b24f6e0a4 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -4,6 +4,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.google.common.truth.Truth import com.tangem.datasource.api.common.blockaid.models.response.* @@ -44,6 +45,7 @@ class BlockAidMapperTest { val exposure = Exposure( asset = Asset(chainId = 1, logoUrl = "logo", symbol = "PEPE", decimals = 8), spenders = mapOf("spender" to spenderDetails), + assetType = "native", ) val response = TransactionScanResponse( validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), @@ -65,9 +67,10 @@ class BlockAidMapperTest { val approve = simulation?.data as? SimulationData.Approve Truth.assertThat(approve).isNotNull() - Truth.assertThat(approve?.approvedAmounts?.size).isEqualTo(1) - Truth.assertThat(approve?.approvedAmounts?.first()?.approvedAmount).isEqualTo(BigDecimal("1000.0")) - Truth.assertThat(approve?.approvedAmounts?.first()?.isUnlimited).isTrue() + Truth.assertThat(approve?.items?.size).isEqualTo(1) + Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.approvedAmount) + .isEqualTo(BigDecimal("1000.0")) + Truth.assertThat((approve?.items?.first() as? ApproveInfo.Amount)?.isUnlimited).isTrue() } @Test diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index 057afaa2e9..3526fb2d8c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -2,7 +2,7 @@ package com.tangem.data.walletconnect.network.ethereum import com.domain.blockaid.models.transaction.CheckTransactionResult import com.domain.blockaid.models.transaction.SimulationResult -import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData @@ -68,13 +68,15 @@ internal class WcEthTxHelper @Inject constructor( ) } - fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApprovedAmount? { + fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApproveInfo.Amount? { val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId val isApprovalWcMethod = txData?.startsWith(approvalMethodId) if (isApprovalWcMethod != true) return null val simulation = result.simulation as? SimulationResult.Success ?: return null - val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts + val approves = (simulation.data as? SimulationData.Approve) + ?.items + ?.filterIsInstance() ?: return null if (approves.isEmpty()) return null val amount = approves.first() diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt new file mode 100644 index 0000000000..2325c38b32 --- /dev/null +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApproveInfo.kt @@ -0,0 +1,13 @@ +package com.domain.blockaid.models.transaction.simultation + +import java.math.BigDecimal + +sealed class ApproveInfo { + data class Amount( + val approvedAmount: BigDecimal, + val isUnlimited: Boolean, + val tokenInfo: TokenInfo, + ) : ApproveInfo() + + data class NonFungibleToken(val name: String, val logoUrl: String?) : ApproveInfo() +} \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt deleted file mode 100644 index 6fdad58ec2..0000000000 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/ApprovedAmount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.domain.blockaid.models.transaction.simultation - -import java.math.BigDecimal - -data class ApprovedAmount( - val approvedAmount: BigDecimal, - val isUnlimited: Boolean, - val tokenInfo: TokenInfo, -) \ No newline at end of file diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt index 13c28ebba4..13937ad93e 100644 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/simultation/SimulationData.kt @@ -16,9 +16,7 @@ sealed class SimulationData { /** * Represents an approve operation with the specified amount (can be multiple amounts for NFT) */ - data class Approve( - val approvedAmounts: List, - ) : SimulationData() + data class Approve(val items: List) : SimulationData() /** * Simulation was successfully performed and no changes detected diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt index 25e8b53eba..590b49c95e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -132,8 +132,7 @@ private fun EstimatedWalletChangesPreviewMoreThanFour( } } -private class EstimatedWalletChangesPreviewProviderTwoItems : - PreviewParameterProvider { +private class EstimatedWalletChangesPreviewProviderTwoItems : PreviewParameterProvider { override val values = sequenceOf( WcEstimatedWalletChangesUM( items = persistentListOf( @@ -149,6 +148,13 @@ private class EstimatedWalletChangesPreviewProviderTwoItems : description = "+ 1,131.46 MATIC", tokenIconUrl = "https://tangem.com", ), + WcEstimatedWalletChangeUM( + iconRes = R.drawable.img_approvale_new_24, + title = resourceReference(R.string.common_approve), + description = "10 Collection", + tokenIconUrl = "https://cdn.blockaid.io/nft/0x09851531816f78cF4841f1DeF22fbaB78aDD02c5/29805/" + + "polygon?r=ed117da0-6065-4ff8-ba81-cb7395e1ec3d", + ), ), ), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 3312c609d3..3ef6afbf4c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -4,6 +4,7 @@ import com.domain.blockaid.models.transaction.CheckTransactionResult import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.domain.blockaid.models.transaction.simultation.ApproveInfo import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto @@ -55,7 +56,20 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( }, estimatedWalletChanges = (simulation as? SimulationResult.Success)?.data?.let { data -> when (data) { - is SimulationData.Approve, SimulationData.NoWalletChangesDetected -> null + is SimulationData.NoWalletChangesDetected -> null + is SimulationData.Approve -> { + val nftItems = data.items.mapNotNull { item -> + if (item !is ApproveInfo.NonFungibleToken) return@mapNotNull null + + WcEstimatedWalletChangeUM( + iconRes = R.drawable.img_approvale_new_24, + title = TextReference.Res(R.string.common_approve), + description = item.name, + tokenIconUrl = item.logoUrl, + ) + } + WcEstimatedWalletChangesUM(nftItems.toImmutableList()).takeIf { nftItems.isNotEmpty() } + } is SimulationData.SendAndReceive -> { val items: ImmutableList = ( data.send.map { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index ec1400b0df..8712039925 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -24,6 +24,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.components.notifications.Notification +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.stringReference @@ -122,7 +123,7 @@ internal fun WcSendTransactionModalBottomSheet( modifier = Modifier.padding(top = 14.dp), config = state.feeErrorNotification.config, iconTint = TangemTheme.colors.icon.warning, - containerColor = TangemTheme.colors.button.disabled, + containerColor = TangemTheme.colors.background.action, ) } } @@ -301,8 +302,12 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide address = null, sendEnabled = false, feeErrorNotification = NotificationUM.Info( - title = stringReference("Insufficient Ethereum"), - subtitle = stringReference("Top up your balance to cover the network fee"), + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = {}, + ), ), ), ), From 44a191019a337016f078272037edb0bb94762fe5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 15:02:46 +0500 Subject: [PATCH 12/87] Updated on 2026-08-14 --- .../converter/WcSendTransactionUMConverter.kt | 3 ++ .../entity/send/WcSendTransactionUM.kt | 2 + .../model/WcSendTransactionModel.kt | 2 + ...AddEthereumChainModalBottomSheetContent.kt | 1 + .../ui/common/WcTransactionRequestButtons.kt | 40 ++++++++++++++----- .../send/WcSendTransactionModalBottomSheet.kt | 5 +++ ...cSignTransactionModalBottomSheetContent.kt | 1 + .../utils/WcNotificationsFactory.kt | 8 +--- 8 files changed, 46 insertions(+), 16 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index 43c5b87851..c9f6f86182 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.transaction.converter import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck import com.tangem.domain.walletconnect.usecase.method.WcMethodContext import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep @@ -52,6 +53,7 @@ internal class WcSendTransactionUMConverter @Inject constructor( estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(), isLoading = value.signState.domainStep == WcSignStep.Signing, address = WcAddressConverter.convert(value.context.derivationState), + transactionValidationResult = value.securityCheck?.result?.validation, sendEnabled = value.feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null, feeErrorNotification = feeErrorNotification, ), @@ -78,6 +80,7 @@ internal class WcSendTransactionUMConverter @Inject constructor( val actions: WcTransactionActionsUM, val feeSelectorUM: FeeSelectorUM?, val cryptoCurrencyStatus: CryptoCurrencyStatus, + val securityCheck: BlockAidTransactionCheck.Result?, val onFeeReload: () -> Unit, ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt index 86ef260951..e97fc79509 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.entity.send +import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -27,6 +28,7 @@ internal data class WcSendTransactionItemUM( val walletName: String?, val networkInfo: WcNetworkInfoUM, val address: String?, + val transactionValidationResult: ValidationResult?, val sendEnabled: Boolean, val feeErrorNotification: NotificationUM.Info?, val isLoading: Boolean = false, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 8a7315fe55..c0e035a70e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -249,6 +249,7 @@ internal class WcSendTransactionModel @Inject constructor( feeSelectorUM = uiState.value?.feeSelectorUM, cryptoCurrencyStatus = cryptoCurrencyStatus, onFeeReload = ::triggerFeeReload, + securityCheck = securityCheck.getOrNull(), ), ) transactionUM = transactionUM?.copy( @@ -280,6 +281,7 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.SelectFee) } + // Before change, make sure you are align with WcTransactionRequestButtons private fun onSign(securityCheck: BlockAidTransactionCheck.Result?) { when (securityCheck?.result?.validation) { ValidationResult.UNSAFE -> showMaliciousAlert(securityCheck.result.description) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt index 36f2195df9..3f1a55cfe8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt @@ -97,6 +97,7 @@ internal fun WcAddEthereumChainModalBottomSheetContent( onClickActiveButton = state.onSign, activeButtonText = resourceReference(R.string.common_sign), isLoading = state.isLoading, + validationResult = null, ) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index ed0217aee9..ffd23bc142 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.domain.blockaid.models.transaction.ValidationResult +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.extensions.TextReference @@ -17,6 +19,7 @@ import com.tangem.features.walletconnect.impl.R internal fun WcTransactionRequestButtons( activeButtonText: TextReference, isLoading: Boolean, + validationResult: ValidationResult?, onDismiss: () -> Unit, onClickActiveButton: () -> Unit, modifier: Modifier = Modifier, @@ -30,15 +33,32 @@ internal fun WcTransactionRequestButtons( text = stringResourceSafe(R.string.common_cancel), onClick = onDismiss, ) - PrimaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - text = activeButtonText.resolveReference(), - onClick = onClickActiveButton, - iconResId = R.drawable.ic_tangem_24, - showProgress = isLoading, - enabled = enabled, - ) + // Before change, make sure you are align with WcSendTransactionModel::onSign + when (validationResult) { + ValidationResult.UNSAFE, + ValidationResult.WARNING, + -> PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.common_continue), + onClick = onClickActiveButton, + showProgress = isLoading, + enabled = enabled, + ) + ValidationResult.SAFE, + ValidationResult.FAILED_TO_VALIDATE, + null, + -> PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = activeButtonText.resolveReference(), + onClick = onClickActiveButton, + iconResId = R.drawable.ic_tangem_24, + showProgress = isLoading, + enabled = enabled, + ) + } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index 8712039925..b647ef5abd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp +import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -137,6 +138,7 @@ internal fun WcSendTransactionModalBottomSheet( activeButtonText = resourceReference(R.string.common_send), isLoading = state.isLoading, enabled = state.sendEnabled, + validationResult = state.transactionValidationResult, ) }, ) @@ -221,6 +223,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide address = null, sendEnabled = true, feeErrorNotification = null, + transactionValidationResult = null, ), WcSendTransactionItemUM( onDismiss = {}, @@ -263,6 +266,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide title = stringReference("Insufficient Ethereum"), subtitle = stringReference("Top up your balance to cover the network fee"), ), + transactionValidationResult = ValidationResult.WARNING, ), WcSendTransactionItemUM( onDismiss = {}, @@ -309,6 +313,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide onClick = {}, ), ), + transactionValidationResult = ValidationResult.SAFE, ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 21dd6eb5a9..5b0b50fe2e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -95,6 +95,7 @@ internal fun WcSignTransactionModalBottomSheetContent( onClickActiveButton = state.onSign, activeButtonText = resourceReference(R.string.common_sign), isLoading = state.isLoading, + validationResult = null, ) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt index bb97456631..c628689c04 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/utils/WcNotificationsFactory.kt @@ -1,6 +1,5 @@ package com.tangem.features.walletconnect.utils -import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference @@ -54,14 +53,11 @@ internal class WcNotificationsFactory @Inject constructor() { feeSelectorUM: FeeSelectorUM?, ): Boolean { val feeSelectorContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false - val lowestFee = when (val fees = feeSelectorContent.fees) { - is TransactionFee.Choosable -> fees.minimum - is TransactionFee.Single -> fees.normal - } + val selectedFee = feeSelectorContent.selectedFeeItem.fee return FeeCalculationUtils.checkExceedBalance( feeBalance = cryptoCurrencyStatus.value.amount, - feeAmount = lowestFee.amount.value, + feeAmount = selectedFee.amount.value, ) } } \ No newline at end of file From becbc0ed5c8d229d0e8165a726faa82cab78a88d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 17:14:15 +0300 Subject: [PATCH 13/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 6a3d13307a..1018ab6255 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1132" +tangemBlockchainSdk = "releases-5.27.0-1142" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-510" +tangemCardSdk = "releases-5.27.0-513" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-446" +tangemHotSdk = "develop-454" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 973c591ff20ecec5e61d25fa6fd951b769878cbc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Aug 2025 16:04:59 +0500 Subject: [PATCH 14/87] Updated on 2026-08-14 --- .../state/transformers/SetInitialDataStateTransformer.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 82f48a612c..b13bae78df 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 @@ -29,6 +29,7 @@ import com.tangem.features.staking.impl.presentation.state.converters.RewardsVal import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.PersistentList @@ -227,6 +228,9 @@ internal class SetInitialDataStateTransformer( } private fun getAprRange(validators: List): TextReference { + if (validators.isEmpty()) { + return stringReference(DASH_SIGN) + } val aprValues = validators .filter { it.preferred } .takeIf { it.isNotEmpty() } From 0ffde1aedb0d6b5043aaecc5bfcb6c94b3b86cac Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 13:55:02 +0700 Subject: [PATCH 15/87] Updated on 2026-08-14 --- .../walletconnect/connections/components/WcPairComponent.kt | 4 ++-- .../walletconnect/connections/routes/WcAppInfoRoutes.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index a171a0f38a..e9aa7aa21c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -57,7 +57,7 @@ internal class WcPairComponent( private fun onChildBack() { when (val config = contentStack.value.active.configuration) { is WcAppInfoRoutes.AppInfo -> dismiss() - is Alert -> when (config.type) { + is Alert -> when (config.alertType) { is Alert.Type.UnsupportedDApp, is Alert.Type.UnsupportedNetwork, -> dismiss() @@ -85,7 +85,7 @@ internal class WcPairComponent( ) is Alert -> AlertsComponentV2( appComponentContext = appComponentContext, - messageUM = createBottomSheetMessageUM(config.type), + messageUM = createBottomSheetMessageUM(config.alertType), ) is WcAppInfoRoutes.SelectNetworks -> WcSelectNetworksComponent( appComponentContext = appComponentContext, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index cf5b226353..27a3879932 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -26,7 +26,7 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { ) : WcAppInfoRoutes() @Serializable - data class Alert(val type: Type) : WcAppInfoRoutes() { + data class Alert(val alertType: Type) : WcAppInfoRoutes() { @Serializable sealed class Type { data class Verified(val appName: String) : Type() From 9b41230fa95aab957735d9a77cd5473414edb3da Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 09:19:15 +0000 Subject: [PATCH 16/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 7250e5eb0c..1018ab6255 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.26.0-1123" +tangemBlockchainSdk = "releases-5.27.0-1142" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.26.0-508" +tangemCardSdk = "releases-5.27.0-513" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ +tangemHotSdk = "develop-454" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } From 77e400b400544589d8b37c57799c9d8cb1442cc4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 18:15:10 +0500 Subject: [PATCH 17/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 13 ++++ core/res/src/main/res/values-es/strings.xml | 71 ++++++++++++++++++- core/res/src/main/res/values-fr/strings.xml | 13 ++++ core/res/src/main/res/values-it/strings.xml | 14 ++++ core/res/src/main/res/values-ja/strings.xml | 49 ++++++++++--- core/res/src/main/res/values-ru/strings.xml | 31 +++++++- .../src/main/res/values-uk-rUA/strings.xml | 56 +++++++++++++++ .../src/main/res/values-zh-rTW/strings.xml | 13 ++++ core/res/src/main/res/values/strings.xml | 39 ++++++++-- .../pair/DefaultWcPairUseCase.kt | 7 ++ .../connections/components/WcPairComponent.kt | 1 + .../connections/model/WcPairModel.kt | 3 + .../connections/routes/WcAppInfoRoutes.kt | 1 + .../connections/utils/WcAlertsFactory.kt | 18 +++++ .../converter/WcSendTransactionUMConverter.kt | 12 +++- 15 files changed, 322 insertions(+), 19 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 673c284024..7bc007eb36 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -147,6 +147,7 @@ Schließen Demnächst verfügbar Bestätigen + Verbinden Kontakt zum Tangem-Support Kontakt zum Visa-Support Weiter @@ -249,6 +250,10 @@ Allgemeine Geschäftsbedingungen Nutzungsbedingungen Heute + + %d Token + %d Token + Transaktion fehlgeschlagen Transaktionsstatus Transaktionen @@ -315,6 +320,8 @@ Details Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk Nutzungsbedingungen + Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt. + WalletConnect-Fehler Du hast eine Karte oder Ring aus einer anderen Wallet verwendet. Tippe auf die Karte oder Ring, die dieser Wallet zugeordnet ist. Nicht genug Geld für die Transaktion. Bitte lade dein Konto auf. Meine Token @@ -1426,6 +1433,12 @@ Alle trennen Text über die Trennung aller dApps Alle dApps trennen + Versuchen Sie erneut, mit einer neuen URI zu koppeln + Ungültige dApp-Domain + %s gibt keine Blockchains an — weder erforderlich noch optional.\nBitte stellen Sie sicher, dass Sie die richtige URI verwendet haben + Keine Netzwerke + Bitte generieren Sie eine neue URI und versuchen Sie erneut, eine Verbindung herzustellen + Verbindungsvorschlag abgelaufen Geschätzte Wallet-Änderungen Die Transaktion konnte nicht simuliert werden. Bitte sei vorsichtig. Böswillige/ gefährliche Transaktion diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index f17262fd95..8d38231daf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1,5 +1,9 @@ + Archivar cuenta + Archivo + Estás archivando esta cuenta, pero siempre puedes recuperarla. + Cuenta ¿No encuentra el token en su billetera? Consulte la sección de Mercados para encontrarlo y añadirlo a la compra ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta Vender @@ -34,12 +38,16 @@ Esta tarjeta no admite tokens en la red %1$s debido a una limitación del firmware. ¿Tiene dificultades para escanear su tarjeta/anillo? Esta tarjeta no está diseñada para funcionar con Tangem + Utilice %1$s para desbloquear de forma rápida y segura su biletera y autorizar todas las acciones importantes, como la firma de transacciones. En el caso de las billeteras físicas, seguirá necesitando una tarjeta para firmar. Tarifa por defecto Habilite las Tarifas predeterminadas para establecer automáticamente las tarifas de transacción y omitir la página de Tarifas al enviar fondos. Siempre puede volver a esta página si es necesario. Vaya a ajustes para habilitar la autenticación biométrica en la Tangem App Habilitar autenticación biométrica + Para deshabilitar %1$s deberá ingresar su código de acceso para desbloquear la aplicación e interactuar con su billetera. Esto eliminará todos los códigos de acceso guardados de la billetera. Cualquier operación posterior con la billetera requerirá introducir el código de acceso. Eliminar la tarjeta guardada borra todos las billeteras guardadas y sus códigos de acceso de la app. + Requerir código de acceso + Esta opción desactiva la autenticación biométrica para acciones importantes. Se le pedirá que introduzca su código de acceso cada vez que, por ejemplo, deba firmar una transacción. Guardar código de acceso Se solicitará la autenticación biométrica en lugar del código de acceso para las interacciones con su tarjeta o anillo. Mantener la billetera en la app @@ -135,6 +143,7 @@ Cerrar Próximamente Confirme + Conectando Contacte con el soporte de Tangem Contacte con el soporte de Visa Continuar @@ -153,6 +162,7 @@ días Suprimir + Desactivar Desactivado Desconectar Listo @@ -191,6 +201,7 @@ No Ninguna dirección No aregada + Ahora no Ahora OK Abrir en el navegador @@ -206,6 +217,7 @@ Rechazar Recargar Renombrar + Requerido Guarde Guardar cambios Buscar @@ -235,6 +247,10 @@ términos y condiciones Condiciones de uso Hoy + + %d ficha + %d fichas + Transacción fallida Estado de la transacción Transacciones @@ -301,6 +317,9 @@ Detalles Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso + Recibir activos + Hola equipo de soporte, he encontrado un error con el código: %s + Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. Mis tokens @@ -358,6 +377,7 @@ Proveedor Mejor tarifa Lista de advertencias de la FCA + Proveedor en la lista de advertencia de la FCA Disponible hasta %s Disponible desde %s No disponible para este par @@ -405,6 +425,8 @@ Escanee a %s En la red %s + ¿Está seguro de que desea salir del proceso de creación del código de acceso? + Si lo haces, tendrás que empezar de nuevo. Mantente al día con las últimas funciones y noticias Esta información fue generada con IA.\nPulse aquí si encuentra algún error. Para cambiar el código de acceso coloque la tarjeta o el anillo como se muestra arriba y no lo retire hasta el fin de la operación @@ -908,9 +930,14 @@ Cantidad no válida La tarifa excede el saldo El monto total excede el saldo + Intercambiar y enviar + ¿Continuar con la conversión? Esto borrará tus datos anteriores. + Confirmar Conversión Envía cualquier token y lo convertiremos en el camino. Su destinatario obtiene exactamente lo que necesita, sin problemas. - Se enviará un destinatario + El destinatario recibirá + Al destinatario Cantidad a recibir + ¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores. Enviar con swap Transacción enviada Escanee la tarjeta/anillo que quiere configurar @@ -1210,8 +1237,10 @@ Consíguelo ahora con un 10 % de descuento Accede a más de 13 000 criptomonedas. Compra, vende, intercambia y realiza staking con un solo toque.\nVincula hasta tres tarjetas para hacer copias de seguridad. Descubre Tangem Wallet + Cambiar código de acceso Manténteinformado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem. Notificaciones de transacciones + Establecer código de acceso Ajustes de la wallet Tangem Use %s o escanee una tarjeta/anillo para desbloquear el acceso a su billetera @@ -1308,16 +1337,27 @@ Se requiere línea de confianza Dominio malicioso Dominio desconocido + Conectarse de todas formas + Error de tiempo de espera. Por favor, inténtalo de nuevo más tarde. + Error al establecer WalletConnect Este dominio no puede ser verificado. Compruebe cuidadosamente la solicitud de aprobación. Vuelva a su navegador y vuelva a conectarse a través de WalletConnect. + La sesión de Wallet Connect se desconectó Firmar de todos modos Código de error: %s. Si el problema persiste - no dude en ponerse en contacto con nuestro soporte. Si el problema persiste, no dudes en ponerte en contacto con nuestro soporte. + Hemos encontrado un error desconocido + Tangem Wallet actualmente no es compatible con %s. + dApp no compatible Código de error: 8 005. Si el problema persiste, no dudes en contactar con nuestro soporte. Hemos encontrado un error desconocido Actualmente, Tangem no es compatible con una red requerida por %s. + Redes no compatibles Tangem soporta una red requerida por %s + Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app + Tenemos algún tipo de problema + Todas las dApps desconectadas Permitir gastar Dirección Conectar @@ -1326,30 +1366,59 @@ Redes Ilimitado Billetera + Aplicación conectada Redes conectadas + Conectado a %1$s Ver el saldo y la actividad de su billetera Firmar transacciones sin previo aviso Solicitar aprobación para transacciones No podrá Me gustaría Solicitud de conexión + Conexiones Contenido Copiar datos Asignación personalizada + dApp desconectada + Desconectar todo + Texto sobre desconexión de todas las dApps + Desconectar todas las dApps + Intente emparejar nuevamente con una URI nueva + Dominio de dApp no válido + %s no especifica ninguna cadena de bloques, ni obligatoria ni opcional. Asegúrese de haber utilizado el URI correcto + Sin redes + Por favor, genere una nueva URI e intente conectarse nuevamente + Propuesta de conexión caducada Cambios estimados en la billetera La transacción no ha podido ser simulada. Por favor, proceda con precaución. + La estimación no es compatible con %s + Sugerido por %s + Recargue su saldo para cubrir la tarifa de red + Insuficiente %1$s Transacción maliciosa Añada la red %s a su perfil para esta billetera La billetera no tiene las redes requeridas + Nueva conexión + Conecta tu billetera a diferentes dApps + Sin sesiones + No se han detectado cambios en la cartera Se han detectado riesgos potenciales o comportamiento malicioso. Conectarse o firmar transacciones puede resultar en la pérdida de fondos. Riesgo de seguridad conocido + Abra la aplicación Web3 y elija la opción WalletConnect Solicitud de + Firmar de todos modos Tipo de firma + Se requiere al menos una red para la conexión dApp + Especificar las redes seleccionadas + Firmado correctamente A Solicitud de transacción Solicitud de transacción Cantidad ilimitada + Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único + URI ya utilizado Wallet connect + Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? Sí, reanudar diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index bbf154df4e..00b0250a97 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -124,6 +124,7 @@ Réclamez des récompenses Fermer Confirmez + Connexion Continuer Convertir Copier @@ -222,6 +223,10 @@ termes et conditions Conditions d\'utilisation Aujourd\'hui + + %d jeton + %d jetons + La transaction a échoué Statut de la transaction Transactions @@ -288,6 +293,8 @@ Détails Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation + Bonjour équipe de support, j’ai rencontré une erreur avec le code : %s + Erreur WalletConnect Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille Pas assez de fonds pour la transaction. Veuillez recharger votre compte. Mes jetons @@ -1327,6 +1334,12 @@ Déconnecter tout Texte sur la déconnexion de toutes les dApps Déconnecter toutes les dApps + Essayez de jumeler à nouveau avec un nouvel URI + Domaine dApp invalide + %s ne spécifie aucune blockchain — ni requise ni optionnelle.\nVeuillez vous assurer que vous avez utilisé l’URI correct + Aucun réseau + Veuillez générer un nouvel URI et essayer de vous connecter à nouveau + La proposition de connexion a expiré Modifications estimées du portefeuille La transaction n\'a pas pu être simulée. Veuillez procéder avec prudence. Transaction malveillante diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 5caec91961..a585f2e30a 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -10,6 +10,8 @@ Impossibile creare la transazione Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy Annulla + Scegli portafoglio + Connessione in corso Rimuovere Fatto Errore @@ -20,6 +22,10 @@ Invia Impossibile inviare la transazione Con successo + + %d gettone + %d gettoni + Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta @@ -34,6 +40,8 @@ Firmato Requisiti Termini del servizio + Ciao team di supporto, ho riscontrato un errore con il codice: %s + Errore WalletConnect Not enough funds for the transaction. Please top up your account. An error occurred Notifica richiesta @@ -66,6 +74,12 @@ Nessuna connessione a Internet Tangem Ok, ho capito! + Prova a eseguire nuovamente l’associazione con un nuovo URI + Dominio dApp non valido + %s non specifica alcuna blockchain — né obbligatoria né opzionale.\nAssicurati di aver utilizzato l’URI corretto + Nessuna rete + Genera un nuovo URI e prova a connetterti di nuovo + La proposta di connessione è scaduta No, invia l\'intero importo Riduci di %s XTZ Per evitare di pagare una commissione maggiore la prossima volta che ricarichi il tuo portafoglio, riduci l\'importo di %s XTZ diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index cf05e40cb9..ff32b8bf8b 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,6 +12,10 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + アカウントをアーカイブする + アーカイブ + このアカウントをアーカイブしますが、いつでも復元できます。 + アカウント アカウントを追加 保存 アカウント名 @@ -58,12 +62,16 @@ %1$s ネットワークのトークンは、ファームウェアの制限により、このカードまたはリングではサポートされていません。 カードまたはリングのスキャンに問題がありますか? このカードはこのアプリでは使用できません。 + %1$sを使用すると、ウォレットのロックを迅速かつ安全に解除できます。また、取引の署名など、機密性の高い操作も承認できます。ハードウェアウォレットの場合は、署名にカードが必要です。 デフォルト手数料 デフォルト手数料を有効にすると、取引手数料が自動的に設定され、送金時に手数料ページを表示する必要がなくなります。必要に応じて、いつでもこのページに戻ることができます。 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする + %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 + アクセスコードを要求する + このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。 アクセスコードを保存 カードまたはリングとのやり取りには、アクセスコードの代わりに生体認証が要求されます。 ウォレットをアプリに保存する @@ -89,6 +97,7 @@ これらの%s個の単語をパスワードマネージャーなどの安全な場所に保存し、決して他の人と共有しないでください。 復元は不可能です リカバリーフレーズ + これらの単語は、絶対に誰にも共有しないでください。もし他人がこの単語を知れば、あなたの暗号資産をすべて盗むことができます。Tangemがこれらの単語を尋ねることはありません。以下の%s単語はウォレットの復元フレーズです。これらのフレーズを使用すると、デバイスを紛失した場合でもウォレットを復元できます。 これらの%s語を順番に書き留めて、安全かつプライベートに保管してください。 ウォレットと、リカバリーフレーズのセキュリティとバックアップの全責任は、Tangemではなくユーザーにあります。 リカバリーフレーズ @@ -166,6 +175,7 @@ 閉じる 近日公開 確認 + 接続中 Tangemサポートへ問い合わせる Visaサポートへ問い合わせる 続ける @@ -182,6 +192,7 @@ 削除 + 無効にする 無効 切断 完了 @@ -221,6 +232,7 @@ いいえ アドレスがありません 未追加 + 今はしない わかりました ブラウザで開く @@ -267,6 +279,9 @@ 利用規約 利用規約 今日 + + %d トークン + 取引が失敗しました 取引状況 取引 @@ -333,9 +348,12 @@ 詳細 インターネット接続を確認するか、別のネットワークに切り替えてください。 利用規約 + 資金を受け取る 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する + サポートチームの皆様、コード %s のエラーが発生しました。 + WalletConnect エラー 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 取引に必要な資金が不足しています。アカウントに入金してください。 マイトークン @@ -441,10 +459,15 @@ Tangemをスキャン %sへ %sネットワーク + アクセスコードの作成プロセスを終了してもよろしいですか? 今すぐバックアップ セットアップを完了するには、ウォレットをバックアップし、アクセスコードを使用してアプリへのアクセスを保護します。 今すぐ実施 ウォレットのアクティベーションを完了する + セットアップを完了するには、アクセスコードを使用してアプリへのアクセスを保護します。 + そうした場合は、最初からやり直す必要があります。 + アクティベーションプロセスを終了してもよろしいですか? + 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップに保存されている既存のウォレットを復元する Googleドライブのバックアップ バックアップへ移動 @@ -458,6 +481,7 @@ 最新の機能とニュースをお届けします シードフレーズのバックアップ モバイルウォレットを作成する + モバイルウォレット この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。 @@ -954,15 +978,17 @@ 無効な金額 手数料が残高を超えています 合計金額が残高を超えています - トークンを変更してもよろしいですか? 変更後、以前のデータはリセットされます。 + 受信トークンを変更してもよろしいですか? 変更すると、以前入力したデータがリセットされます。 トークンの変更 スワップして送信 + 変換を続行しますか? これにより以前のデータは消去されます。 + 変換を確定 トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります - 受信者に送信されます + 受取人へ 受取金額 - 変換をキャンセルしてもよろしいですか? 変更後、以前のデータはリセットされます。 - キャンセルの確認 + 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 + 変換を削除 スワップして送信 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 @@ -1420,7 +1446,7 @@ エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 - サポートされていないネットワーク + 未対応のネットワーク Tangemは%sで必要なネットワークをサポートします 検証済みドメイン アプリで間違ったカードまたはリングが選択されました @@ -1451,6 +1477,12 @@ すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する + 新しいURIで、再度ペアリングを試してください + 無効なdAppドメイン + %sはブロックチェーンを指定していません(必須でもオプションでもありません)。 \n正しいURIを使用していることを確認してください。 + ネットワークなし + 新しいURIを生成し、再度接続してください + 接続提案の期限が切れました ウォレットの変更の予測 取引をシミュレーションできませんでした。注意して続行してください。 %sでは見積もりはサポートされていません @@ -1461,13 +1493,14 @@ このウォレットのポートフォリオに%sネットワークを追加します ウォレットに必要なネットワークはありません 新しい接続 - ウォレットを別のdAppに接続する + ウォレットをさまざまなdAppに接続する セッションなし ウォレットの変更は検出されませんでした - このドメインは複数のセキュリティプロバイダーから安全でないとの警告を受けています。あなたの資産を守るため、直ちにアクセスを中止してください。 + 潜在的なリスクまたは悪意のある行為が検出されました。接続または取引への署名は資金の損失につながる可能性があります。 既知のセキュリティリスク + Web3アプリを開き、WalletConnectオプションを選択します リクエスト元 - とにかく送金 + とにかく署名する 署名タイプ dApp接続には、少なくとも1つのネットワークが必要です 選択したネットワークを指定する diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 10a53f974e..5b238275be 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -116,11 +116,12 @@ Выберите действие Выберите сеть Выберите токен - Выберите кошелек + Выберите кошелёк Получить Вывести награду Закрыть Подтвердить + Подключение Продолжить Копировать Скопировать адрес @@ -219,6 +220,12 @@ условия участия Условиями использования Сегодня + + %d токен + %d токена + %d токенов + %d токенов + Ошибка транзакции Статус транзакции Транзакции @@ -284,6 +291,8 @@ Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования + Привет, команда поддержки, у меня возникла ошибка с кодом: %s + Ошибка WalletConnect Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком. Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Мои токены @@ -895,6 +904,18 @@ Недопустимая сумма Комиссия превышает остаток Отправляемая сумма превышает остаток + Вы уверены, что хотите изменить токен для получения? Это действие сбросит ранее введённые данные. + Изменение токена + Обмен и отправка + Продолжить с конвертацией? Это действие удалит предыдущие данные + Подтвердить конвертацию + Отправьте любой токен, и мы конвертируем его по пути. Адресат получит именно то, что нужно — без лишних действий. + Будет получено + Получателю + Сумма к получению + Вы уверены, что хотите отменить конвертацию? Ваши предыдущие данные будут удалены. + Убрать конвертацию + Отправка с обменом Транзакция отправлена Подготовьтесь к сканированию кольца или карты, которую вы хотите настроить. Забыть кошелек @@ -1285,7 +1306,7 @@ Требуется трастлайн Вредоносный домен Неизвестный домен - Подключиться всё равно + Всё равно подключиться Ошибка тайм-аута. Пожалуйста, попробуйте позже. Не удалось подключиться через Wallet Connect Этот домен не может быть верифицирован. Внимательно проверьте запрос перед одобрением. @@ -1329,6 +1350,12 @@ dApp отключен Отключить все Отключить все dApp + Попробуйте соединиться снова, используя новый URI + Недействительный домен dApp + %s не указывает никаких блокчейнов — ни обязательных, ни опциональных.\nПожалуйста, убедитесь, что вы использовали правильный URI + Нет сетей + Пожалуйста, сгенерируйте новый URI и попробуйте подключиться снова + Предложение подключения истекло Предварительные изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. Оценка не поддерживается для %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 9c820fac97..79d1d3c092 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -120,6 +120,7 @@ Отримати винагороди Закрити Підтвердити + Підключення Продовжити Копіювати Скопіювати адресу @@ -191,6 +192,7 @@ Відхилити Оновити Перейменувати + Обов\'язково Зберегти Зберегти зміни Пошук @@ -218,6 +220,12 @@ умови участі Умовами використання Сьогодні + + %d токен + %d токени + %d токенів + %d токенів + Помилка транзакції Статус транзакції Транзакції @@ -282,6 +290,8 @@ Деталі Перевірте підключення до інтернету або змініть мережу Умови використання + Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s + Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. Мої токени @@ -1282,18 +1292,64 @@ Відкрити Trustline Щоб отримати цей токен, потрібно увімкнути Trustline. Мережа вимагає резерв %1$s %2$s. Відкрийте Trustline + Невідомий домен + Все одно підключитися + Помилка тайм-ауту. Будь ласка, спробуйте пізніше. + Не вдалося зʼєднатися через Wallet Connect + Цей домен не може бути підтверджений. Уважно перевірте запит перед схваленням. Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. + Сеанс Wallet Connect було завершено + Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. + Ми зіткнулися з невідомою помилкою + Tangem наразі не підтримує необхідну мережу для %s. + Непідтримувані мережі + Tangem підтримує мережу, необхідну для %s + Верифікований домен Обрана не вірна картка або кільце + Схоже, виникла проблема + Усі dApps відключені Адреса Підключення + Мережа + Мережі + Гаманець + Підключений додаток Підключені мережі + Підключений до %1$s Переглянути баланс гаманця та активність + Підписати транзакцію без вашої участі + Запит схвалення на транзакцію + Не має можливості + Хотіли б Запит на підключення + З\'єднання Вміст Копіювати дані + dApp відключено + Розʼєднати все + Відключити всі dApps + Відключити всі dApps + Спробуйте ще раз з новим URI + Недійсний домен dApp + %s не вказує жодних блокчейнів — ні обов\'язкових, ні необов\'язкових. \n Переконайтеся, що ви використали правильний URI + Немає мереж + Будь ласка, згенеруйте новий URI та спробуйте ще раз + Термін для з’єднання минув + Поповніть баланс, щоб покрити комісію мережі + Недостатньо %1$s + Додайте %s мережі до вашого портфелю для цього гаманця + В гаманці не додані необхідні мережі + Нове з\'єднання + Підключайте свій гаманець до різних dApps + Немає підключень Виявлено потенційні ризики або шкідливу активність. Підключення чи підпис транзакцій можуть призвести до втрати коштів. + Відомий ризик безпеки + Відкрийте програму Web3 та виберіть опцію WalletConnect Запит від Тип підпису + Для підключення dApp потрібна принаймні одна мережа + Вкажіть вибрані мережі + Успішно підписано До Запит транзакції Запит транзакції diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index b33588b325..713520796a 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -52,7 +52,9 @@ 您尚未授予相機訪問權限,請更改您的隱私設置 删除 選擇代幣 + 選擇錢包 關閉 + 連線中 繼續 複製 複製地址 @@ -90,6 +92,9 @@ 成功 交換 條款和條件 + + %d 代幣 + 交易 我了解 無法觸達 @@ -129,6 +134,8 @@ 更多 檢查您的網路連接或切換到其他網絡 服務條款 + 您好,支援團隊,我遇到了一個錯誤,代碼為:%s + WalletConnect 錯誤 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 沒有足夠的資金進行交易。請先入金 以下信息是可選的。如果不想共享,可以將其刪除 @@ -372,6 +379,12 @@ 認證檢查失敗 網路無法使用 Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用 + 請使用新的 URI 再次嘗試配對 + 無效的 dApp 網域 + %s 未指定任何區塊鏈——無論是必須的還是可選的。\n請確保您使用了正確的 URI + 沒有網路 + 請生成新的 URI,然後再次嘗試連線 + 連線提案已過期 捨棄 您有一個備份中斷了,您想繼續嗎? 是的,恢復 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4c933aff60..4089de8ca8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -12,6 +12,12 @@ Set a %s-digit Access Code to unlock your wallet. Create Access Code Access code + Recover + Archived + Archive account + Archive + You are archiving this account, but you can always get it back. + Account Add account Save Account name @@ -63,6 +69,7 @@ Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication + Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. Require Access Code @@ -173,6 +180,7 @@ Close Coming Soon Confirm + Connecting Contact Tangem Support Contact Visa Support Continue @@ -231,6 +239,7 @@ No No address Not Added + Not Now Now OK Open in Browser @@ -278,6 +287,10 @@ terms and conditions Terms of Use Today + + %d token + %d tokens + Transaction failed Transaction status Transactions @@ -348,6 +361,8 @@ Sending assets in other networks will result in permanent loss. %s network Send funds using only + Hi support team, I\'ve encountered an error with code: %s + WalletConnect error You have used a card or ring from another wallet. Tap the card or ring associated with this wallet Not enough funds for the transaction. Please top up your account. My tokens @@ -453,6 +468,7 @@ Scan Tangem to %s On %s network + Are you sure you want to exit the access code creation process? Backup Now To complete setup, back up your wallet and secure app access with a Access Code. Finish Now @@ -460,6 +476,7 @@ To complete setup, secure app access with Access Code. If you do, you\'ll need to start over. Are you sure you want to exit the activation process? + If you do, you\'ll need to start over. Recover an existing wallet stored in your Google Drive backup Google Drive Backup Go to backup @@ -979,15 +996,18 @@ Invalid amount Fee exceeds balance Total amount exceeds balance - Are you sure you want to change the token? After changing, previous data will be reset. + Are you sure you want to change the receiving token? This will reset your previously entered data. Changing token Swap and send + Proceed with conversion? This will clear your previous data. + Confirm Conversion Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive - Will be sent a recipient + To recipient Amount to receive - Are you sure you want to cancel the conversion? After changing, previous data will be reset. - Confirm cancelation + Recipient get %s + Are you sure you want to cancel the conversion? Your previous data will be cleared. + Remove Conversion Send with swap Transaction sent Prepare to scan card or ring you want to set up. @@ -1525,6 +1545,12 @@ Disconnect all Text about discnected all dApps Disconect All dApps + Try pairing again with a fresh URI + Invalid dApp domain + %s does not specify any blockchains — neither required nor optional.\nPlease ensure you used the correct URI + No networks + Please, generate a new URI and attempt connecting again + Connection proposal expired Estimated wallet changes The transaction couldn\'t be simulated. Please proceed with caution. Estimation is not supported for %s @@ -1535,18 +1561,19 @@ Add the %s network to your portfolio for this wallet The wallet has no required networks New connection - Connect your wallet to a different dApps + Connect your wallet to different dApps No sessions No wallet changes detected Potential risks or malicious behavior have been detected. Connecting or signing transactions may lead to loss of funds. Known security risk + Open Web3 app and chose WalletConnect option Request from Sign anyway Signature Type At least one network is required for dApp connection Specify selected networks Successfully signed - Wallet connect + WalletConnect To Transaction request Transaction request diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index bfd1192118..c9e11ec7fb 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime import timber.log.Timber +import java.net.URI @Suppress("LongParameterList") internal class DefaultWcPairUseCase @AssistedInject constructor( @@ -63,6 +64,12 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( return@flow } + val dAppUri = URI(sdkSessionProposal.url) + if (!dAppUri.host.isNullOrEmpty()) { + emit(WcPairState.Error(WcPairError.InvalidDomainURL)) + return@flow + } + val proposalState = buildProposalState(sdkSessionProposal, sdkVerifyContext) .onLeft { analytics.send(WcAnalyticEvents.PairFailed(it.code)) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index e9aa7aa21c..b41e493f67 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -114,6 +114,7 @@ internal class WcPairComponent( return when (alertType) { is Alert.Type.Verified -> WcAlertsFactory.createVerifiedDomainAlert(alertType.appName) is Alert.Type.UnknownDomain -> WcAlertsFactory.createUnknownDomainAlert(model::connectFromAlert) + is Alert.Type.InvalidDomain -> WcAlertsFactory.createInvalidDomainAlert(model::errorAlertOnDismiss) is Alert.Type.UnsafeDomain -> WcAlertsFactory.createUnsafeDomainAlert(model::connectFromAlert) is Alert.Type.UnsupportedDApp -> WcAlertsFactory.createUnsupportedDomainAlert(alertType.appName, model::errorAlertOnDismiss) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index c1dcb8cf95..7c566be7c6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -216,6 +216,9 @@ internal class WcPairModel @Inject constructor( private fun processError(error: WcPairError) { val alert = when (error) { + is WcPairError.InvalidDomainURL -> { + WcAppInfoRoutes.Alert.Type.InvalidDomain + } is WcPairError.UnsupportedDApp -> { WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index 27a3879932..226ececbbc 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -32,6 +32,7 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { data class Verified(val appName: String) : Type() data object UnknownDomain : Type() data object UnsafeDomain : Type() + data object InvalidDomain : Type() data class UnsupportedDApp(val appName: String) : Type() data class UnsupportedNetwork(val appName: String) : Type() data object UriAlreadyUsed : Type() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt index 095ccd662e..a17284a34d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt @@ -48,6 +48,24 @@ internal object WcAlertsFactory { } } + fun createInvalidDomainAlert(onDismiss: () -> Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_wallet_connect_24) { + type = Type.Informative + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.wc_errors_invalid_domain_title) + body = resourceReference(R.string.wc_errors_invalid_domain_subtitle) + } + primaryButton { + text = resourceReference(R.string.common_got_it) + onClick { onDismiss() } + } + onDismissRequest = onDismiss + } + } + fun createUnsafeDomainAlert(activeButtonOnClick: (() -> Unit)? = null): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt index c9f6f86182..49aadb776f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcSendTransactionUMConverter.kt @@ -54,10 +54,18 @@ internal class WcSendTransactionUMConverter @Inject constructor( isLoading = value.signState.domainStep == WcSignStep.Signing, address = WcAddressConverter.convert(value.context.derivationState), transactionValidationResult = value.securityCheck?.result?.validation, - sendEnabled = value.feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null, + sendEnabled = when (value.feeState) { + WcTransactionFeeState.None -> feeErrorNotification == null + is WcTransactionFeeState.Success -> { + value.feeSelectorUM is FeeSelectorUM.Content && feeErrorNotification == null + } + }, feeErrorNotification = feeErrorNotification, ), - feeSelectorUM = value.feeSelectorUM ?: FeeSelectorUM.Loading, + feeSelectorUM = when (value.feeState) { + WcTransactionFeeState.None -> FeeSelectorUM.Loading + is WcTransactionFeeState.Success -> value.feeSelectorUM ?: FeeSelectorUM.Loading + }, transactionRequestInfo = WcTransactionRequestInfoUM( blocks = buildList { addAll( From d00064cee3e3e6047f192eee239c2e8fd513a45e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 18:15:10 +0500 Subject: [PATCH 18/87] Updated on 2026-08-14 --- .../com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt | 2 +- .../com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index c9e11ec7fb..0e78ed19ce 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -65,7 +65,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } val dAppUri = URI(sdkSessionProposal.url) - if (!dAppUri.host.isNullOrEmpty()) { + if (dAppUri.host.isNullOrEmpty()) { emit(WcPairState.Error(WcPairError.InvalidDomainURL)) return@flow } diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index df3b560b15..573d2949bc 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -47,7 +47,7 @@ internal class DefaultWcPairUseCaseTest { pairingTopic = "", name = "", description = "", - url = "", + url = "https://react-app.walletconnect.com/", icons = listOf(), redirect = "", requiredNamespaces = mapOf(), From 57439e1682a0ef000178a6753f78ea21adba22e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 13:36:06 +0500 Subject: [PATCH 19/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 1 + core/res/src/main/res/values-es/strings.xml | 1 + core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values-uk-rUA/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 4 ++++ 7 files changed, 10 insertions(+) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7bc007eb36..554f254397 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -118,6 +118,7 @@ Zugang verweigert Zum Portfolio hinzufügen Token hinzufügen + Vertragsadresse Alle Erlauben Betrag diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 8d38231daf..b5eeeccbc8 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -115,6 +115,7 @@ Acceso denegado Añadir al portafolio Agregar token + Dirección Todos Autorizar Montante diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 00b0250a97..8e7d9898e8 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -97,6 +97,7 @@ Accès refusé Ajouter au portfolio Ajouter un jeton + Adresse Tous Permettre Montant diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index ff32b8bf8b..2986582d9d 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -146,6 +146,7 @@ アクセスが拒否されました ポートフォリオに追加 トークンを追加 + アドレス すべて 許可する 金額 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5b238275be..32374c0f84 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -94,6 +94,7 @@ Доступ запрещен Добавить в портфель Добавить токен + Адрес Все Разрешить Сумма diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 79d1d3c092..0b17da86ca 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -94,6 +94,7 @@ Доступ заборонено Додати у портфель Додати токен + Адреса Усе Дозволити Сума diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4089de8ca8..f0624f96ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Archive You are archiving this account, but you can always get it back. Account + Account #%s — used for address derivation. Add account Save Account name @@ -151,6 +152,7 @@ Access denied Add to portfolio Add token + Address All Allow Amount @@ -1001,6 +1003,8 @@ Swap and send Proceed with conversion? This will clear your previous data. Confirm Conversion + Sending any other currency will result in its irreversible loss. + Select the correct recipient network Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive To recipient From 2d96d77fe39e3ecf4712476ef300974c27c9441d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 13:42:16 +0500 Subject: [PATCH 20/87] Updated on 2026-08-14 --- .../com/tangem/features/send/v2/send/DefaultSendComponent.kt | 2 +- .../swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt | 2 +- .../walletconnect/transaction/ui/common/WcAddressItem.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 47c52407c5..9ac55d3a43 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -156,7 +156,7 @@ internal class DefaultSendComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index ed82f2f734..bc241e0ec0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -160,7 +160,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( currentRoute = model.currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, analyticsCategoryName = model.analyticCategoryName, - title = resourceReference(R.string.send_recipient_label), + title = resourceReference(R.string.common_address), userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, callback = model, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt index 35e9a28c76..b8c0eb3057 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcAddressItem.kt @@ -30,7 +30,7 @@ internal fun WcAddressItem(address: String, modifier: Modifier = Modifier) { ) Text( modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - text = stringResourceSafe(R.string.wc_common_address), + text = stringResourceSafe(R.string.common_address), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, maxLines = 1, From c469d4133eba619b70b42a2a35ec1462a90b20a3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 13:43:24 +0500 Subject: [PATCH 21/87] Updated on 2026-08-14 --- .../subcomponents/destination/analytics/EnterAddressSource.kt | 3 +++ .../v2/subcomponents/destination/model/SendDestinationModel.kt | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt index 489d2d0bc6..7dc514bb33 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt @@ -10,4 +10,7 @@ internal enum class EnterAddressSource { val isPasted: Boolean get() = this != InputField + + val isAutoNext: Boolean + get() = this == RecentAddress || this == MyWallets } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index b457df2b9a..d68153cd9e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -286,8 +286,7 @@ internal class SendDestinationModel @Inject constructor( } private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { - val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress && isValidMemo) { + if (type?.isAutoNext == true && isValidAddress && isValidMemo) { saveResult() (params as? SendDestinationComponentParams.DestinationParams)?.callback?.onNextClick() } From b58df2dfb713feae2a301260695c78bf5ba76712 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 12:31:22 +0300 Subject: [PATCH 22/87] Updated on 2026-08-14 --- .../java/com/tangem/core/ui/components/fields/PinTextField.kt | 2 +- .../impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt | 3 +++ .../v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index fa00fb9959..f46d5a579c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -58,7 +58,7 @@ fun PinTextField( } .focusRequester(focusRequester), keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, + keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done, ), singleLine = true, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index 6c8cb1fc1b..d4c055fcda 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -73,6 +75,7 @@ internal fun MultiWalletAccessCodeEnter( label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), isError = state.codesNotMatchError, visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt index cb2d5993c8..094a37d884 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -153,7 +153,7 @@ private fun PinCode( } }, keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number, + keyboardType = KeyboardType.NumberPassword, imeAction = ImeAction.Done, ), keyboardActions = KeyboardActions( From 2557efe5434ca7c9c1943b54b42b140c6972a718 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:21:14 +0500 Subject: [PATCH 23/87] Updated on 2026-08-14 --- .../ui/amountScreen/converters/AmountStateConverter.kt | 2 ++ .../converters/field/AmountBoundaryUpdateTransformer.kt | 1 + .../com/tangem/common/ui/amountScreen/models/AmountState.kt | 4 +++- .../common/ui/amountScreen/preview/AmountStatePreviewData.kt | 3 ++- .../com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt | 2 +- .../SendConfirmationNotificationsTransformerTest.kt | 1 + .../SendConfirmationNotificationsTransformerV2Test.kt | 1 + .../confirm/model/transformers/TransformersComparisonTest.kt | 1 + .../swap/v2/impl/amount/ui/SwapAmountBlockContent.kt | 5 ++--- 9 files changed, 14 insertions(+), 6 deletions(-) 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 ac0ebecf2f..2b60066ea7 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 @@ -60,6 +60,7 @@ class AmountStateConverter( return AmountState.Data( title = value.title, availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + availableBalanceShort = stringReference(crypto), tokenName = stringReference(status.currency.name), tokenIconState = iconStateConverter.convert(status), amountTextField = amountFieldConverter.convert(value.value), @@ -130,6 +131,7 @@ class AmountStateConverterV2( } else { resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)) }, + availableBalanceShort = stringReference(crypto), tokenName = stringReference(cryptoCurrencyStatus.currency.name), tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency), amountTextField = amountFieldConverter.convert(value.value), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index 89c1074a49..7385fa9542 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -48,6 +48,7 @@ class AmountBoundaryUpdateTransformer( return prevState.copy( availableBalance = availableBalance, + availableBalanceShort = stringReference(crypto), ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index cc23669f47..cac4628783 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 @@ -17,7 +17,8 @@ sealed class AmountState { /** * @param isPrimaryButtonEnabled indicates if next state button enabled * @param title title - * @param availableBalance user crypto currency balance + * @param availableBalance user crypto currency balance with fiat balance + * @param availableBalanceShort user crypto currency balance without fiat balance * @param tokenIconState crypto currency icon state * @param segmentedButtonConfig currency switcher config * @param selectedButton selected currency index @@ -33,6 +34,7 @@ sealed class AmountState { override val isRedesignEnabled: Boolean, val title: TextReference, val availableBalance: TextReference, + val availableBalanceShort: TextReference, val tokenName: TextReference, val tokenIconState: CurrencyIconState, val segmentedButtonConfig: PersistentList, 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 99c11a9a14..a2db25c4dd 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 @@ -24,7 +24,8 @@ object AmountStatePreviewData { val amountState = AmountState.Data( isPrimaryButtonEnabled = false, title = stringReference("Family Wallet"), - availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), + availableBalance = stringReference("2 130,88 USDT • 2 129,92 \$)"), + availableBalanceShort = stringReference("2 130,88 USDT"), tokenIconState = CurrencyIconState.Loading, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt index ee080d579f..979a1a4419 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlockV2.kt @@ -64,7 +64,7 @@ fun AmountBlockV2( AmountBlockV2( title = amountState.title, - balance = amountState.availableBalance, + balance = amountState.availableBalanceShort, currencyTitle = currencyTitle, currencyIconState = amountState.tokenIconState, firstAmount = firstAmount, diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt index 6eece69628..020d3afcab 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt @@ -205,6 +205,7 @@ class SendConfirmationNotificationsTransformerTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 873cfb305a..6903fc2550 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -204,6 +204,7 @@ class SendConfirmationNotificationsTransformerV2Test { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt index 79cba32b78..b07143a6b1 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt @@ -307,6 +307,7 @@ class TransformersComparisonTest { isRedesignEnabled = false, title = mockk(relaxed = true), availableBalance = mockk(relaxed = true), + availableBalanceShort = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), segmentedButtonConfig = persistentListOf(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index b788f33190..c68f7e3923 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -72,14 +72,13 @@ internal fun SwapAmountBlockContent( start.linkTo(parent.start) end.linkTo(parent.end) }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) - }, + extraContent = { SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) }, ) AmountBlockV2( amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( title = resourceReference(R.string.send_with_swap_recipient_amount_title), availableBalance = TextReference.EMPTY, + availableBalanceShort = TextReference.EMPTY, ) ?: amountUM.secondaryAmount.amountField, isClickDisabled = true, isEditingDisabled = false, From bea775e94bc0b8b5c8a9c24403612568b715eede Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 14:47:48 +0300 Subject: [PATCH 24/87] Updated on 2026-08-14 --- .../java/com/tangem/tap/LockTimerWorker.kt | 17 ++- .../com/tangem/tap/LockUserWalletsTimer.kt | 35 ++++-- .../main/java/com/tangem/tap/MainActivity.kt | 68 +++++++++++ .../tap/data/RuntimeUserWalletsStore.kt | 5 - .../data/UserWalletsStoreRepositoryProxy.kt | 50 ++++++++ .../tap/di/data/UserWalletsStoreModule.kt | 15 ++- .../tangem/tap/di/domain/CardDomainModule.kt | 15 ++- .../tap/di/domain/CardLegacyDomainModule.kt | 14 ++- .../tap/di/domain/MarketsDomainModule.kt | 6 + .../tap/di/domain/WalletsDomainModule.kt | 115 +++++++++++++++--- .../DefaultUserWalletsListRepository.kt | 25 ++-- .../UserWalletEncryptionKeysRepository.kt | 17 +-- .../di/WalletConnectInteractorModule.kt | 6 +- .../domain/WalletConnectInteractor.kt | 7 +- .../tap/network/auth/DefaultAuthProvider.kt | 35 ++++-- .../tangem/tap/network/auth/di/AuthModule.kt | 14 ++- .../di/FeatureTogglesManagerModule.kt | 7 ++ .../feature/impl/DevFeatureTogglesManager.kt | 23 ++-- .../feature/impl/ProdFeatureTogglesManager.kt | 11 +- .../datasource/api/common/AuthProvider.kt | 6 +- .../local/userwallet/UserWalletsStore.kt | 6 +- .../managers/ProdApiConfigsManagerTest.kt | 5 +- .../wallets/DefaultWalletsRepositoryTest.kt | 2 +- ...FilterAvailableNetworksForWalletUseCase.kt | 12 +- .../DefaultUserWalletsSyncDelegate.kt | 38 +++++- .../wallets/models/SelectWalletError.kt | 6 - .../wallets/usecase/DeleteWalletUseCase.kt | 13 +- .../usecase/GenerateWalletNameUseCase.kt | 16 ++- .../usecase/GetSavedWalletsCountUseCase.kt | 7 ++ .../usecase/GetSelectedWalletSyncUseCase.kt | 13 +- .../usecase/GetSelectedWalletUseCase.kt | 26 +++- .../wallets/usecase/GetUserWalletUseCase.kt | 23 +++- .../wallets/usecase/GetWalletNamesUseCase.kt | 14 ++- .../wallets/usecase/GetWalletsUseCase.kt | 22 +++- .../wallets/usecase/IsNeedToBackupUseCase.kt | 17 ++- .../wallets/usecase/SaveWalletUseCase.kt | 56 +++++++-- .../wallets/usecase/SelectWalletUseCase.kt | 12 +- .../wallets/usecase/UpdateWalletUseCase.kt | 36 +++++- .../GetSavedWalletsCountUseCaseTest.kt | 6 +- .../biometry/impl/model/AskBiometryModel.kt | 6 +- .../model/MultiWalletFinalizeModel.kt | 16 +-- .../v2/twin/impl/model/OnboardingTwinModel.kt | 31 +++-- .../model/OnboardingVisaInProgressModel.kt | 6 +- .../feature/swap/DefaultSwapRepository.kt | 8 +- .../tangem/feature/swap/di/SwapDataModule.kt | 6 +- .../domain/WalletNameMigrationUseCase.kt | 33 +++-- 46 files changed, 749 insertions(+), 178 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index e51fc573ea..f6b3c93141 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -7,6 +7,8 @@ import androidx.work.WorkerParameters import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedInject import timber.log.Timber @@ -17,13 +19,22 @@ class LockTimerWorker @AssistedInject constructor( @Assisted params: WorkerParameters, private val settingsRepository: SettingsRepository, private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { Timber.i("onStart job") - val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() - userWalletsListManagerLockable.lock() - settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.lockAllWallets() + .onRight { + settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + } + } else { + val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() + userWalletsListManagerLockable.lock() + settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) + } Timber.i("onStart job complete") return Result.success() } diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 57d5d8c6ca..e3d0a18067 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -10,6 +10,8 @@ import com.tangem.common.routing.AppRoute import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.common.extensions.dispatchNavigationAction import kotlinx.coroutines.CoroutineScope @@ -25,6 +27,8 @@ internal class LockUserWalletsTimer( private val settingsRepository: SettingsRepository, private val duration: Duration = with(Duration) { 5.minutes }, private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val coroutineScope: CoroutineScope, ) : LifecycleOwner by context as LifecycleOwner, DefaultLifecycleObserver { @@ -108,20 +112,33 @@ internal class LockUserWalletsTimer( delay(duration) - val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch + if (hotWalletFeatureToggles.isHotWalletEnabled) { + val userWallets = userWalletsListRepository.userWalletsSync() + if (userWallets.isNotEmpty()) { + userWalletsListRepository.lockAllWallets() + .onLeft { + start() + } + .onRight { + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } + } + } else { + val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch - if (userWalletsListManager.hasUserWallets) { - val currentTime = System.currentTimeMillis() + if (userWalletsListManager.hasUserWallets) { + val currentTime = System.currentTimeMillis() - Timber.i( - """ + Timber.i( + """ Finished |- Millis passed: ${currentTime - startTime} - """.trimIndent(), - ) + """.trimIndent(), + ) - userWalletsListManager.lock() - store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + userWalletsListManager.lock() + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } } } } \ 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 fa9d503de0..2f037143f3 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -40,6 +40,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.settings.SetGooglePayAvailabilityUseCase import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase @@ -48,7 +49,9 @@ import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper @@ -188,6 +191,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler + @Inject + internal lateinit var userWalletsListRepository: UserWalletsListRepository + + @Inject + internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -271,6 +280,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { settingsRepository = settingsRepository, userWalletsListManager = userWalletsListManager, coroutineScope = mainScope, + userWalletsListRepository = userWalletsListRepository, + hotWalletFeatureToggles = hotWalletFeatureToggles, ) initIntentHandlers() @@ -429,6 +440,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { + // TODO refactor this method to return a route instead of navigating directly + if (hotWalletFeatureToggles.isHotWalletEnabled) { + navigateToInitialScreenIfNeededNew(intentWhichStartedActivity) + return + } + val backStack = appRouterConfig.stack ?: emptyList() // TODO move inital navigation to navigation component ([REDACTED_JIRA]) val isOnlyInitialRoute = backStack.all { it is AppRoute.Initial } @@ -448,6 +465,57 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } + @Deprecated("Refactor this method to return a route instead of navigating directly") + private fun navigateToInitialScreenIfNeededNew(intentWhichStartedActivity: Intent?) { + lifecycleScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) + if (userWallets.isEmpty()) { + val shouldShowTos = !cardRepository.isTangemTOSAccepted() + + val route = if (shouldShowTos) { + AppRoute.Disclaimer(isTosAccepted = false) + } else { + AppRoute.Home(launchMode = launchMode) + } + + store.dispatchNavigationAction { replaceAll(route) } + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = false, + ) + } else { + if (userWallets.any { it.isLocked }) { + store.dispatchNavigationAction { + replaceAll( + AppRoute.Welcome( + launchMode = launchMode, + intent = intentWhichStartedActivity?.let(::SerializableIntent), + ), + ) + } + } else { + store.dispatchNavigationAction { + replaceAll(AppRoute.Wallet) + } + } + + intentProcessor.handleIntent( + intent = intentWhichStartedActivity, + isFromForeground = false, + skipNavigationHandlers = true, + ) + } + + if (intent != null) { + handleDeepLink(intent = intent, isFromOnNewIntent = false) + } + + viewModel.checkForUnfinishedBackup() + } + } + private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index dea8dc75a4..d780f3d15a 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -6,7 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented // [REDACTED_JIRA] @@ -28,10 +27,6 @@ internal class RuntimeUserWalletsStore( return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } } - override suspend fun getAllSyncOrNull(): List? { - return userWalletsListManager.userWallets.firstOrNull() - } - override suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt new file mode 100644 index 0000000000..c5275bf034 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.data + +import com.tangem.common.CompletionResult +import com.tangem.common.catching +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class UserWalletsStoreRepositoryProxy( + private val userWalletsListRepository: UserWalletsListRepository, +) : UserWalletsStore { + + override val selectedUserWalletOrNull: UserWallet? + get() = userWalletsListRepository.selectedUserWallet.value + + override val userWallets: Flow> + get() = flow { + userWalletsListRepository.load() + userWalletsListRepository.userWallets.collect { + emit(requireNotNull(it)) + } + } + + override fun getSyncOrNull(key: UserWalletId): UserWallet? { + return userWalletsListRepository.userWallets.value?.find { it.walletId == key } + } + + override fun getSyncStrict(key: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } + } + + override suspend fun update( + userWalletId: UserWalletId, + update: suspend (UserWallet) -> UserWallet, + ): CompletionResult { + return catching { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } + val updatedUserWallet = update(userWallet) + userWalletsListRepository.saveWithoutLock( + userWallet = updatedUserWallet, + canOverride = true, + ) + updatedUserWallet + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt index b2a04f9d52..2fe50e3705 100644 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -2,7 +2,10 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.data.RuntimeUserWalletsStore +import com.tangem.tap.data.UserWalletsStoreRepositoryProxy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,7 +18,15 @@ internal object UserWalletsStoreModule { @Provides @Singleton - fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore { - return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) + fun provideUserWalletsStore( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): UserWalletsStore { + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + UserWalletsStoreRepositoryProxy(userWalletsListRepository) + } else { + RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 0aa400353e..a674e4ac97 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -7,11 +7,13 @@ import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import com.tangem.tap.domain.card.DefaultResetCardUseCase @@ -42,9 +44,16 @@ internal object CardDomainModule { } @Provides - @Singleton - fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager) + fun provideIsNeedToBackupUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 7be9d95b9a..5f8a03b7d2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -3,7 +3,9 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import com.tangem.tap.domain.scanCard.LegacyScanProcessor @@ -31,7 +33,15 @@ internal object CardLegacyDomainModule { @Provides @Singleton - fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase { - return GenerateWalletNameUseCase(userWalletsListManager) + fun providesWalletNameGenerateUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GenerateWalletNameUseCase { + return GenerateWalletNameUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 1bb8a1ec1c..1989f4868e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -13,6 +13,8 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -81,10 +83,14 @@ object MarketsDomainModule { @Singleton fun provideFilterNetworksUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, excludedBlockchains: ExcludedBlockchains, ): FilterAvailableNetworksForWalletUseCase { return FilterAvailableNetworksForWalletUseCase( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, excludedBlockchains = excludedBlockchains, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 87b107936a..eff26615ae 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -10,11 +10,13 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -31,18 +33,30 @@ internal object WalletsDomainModule { @Provides fun providesUserWalletsSyncDelegate( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): UserWalletsSyncDelegate { return DefaultUserWalletsSyncDelegate( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, dispatchers = dispatchers, ) } @Provides @Singleton - fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { - return GetWalletsUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetWalletsUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetWalletsUseCase { + return GetWalletsUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -50,37 +64,71 @@ internal object WalletsDomainModule { fun providesWalletNameMigrationUseCase( userWalletsListManager: UserWalletsListManager, walletNamesMigrationRepository: WalletNamesMigrationRepository, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): WalletNameMigrationUseCase { return WalletNameMigrationUseCase( userWalletsListManager = userWalletsListManager, walletNamesMigrationRepository = walletNamesMigrationRepository, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } @Provides @Singleton - fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase { - return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetUserWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetUserWalletUseCase { + return GetUserWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton fun providesGetSelectedWalletSyncUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager) + return GetSelectedWalletSyncUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetSelectedWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetSelectedWalletUseCase { + return GetSelectedWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase { - return SaveWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesSaveWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): SaveWalletUseCase { + return SaveWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -99,15 +147,30 @@ internal object WalletsDomainModule { @Singleton fun providesSelectWalletUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, reduxStateHolder: ReduxStateHolder, ): SelectWalletUseCase { - return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder) + return SelectWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + reduxStateHolder = reduxStateHolder, + ) } @Provides @Singleton - fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase { - return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesUpdateWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): UpdateWalletUseCase { + return UpdateWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -124,14 +187,30 @@ internal object WalletsDomainModule { @Provides @Singleton - fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase { - return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager) + fun providesGetWalletsSyncUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): GetWalletNamesUseCase { + return GetWalletNamesUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @Singleton - fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase { - return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager) + fun providesDeleteWalletUseCase( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): DeleteWalletUseCase { + return DeleteWalletUseCase( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides @@ -214,9 +293,13 @@ internal object WalletsDomainModule { @Singleton fun providesGetSavedWalletChangesIdUseCase( userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSavedWalletsCountUseCase { return GetSavedWalletsCountUseCase( userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index d60a2a0f74..c1cb92a998 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -30,6 +30,7 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets import com.tangem.tap.domain.userWalletList.utils.updateWith import com.tangem.utils.Provider import com.tangem.utils.ProviderSuspend +import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -173,6 +174,8 @@ internal class DefaultUserWalletsListRepository( userWalletEncryptionKeysRepository.delete(userWalletIds) + val userWalletsBeforeDelete = userWallets.value ?: return@either + userWallets.update { currentWallets -> currentWallets?.filterNot { it.walletId in userWalletIds } } @@ -181,7 +184,7 @@ internal class DefaultUserWalletsListRepository( if (currentSelected == null) return@update null userWallets.value?.findAvailableUserWallet( - userWallets.value?.indexOfFirst { it.walletId == currentSelected.walletId } ?: 0, + userWalletsBeforeDelete.indexOfFirstOrNull { it.walletId == currentSelected.walletId } ?: 0, ) } } @@ -199,7 +202,7 @@ internal class DefaultUserWalletsListRepository( when (unlockMethod) { UserWalletsListRepository.UnlockMethod.Biometric -> { - unlockAllWallets() + unlockAllWallets().bind() select(userWalletId) } UserWalletsListRepository.UnlockMethod.AccessCode -> { @@ -225,7 +228,7 @@ internal class DefaultUserWalletsListRepository( } sensitiveInformationRepository.getAll(listOf(encryptionKey)) - .doOnSuccess { userWallets.value?.updateWith(it) } + .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock) } @@ -255,6 +258,7 @@ internal class DefaultUserWalletsListRepository( } override suspend fun unlockAllWallets(): Either = either { + val userWalletIds = userWalletsSync().map { it.walletId }.toSet() val biometricKeys = runCatching { userWalletEncryptionKeysRepository.getAllBiometric() }.getOrElse { @@ -264,8 +268,14 @@ internal class DefaultUserWalletsListRepository( val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() val allKeys = biometricKeys + unsecuredKeys + + if (allKeys.all { it.walletId in userWalletIds }.not()) { + raise(UnlockWalletError.UnableToUnlock) + } + sensitiveInformationRepository.getAll(allKeys) - .doOnSuccess { userWallets.value?.updateWith(it) } + .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } + .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } } override suspend fun lockAllWallets(): Either = either { @@ -297,7 +307,7 @@ internal class DefaultUserWalletsListRepository( biometryFallback: suspend () -> Either, ): Either { val result = passwordRequester.requestPassword( - hasBiometry = tangemSdkManagerProvider.invoke().needEnrollBiometrics, + hasBiometry = tangemSdkManagerProvider.invoke().canUseBiometry, ) return when (result) { @@ -312,6 +322,7 @@ internal class DefaultUserWalletsListRepository( requestPasswordRecursive(block, biometryFallback) } else { passwordRequester.successfulAuthentication() + passwordRequester.dismiss() decrypted.right() } } @@ -319,9 +330,9 @@ internal class DefaultUserWalletsListRepository( biometryFallback() .onRight { passwordRequester.successfulAuthentication() + passwordRequester.dismiss() } - passwordRequester.dismiss() - null.right() + .map { null } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index fb42970c29..43a5b4916f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -62,15 +62,16 @@ internal class UserWalletEncryptionKeysRepository( } } - suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? { - val encrypted = secureStorage.get( - account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name, - ) ?: return null + suspend fun getEncryptedWithPassword(userWalletId: UserWalletId, password: CharArray): UserWalletEncryptionKey? = + withContext(dispatchers.io) { + val encrypted = secureStorage.get( + account = StorageKey.UserWalletEncryptionKeyEncrypted(userWalletId).name, + ) ?: return@withContext null - val decrypted = AESEncryptionProtocol.decryptWithPassword(password, encrypted) - - return decrypted.decodeToKey() - } + withContext(dispatchers.default) { + AESEncryptionProtocol.decryptWithPassword(password, encrypted).decodeToKey() + } + } suspend fun getAllBiometric(): List = withContext(dispatchers.io) { val keys = getUserWalletsIds().map { userWalletId -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 6992564847..6c9e9f189d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -11,7 +11,7 @@ import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -42,9 +42,9 @@ internal object WalletConnectInteractorModule { wcSessionsRepository: WalletConnectSessionsRepository, currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, walletConnectFeatureToggles: WalletConnectFeatureToggles, coroutineDispatcherProvider: CoroutineDispatcherProvider, + getSelectedWalletUseCase: GetSelectedWalletUseCase, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -54,7 +54,7 @@ internal object WalletConnectInteractorModule { blockchainHelper = TangemWcBlockchainHelper(), currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, - userWalletsListManager = userWalletsListManager, + getSelectedWalletUseCase = getSelectedWalletUseCase, dispatchers = coroutineDispatcherProvider, walletConnectFeatureToggles = walletConnectFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index f602ed8fc4..395ad23885 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -13,7 +13,6 @@ import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.model.legacy.Session import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.extensions.dispatchOnMain @@ -38,18 +37,14 @@ class WalletConnectInteractor( private val dispatchers: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, - private val userWalletsListManager: UserWalletsListManager, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, val blockchainHelper: WcBlockchainHelper, ) { private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled } private var isWalletConnectReadyForDeepLinks = false - private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { - GetSelectedWalletUseCase(userWalletsListManager) - } - private val wcScope = CoroutineScope( SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 5ce4ad3c20..99425e0191 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -4,11 +4,16 @@ import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository -internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider { +internal class DefaultAuthProvider( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean = false, +) : AuthProvider { - override fun getCardPublicKey(): String { - val userWallet = userWalletsListManager.selectedUserWalletSync + override suspend fun getCardPublicKey(): String { + val userWallet = getSelectedWallet() if (userWallet !is UserWallet.Cold) { return "" @@ -17,8 +22,8 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle return userWallet.scanResponse.card.cardPublicKey.toHexString() } - override fun getCardId(): String { - val userWallet = userWalletsListManager.selectedUserWalletSync + override suspend fun getCardId(): String { + val userWallet = getSelectedWallet() if (userWallet !is UserWallet.Cold) { return "" @@ -27,9 +32,25 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle return userWallet.scanResponse.card.cardId } - override fun getCardsPublicKeys(): Map { - return userWalletsListManager.userWalletsSync.filterIsInstance().associate { + override suspend fun getCardsPublicKeys(): Map { + return getWallets().filterIsInstance().associate { it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() } } + + private suspend fun getWallets(): List { + return if (useNewListRepository) { + userWalletsListRepository.userWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } + } + + private suspend fun getSelectedWallet(): UserWallet? { + return if (useNewListRepository) { + userWalletsListRepository.selectedUserWalletSync() + } else { + userWalletsListManager.selectedUserWalletSync + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index 5f724624af..95004c11d8 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -3,6 +3,8 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.tap.network.auth.DefaultAppVersionProvider @@ -22,8 +24,16 @@ internal class AuthModule { @Provides @Singleton - fun provideAuthProvider(userWalletsListManager: UserWalletsListManager): AuthProvider { - return DefaultAuthProvider(userWalletsListManager) + fun provideAuthProvider( + userWalletsListManager: UserWalletsListManager, + userWalletsListRepository: UserWalletsListRepository, + hotWalletFeatureToggles: HotWalletFeatureToggles, + ): AuthProvider { + return DefaultAuthProvider( + userWalletsListManager = userWalletsListManager, + userWalletsListRepository = userWalletsListRepository, + useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, + ) } @Provides diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index 8346378b52..fa5c04f6a1 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -14,6 +14,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.runBlocking import javax.inject.Singleton @Module @@ -41,6 +42,12 @@ internal object FeatureTogglesManagerModule { localTogglesStorage = localTogglesStorage, versionProvider = versionProvider, ) + }.also { + // We need to initialize during the hilt graph creation + // in order to provide the feature toggles correctly to other dependencies. + runBlocking { + it.init() + } } } } \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 7aca4511ea..ce0d035439 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -9,7 +9,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import kotlin.properties.Delegates /** * Feature toggles manager implementation in DEV build @@ -24,10 +23,14 @@ internal class DevFeatureTogglesManager( private val versionProvider: VersionProvider, ) : MutableFeatureTogglesManager { - private var featureTogglesMap: MutableMap by Delegates.notNull() - private var localFeatureTogglesMap: Map by Delegates.notNull() + private var featureTogglesMap: MutableMap? = null + private var localFeatureTogglesMap: Map? = null override suspend fun init() { + if (featureTogglesMap != null && localFeatureTogglesMap != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( @@ -46,21 +49,21 @@ internal class DevFeatureTogglesManager( .toMutableMap() } - override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap - override fun getFeatureToggles(): Map = featureTogglesMap + override fun getFeatureToggles(): Map = featureTogglesMap!! override suspend fun changeToggle(name: String, isEnabled: Boolean) { - featureTogglesMap[name] ?: return - featureTogglesMap[name] = isEnabled - appPreferencesStore.storeFeatureToggles(value = featureTogglesMap) + featureTogglesMap!![name] ?: return + featureTogglesMap!![name] = isEnabled + appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!) } override suspend fun recoverLocalConfig() { - featureTogglesMap = localFeatureTogglesMap.toMutableMap() - appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap) + featureTogglesMap = localFeatureTogglesMap!!.toMutableMap() + appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index d723297a45..54fe7a011d 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -5,7 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.configtoggle.storage.TogglesStorage import com.tangem.core.configtoggle.utils.associateToggles import com.tangem.core.configtoggle.version.VersionProvider -import kotlin.properties.Delegates /** * Feature toggles manager implementation in PROD build @@ -18,18 +17,22 @@ internal class ProdFeatureTogglesManager( private val versionProvider: VersionProvider, ) : FeatureTogglesManager { - private var featureToggles: Map by Delegates.notNull() + private var featureToggles: Map? = null override suspend fun init() { + if (featureToggles != null) { + return // Already initialized + } + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) featureToggles = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get() ?: "") } - override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false + override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false @VisibleForTesting(otherwise = VisibleForTesting.NONE) - fun getProdFeatureToggles() = featureToggles + fun getProdFeatureToggles() = featureToggles!! @VisibleForTesting(otherwise = VisibleForTesting.NONE) fun setProdFeatureToggles(map: Map) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt index 7945fc59eb..da1b1aca5f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/AuthProvider.kt @@ -8,12 +8,12 @@ interface AuthProvider { /** * Returns authToken for tangem tech api */ - fun getCardPublicKey(): String + suspend fun getCardPublicKey(): String - fun getCardId(): String + suspend fun getCardId(): String /** * Returns map where keys(cardId) associated with cardPublicKey */ - fun getCardsPublicKeys(): Map + suspend fun getCardsPublicKeys(): Map } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 4f1d6b5cba..b4d50f01de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -5,6 +5,10 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow +@Deprecated( + message = "Use UserWalletsListRepository instead", + replaceWith = ReplaceWith("UserWalletsListRepository"), +) interface UserWalletsStore { val selectedUserWalletOrNull: UserWallet? @@ -15,8 +19,6 @@ interface UserWalletsStore { fun getSyncStrict(key: UserWalletId): UserWallet - suspend fun getAllSyncOrNull(): List? - suspend fun update( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index defd5db207..61d25e29b1 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -18,6 +18,7 @@ import com.tangem.utils.ProviderSuspend import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.version.AppVersionProvider import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -55,8 +56,8 @@ internal class ProdApiConfigsManagerTest { every { appVersionProvider.versionName } returns VERSION_NAME every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY - every { appAuthProvider.getCardId() } returns APP_CARD_ID - every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY + coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID + coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY every { appInfoProvider.osVersion } returns "Android 16" } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 2911dd5968..447c2effdf 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -195,7 +195,7 @@ class DefaultWalletsRepositoryTest { ) val authProvider = mockk { - every { getCardsPublicKeys() } returns publicKeys + coEvery { getCardsPublicKeys() } returns publicKeys } repository = DefaultWalletsRepository( diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt index f1c80f3ba0..044bc6a726 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -6,9 +6,13 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync class FilterAvailableNetworksForWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val excludedBlockchains: ExcludedBlockchains, ) { @@ -20,7 +24,7 @@ class FilterAvailableNetworksForWalletUseCase( userWalletId: UserWalletId, networks: Set, ): Set { - val userWallet = userWalletsListManager.userWalletsSync.firstOrNull { + val userWallet = getWallets().firstOrNull { it.walletId == userWalletId } ?: return networks.toSet() @@ -33,4 +37,10 @@ class FilterAvailableNetworksForWalletUseCase( supportedBlockchains.contains(blockchain) }.toSet() } + + private fun getWallets() = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index 16b0be7fa0..403ac7a76b 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -10,11 +10,14 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class DefaultUserWalletsSyncDelegate( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val dispatchers: CoroutineDispatcherProvider, ) : UserWalletsSyncDelegate { @@ -28,10 +31,43 @@ class DefaultUserWalletsSyncDelegate( } } - // TODO remove dispatchers whnen UserWalletsListManager will be main safe private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, + ): Either = if (useNewRepository) { + renameUserWalletInNewRepository(userWalletId, name) + } else { + renameUserWalletInLegacyRepository(userWalletId, name) + } + + private suspend fun renameUserWalletInNewRepository( + userWalletId: UserWalletId, + name: String, + ): Either = either { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.find { it.walletId == userWalletId } + ?: raise(UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found"))) + + ensure(userWallets.none { it.name == name && it.walletId != userWalletId }) { + UpdateWalletError.NameAlreadyExists + } + + ensure(name != userWallet.name) { + UpdateWalletError.NameAlreadyExists + } + + val updatedWallet = userWallet.copy(name = name) + + userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .map { updatedWallet } + .mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("")) } + .bind() + } + + // TODO remove dispatchers whnen UserWalletsListManager will be main safe + private suspend fun renameUserWalletInLegacyRepository( + userWalletId: UserWalletId, + name: String, ): Either = withContext(dispatchers.io) { either { val existingNames = userWalletsListManager.userWalletsSync diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt deleted file mode 100644 index e2aeab608f..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SelectWalletError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed interface SelectWalletError { - - object UnableToSelectUserWallet : SelectWalletError -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index c876b7d526..c3c49c4f7e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.common.doOnFailure import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.error.DeleteWalletError import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for deleting user wallet @@ -14,7 +15,11 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class DeleteWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { /** * Deletes user wallet with provided ID. @@ -24,6 +29,12 @@ class DeleteWalletUseCase(private val userWalletsListManager: UserWalletsListMan * @return [Either] with [DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. * */ suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map { + userWalletsListRepository.selectedUserWallet.value != null + } + } + return either { userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) .doOnFailure { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt index e779085950..fdc88856e7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -2,12 +2,16 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.scan.ProductType import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for user wallet name generation */ class GenerateWalletNameUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(productType: ProductType, isBackupNotAllowed: Boolean, isStartToCoin: Boolean): String { @@ -17,16 +21,24 @@ class GenerateWalletNameUseCase( isStartToCoin = isStartToCoin, ) - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } fun invokeForHot(): String { val defaultName = "Wallet" - val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + val existingNames = getNamesSet() return suggestedWalletName(defaultName, existingNames) } + private fun getNamesSet(): Set { + return if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet() + } else { + userWalletsListManager.userWalletsSync.map { it.name }.toSet() + } + } + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { val startIndex = 2 if (!existingNames.contains(defaultName)) { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt index 6afc79d552..32233cdbf9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt @@ -4,13 +4,20 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.* class GetSavedWalletsCountUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, ) { operator fun invoke(): Flow> { + if (useNewRepository) { + return userWalletsListRepository.userWallets.map { requireNotNull(it) } + } + return userWalletsListManager.savedWalletsCount .filter { count -> if (count == 0) return@filter true diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index 00451b45de..5b681fb679 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.ensureNotNull import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for getting selected wallet. @@ -15,10 +16,20 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class GetSelectedWalletSyncUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetSelectedWalletSyncUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either { + if (useNewRepository) { + return either { + userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound) + } + } + return either { ensureNotNull( value = userWalletsListManager.selectedUserWalletSync, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index d479a9d59b..57a01d23fb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -5,7 +5,9 @@ import arrow.core.raise.either import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull /** * Use case for getting flow of selected wallet. @@ -14,12 +16,32 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetSelectedWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +@Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") +class GetSelectedWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean = false, +) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either> { return either { - userWalletsListManager.selectedUserWallet + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.filterNotNull() + } else { + userWalletsListManager.selectedUserWallet + } + } + } + + @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") + fun sync(): Either { + return either { + if (useNewRepository) { + userWalletsListRepository.selectedUserWallet.value + } else { + userWalletsListManager.selectedUserWalletSync + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index b1a548af9a..6f4e13f0f0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -10,13 +10,24 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest -class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetUserWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWallets = userWalletsListManager.userWalletsSync + val userWallets = if (useNewListRepository) { + userWalletsListRepository.requireUserWalletsSync() + } else { + userWalletsListManager.userWalletsSync + } ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) @@ -25,7 +36,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa @OptIn(ExperimentalCoroutinesApi::class) fun invokeFlow(userWalletId: UserWalletId): EitherFlow { - return userWalletsListManager.userWallets.transformLatest { userWallets -> + val flow = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } + + return flow.transformLatest { userWallets -> userWallets.firstOrNull { it.walletId == userWalletId } ?.let { emit(it.right()) } ?: emit(GetUserWalletError.UserWalletNotFound.left()) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt index 0108e03b67..377bf1b152 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -1,13 +1,23 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.requireUserWalletsSync /** * Use case for getting list of user wallets names. * * @property userWalletsListManager user wallets list manager */ -class GetWalletNamesUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletNamesUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { - operator fun invoke(): List = userWalletsListManager.userWalletsSync.map { it.name } + operator fun invoke(): List = if (useNewRepository) { + userWalletsListRepository.requireUserWalletsSync().map { it.name } + } else { + userWalletsListManager.userWalletsSync.map { it.name } + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 7e6a0b6510..6635d63099 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -1,8 +1,10 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Use case for getting list of user wallets @@ -11,11 +13,23 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -class GetWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { +class GetWalletsUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, +) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = userWalletsListManager.userWallets + operator fun invoke(): Flow> = if (useNewListRepository) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } @Throws(IllegalArgumentException::class) - fun invokeSync(): List = userWalletsListManager.userWalletsSync + fun invokeSync(): List = if (useNewListRepository) { + userWalletsListRepository.userWallets.value!! + } else { + userWalletsListManager.userWalletsSync + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt index 29240ff71b..05d47b70d0 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,12 +13,22 @@ import kotlinx.coroutines.flow.map * * @property userWalletsListManager user wallets list manager */ -class IsNeedToBackupUseCase(private val userWalletsListManager: UserWalletsListManager) { +class IsNeedToBackupUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { operator fun invoke(id: UserWalletId): Flow { - return userWalletsListManager.userWallets + val userWalletsFlow = if (useNewRepository) { + userWalletsListRepository.userWallets + } else { + userWalletsListManager.userWallets + } + + return userWalletsFlow .map { wallets -> - val wallet = wallets.firstOrNull { it.walletId == id } + val wallet = wallets?.firstOrNull { it.walletId == id } if (wallet == null) { false } else { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 9ff8c5bb81..b34885eb98 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -10,6 +10,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for saving user wallet @@ -18,22 +19,51 @@ import com.tangem.domain.models.wallet.UserWallet * [REDACTED_AUTHOR] */ -class SaveWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class SaveWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { - return either { - userWalletsListManager.save(userWallet, canOverride) - .doOnSuccess { return Unit.right() } - .doOnFailure { - return when (it) { - is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( - it.messageResId, - ) - else -> SaveWalletError.DataError(it.messageResId) - }.left() - } + return if (useNewRepository) { + either { + val newUserWallet = + userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() - return Unit.right() + if (newUserWallet) { + when (userWallet) { + is UserWallet.Cold -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } + is UserWallet.Hot -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.NoLock, + ) + } + }.mapLeft { SaveWalletError.DataError(null) }.bind() + } + } + } else { + either { + userWalletsListManager.save(userWallet, canOverride) + .doOnSuccess { return Unit.right() } + .doOnFailure { + return when (it) { + is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( + it.messageResId, + ) + else -> SaveWalletError.DataError(it.messageResId) + }.left() + } + + return Unit.right() + } } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 3ff5b201d7..e0654f947c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -6,9 +6,10 @@ import arrow.core.right import com.tangem.common.CompletionResult import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.SelectWalletError +import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for selecting wallet @@ -20,10 +21,19 @@ import com.tangem.domain.models.wallet.UserWalletId */ class SelectWalletUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { + if (useNewRepository) { + return userWalletsListRepository.select(userWalletId).map { + reduxStateHolder.onUserWalletSelected(it) + it + } + } + return either { return when (val result = userWalletsListManager.select(userWalletId)) { is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index b6d0accf29..96c2f19f7a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -7,6 +7,9 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.wallets.models.UpdateWalletError.* +import com.tangem.domain.core.wallets.UserWalletsListRepository /** * Use case for updating user wallet @@ -15,15 +18,38 @@ import com.tangem.domain.models.wallet.UserWalletId * [REDACTED_AUTHOR] */ -class UpdateWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { +class UpdateWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewRepository: Boolean, +) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, - ): Either = either { - when (val result = userWalletsListManager.update(userWalletId, update)) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data + ): Either { + if (useNewRepository) { + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + ?: return Either.Left( + UpdateWalletError.DataError(IllegalStateException("User wallet with id $userWalletId not found")), + ) + val updatedWallet = update(userWallet) + return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .mapLeft { + when (it) { + is SaveWalletError.DataError -> DataError( + IllegalStateException("Failed to update wallet: ${it.messageId}"), + ) + is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists + } + } + } + + return either { + when (val result = userWalletsListManager.update(userWalletId, update)) { + is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) + is CompletionResult.Success -> result.data + } } } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt index b71be78c5e..4df283dd5e 100644 --- a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt @@ -22,7 +22,11 @@ class GetSavedWalletsCountUseCaseTest { @Before fun setup() { userWalletsListManager = mockk() - useCase = GetSavedWalletsCountUseCase(userWalletsListManager) + useCase = GetSavedWalletsCountUseCase( + userWalletsListManager, + userWalletsListRepository = mockk(), + useNewRepository = false, + ) mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") } diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 933716b04b..0713f99e5b 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -16,8 +16,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM import com.tangem.sdk.api.TangemSdkManager @@ -40,7 +40,7 @@ internal class AskBiometryModel @Inject constructor( private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, private val settingsRepository: SettingsRepository, private val tangemSdkManager: TangemSdkManager, - private val userWalletsListManager: UserWalletsListManager, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val walletsRepository: WalletsRepository, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsManager: SettingsManager, @@ -87,7 +87,7 @@ internal class AskBiometryModel @Inject constructor( * because it will be automatically saved on UserWalletsListManager switch */ - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync ?: run { + val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run { Timber.e("Unable to save user wallet") uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index 23b2dbb674..0c7b8d6290 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.requireColdWallet import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent @@ -51,6 +52,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, private val walletsRepository: WalletsRepository, @@ -231,7 +233,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( OnboardingMultiWalletComponent.Mode.Onboarding, OnboardingMultiWalletComponent.Mode.ContinueFinalize, -> { - userWalletsListManager.save( + saveWalletUseCase( userWallet = userWalletCreated.copy( scanResponse = scanResponse.updateScanResponseAfterBackup(), ), @@ -247,13 +249,11 @@ internal class MultiWalletFinalizeModel @Inject constructor( } ?: userWalletCreated - userWalletsListManager.update( - userWalletId = userWallet.walletId, - update = { wallet -> - wallet.requireColdWallet().copy( - scanResponse = scanResponse.updateScanResponseAfterBackup(), - ) - }, + saveWalletUseCase( + userWallet = userWallet.requireColdWallet().copy( + scanResponse = scanResponse.updateScanResponseAfterBackup(), + ), + canOverride = true, ) userWallet diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 2e3a94788a..db44e189c2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -43,7 +43,8 @@ import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.impl.R @@ -73,7 +74,8 @@ internal class OnboardingTwinModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, private val tangemSdkManager: TangemSdkManager, @@ -199,9 +201,9 @@ internal class OnboardingTwinModel @Inject constructor( // remove wallet only after first step of retwin if (params.mode == Mode.RecreateWallet) { - userWalletsListManager.delete( - listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), - ) + UserWalletIdBuilder.scanResponse(params.scanResponse).build()?.let { + deleteWalletUseCase(it) + } } analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) @@ -329,7 +331,14 @@ internal class OnboardingTwinModel @Inject constructor( return@coroutineScope } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@coroutineScope + } cardRepository.finishCardActivation(params.scanResponse.card.cardId) @@ -456,7 +465,15 @@ internal class OnboardingTwinModel @Inject constructor( return@launch } - userWalletsListManager.save(userWallet, canOverride = true) + saveWalletUseCase( + userWallet = userWallet, + canOverride = true, + ).onLeft { + Timber.e("Unable to save user wallet: $it") + setLoading(false) + return@launch + } + params.modelCallbacks.onDone() } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 102098867b..e517a628cb 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -20,8 +20,8 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent @@ -46,7 +46,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, + private val saveWalletUseCase: SaveWalletUseCase, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -173,7 +173,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } val userWallet = createUserWallet(params.scanResponse, newTokens) - userWalletsListManager.save(userWallet) + saveWalletUseCase(userWallet) visaAuthTokenStorage.remove(params.scanResponse.card.cardId) otpStorage.removeOTP(params.scanResponse.card.cardId) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index a23bc00f4b..743c26e5a2 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -25,12 +25,12 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError @@ -51,7 +51,7 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListManager: UserWalletsListManager, + private val userWalletsStore: UserWalletsStore, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, @@ -409,7 +409,7 @@ internal class DefaultSwapRepository( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - userWallet = requireNotNull(userWalletsListManager.selectedUserWalletSync), + userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull), ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 679015d755..118602742e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,8 +8,8 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.DefaultSwapRepository import com.tangem.feature.swap.DefaultSwapTransactionRepository import com.tangem.feature.swap.converters.ErrorsDataConverter @@ -33,7 +33,7 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, + userWalletsStore: UserWalletsStore, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, @@ -43,7 +43,7 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - userWalletsListManager = userWalletsListManager, + userWalletsStore = userWalletsStore, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt index 1657e84545..3f365ebc8a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -2,29 +2,44 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.models.wallet.copy +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import timber.log.Timber class WalletNameMigrationUseCase( private val userWalletsListManager: UserWalletsListManager, + private val userWalletsListRepository: UserWalletsListRepository, + private val useNewListRepository: Boolean, private val walletNamesMigrationRepository: WalletNamesMigrationRepository, ) { suspend operator fun invoke() { - val wallets = userWalletsListManager.userWalletsSync - if (walletNamesMigrationRepository.isMigrationDone()) { return } - val existingNames: MutableSet = mutableSetOf() - wallets.indices.forEach { i -> - val defaultName = wallets[i].name - val suggestedWalletName = suggestedWalletName(defaultName, existingNames) - if (defaultName != suggestedWalletName) { - userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + if (useNewListRepository) { + val wallets = userWalletsListRepository.userWalletsSync() + val existingNames: MutableSet = mutableSetOf() + wallets.forEach { + val defaultName = it.name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) + } + Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) + } + } else { + val wallets = userWalletsListManager.userWalletsSync + val existingNames: MutableSet = mutableSetOf() + wallets.indices.forEach { i -> + val defaultName = wallets[i].name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } + } + Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } - Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() From e3a7ba0a777ba35f00f444e231cc412d8e80769e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:31:49 +0500 Subject: [PATCH 25/87] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 10 +++ .../DefaultTransactionRepository.kt | 20 ++++++ .../ethereum/WcEthSignTransactionUseCase.kt | 2 +- .../WcSolanaSignAllTransactionUseCase.kt | 6 +- .../solana/WcSolanaSignTransactionUseCase.kt | 6 +- .../transaction/TransactionRepository.kt | 14 ++++ .../usecase/PrepareAndSignUseCase.kt | 70 +++++++++++++++++++ gradle/tangem_dependencies.toml | 2 +- 8 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index eb56a8d16c..d9a7d2b4ad 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 @@ -18,6 +18,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object TransactionDomainModule { @@ -184,6 +185,15 @@ internal object TransactionDomainModule { return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository) } + @Provides + @Singleton + fun providePrepareAndSignUseCase( + transactionRepository: TransactionRepository, + cardSdkConfigRepository: CardSdkConfigRepository, + ): PrepareAndSignUseCase { + return PrepareAndSignUseCase(transactionRepository, cardSdkConfigRepository) + } + @Provides @Singleton fun provideSignUseCase( diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index a9d3b05d9b..7179221efb 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -381,6 +381,26 @@ internal class DefaultTransactionRepository( preparer.prepareForSendMultiple(transactionData, signer) } + override suspend fun prepareAndSign( + transactionData: TransactionData, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(dispatchers.io) { + val preparer = getPreparer(network, userWalletId) + preparer.prepareAndSign(transactionData, signer) + } + + override suspend fun prepareAndSignMultiple( + transactionData: List, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ) = withContext(dispatchers.io) { + val preparer = getPreparer(network, userWalletId) + preparer.prepareAndSignMultiple(transactionData, signer) + } + private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index b6795bf29b..8cfe678df0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -30,7 +30,7 @@ import com.tangem.blockchain.common.Amount as BlockchainAmount internal class WcEthSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, - private val prepareForSend: PrepareForSendUseCase, + private val prepareForSend: PrepareForSendUseCase, // TODO: TODO("[REDACTED_JIRA]") private val ethTxHelper: WcEthTxHelper, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTransaction, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt index 8e91c2c8dd..d8b9e39468 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate -import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck @@ -29,7 +29,7 @@ import org.json.JSONObject internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, - private val prepareForSend: PrepareForSendUseCase, + private val prepareAndSign: PrepareAndSignUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcSolanaMethod.SignAllTransaction, blockAidDelegate: BlockAidVerificationDelegate, @@ -46,7 +46,7 @@ internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor( ).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } } override suspend fun SignCollector>.onSign(state: WcSignState>) { - val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) + val hash = prepareAndSign.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> emit(state.toResult(parseSendError(error).left())) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt index 282125a721..c1d301135a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate -import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.transaction.usecase.PrepareAndSignUseCase import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck @@ -27,7 +27,7 @@ import okio.ByteString.Companion.decodeBase64 internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, - private val prepareForSend: PrepareForSendUseCase, + private val prepareAndSign: PrepareAndSignUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcSolanaMethod.SignTransaction, blockAidDelegate: BlockAidVerificationDelegate, @@ -44,7 +44,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( ).map { lce -> lce.map { result -> BlockAidTransactionCheck.Result.Plain(result) } } override suspend fun SignCollector.onSign(state: WcSignState) { - val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) + val hash = prepareAndSign.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> emit(state.toResult(parseSendError(error).left())) } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 62feb67e90..f6171137cd 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -109,4 +109,18 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): com.tangem.blockchain.extensions.Result> + + suspend fun prepareAndSign( + transactionData: TransactionData, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): com.tangem.blockchain.extensions.Result + + suspend fun prepareAndSignMultiple( + transactionData: List, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): com.tangem.blockchain.extensions.Result> } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt new file mode 100644 index 0000000000..4fecd3147f --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareAndSignUseCase.kt @@ -0,0 +1,70 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins +import com.tangem.domain.card.models.TwinKey +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.SendTransactionError + +class PrepareAndSignUseCase( + private val transactionRepository: TransactionRepository, + private val cardSdkConfigRepository: CardSdkConfigRepository, +) { + + suspend operator fun invoke( + transactionData: TransactionData, + userWallet: UserWallet, + network: Network, + ): Either { + val signer = createSigner(userWallet) + val result = transactionRepository.prepareAndSign( + transactionData = transactionData, + userWalletId = userWallet.walletId, + network = network, + signer = signer, + ) + return when (result) { + is Result.Failure -> SendTransactionUseCase.handleError(result).left() + is Result.Success -> result.data.right() + } + } + + suspend operator fun invoke( + transactionData: List, + userWallet: UserWallet, + network: Network, + ): Either> { + val signer = createSigner(userWallet) + val result = transactionRepository.prepareAndSignMultiple( + transactionData = transactionData, + userWalletId = userWallet.walletId, + network = network, + signer = signer, + ) + return when (result) { + is Result.Failure -> SendTransactionUseCase.handleError(result).left() + is Result.Success -> result.data.right() + } + } + + private fun createSigner(userWallet: UserWallet): TransactionSigner { + userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + + val signer = cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) + return signer + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1018ab6255..03c2b9d8b5 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1142" +tangemBlockchainSdk = "releases-5.27.0-1147" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.27.0-513" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From a1b5761bf3c841ad0419768b8b1ac11f53c37c94 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Aug 2025 12:42:13 +0700 Subject: [PATCH 26/87] Updated on 2026-08-14 --- .../walletconnect/network/ethereum/WcEthNetwork.kt | 12 ++++++++++-- .../domain/walletconnect/model/WcEthAddChain.kt | 5 +++++ .../domain/walletconnect/model/WcEthMethod.kt | 2 +- .../model/WcEthSignTypedDataParams.kt | 14 +++++++------- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 0f9e21543f..fe375b57d6 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -6,9 +6,11 @@ import arrow.core.left import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.extensions.hexToInt import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey +import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork.NamespaceConverter.Companion.ETH_NAMESPACE_KEY import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext @@ -113,8 +115,10 @@ internal class WcEthNetwork( .getOrElse { return it.left() } ?.firstOrNull() ?.let { + val caip2 = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${it.chainId.hexToInt()}") + ?: return null.right() val newNetwork = networksConverter - .mainOrAnyWalletNetworkForRequest(it.chainId, wallet) + .mainOrAnyWalletNetworkForRequest(caip2.raw, wallet) ?: return null.right() WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork).right() } @@ -147,13 +151,17 @@ internal class WcEthNetwork( override val excludedBlockchains: ExcludedBlockchains, ) : WcNamespaceConverter { - override val namespaceKey: NamespaceKey = NamespaceKey("eip155") + override val namespaceKey: NamespaceKey = NamespaceKey(ETH_NAMESPACE_KEY) override fun toBlockchain(chainId: CAIP2): Blockchain? { if (chainId.namespace != namespaceKey.key) return null val ethChainId = chainId.reference.toIntOrNull() ?: return null return Blockchain.fromChainId(ethChainId) } + + companion object { + const val ETH_NAMESPACE_KEY = "eip155" + } } internal class Factories @Inject constructor( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt index 9daf8154a1..33633558c4 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthAddChain.kt @@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WcEthAddChain( + /** + * chainId are identified by EIP-155 integers expressed in hexadecimal notation, + * with 0x prefix and no leading zeroes for the chainId value. + * For more information https://eips.ethereum.org/EIPS/eip-5792#atomicbatch-capability + */ @Json(name = "chainId") val chainId: String, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt index 24a4878ec4..afb348e77e 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -15,7 +15,7 @@ sealed interface WcEthMethod : WcMethod { val account: String, val dataForSign: String, ) : WcEthMethod { - val humanMsg: String = params.message.contents.orEmpty() + val humanMsg: String = params.message?.contents.orEmpty() } data class SendTransaction( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt index 0054a9c68c..b14e6af6df 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthSignTypedDataParams.kt @@ -6,24 +6,24 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WcEthSignTypedDataParams( @Json(name = "domain") - val domain: Domain, + val domain: Domain?, @Json(name = "message") - val message: Message, + val message: Message?, @Json(name = "primaryType") - val primaryType: String, + val primaryType: String?, @Json(name = "types") val types: Map>, ) { @JsonClass(generateAdapter = true) data class Domain( @Json(name = "chainId") - val chainId: Int, + val chainId: Int?, @Json(name = "name") - val name: String, + val name: String?, @Json(name = "verifyingContract") - val verifyingContract: String, + val verifyingContract: String?, @Json(name = "version") - val version: String, + val version: String?, ) @JsonClass(generateAdapter = true) From cf6b5dbabb26cdf91979ba10405b91f183180dde Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 19:05:20 +0400 Subject: [PATCH 27/87] Updated on 2026-08-14 --- .../common/SwitchEnvironmentInterceptor.kt | 13 +++++++++---- .../datasource/di/utils/RetrofitApiBuilder.kt | 19 ++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt index 1b2a53ec3c..ee59740bff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/SwitchEnvironmentInterceptor.kt @@ -15,12 +15,14 @@ import okio.IOException * Switch api environment [Interceptor] * * @property id api config id [ApiConfig.ID] + * @property baseUrls base urls for all api config environments * @property apiConfigsManager api configs manager * [REDACTED_AUTHOR] */ internal class SwitchEnvironmentInterceptor( private val id: ApiConfig.ID, + private val baseUrls: Set, private val apiConfigsManager: ApiConfigsManager, ) : Interceptor { @@ -39,10 +41,13 @@ internal class SwitchEnvironmentInterceptor( return chain.proceed(request) } - private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl { - return this.newBuilder() - .host(host = url.toHttpUrl().host) - .build() + private fun HttpUrl.adjustBaseUrl(newBaseUrl: String): HttpUrl { + val currentUrl = this.toString() + val currentBaseUrl = baseUrls.first { currentUrl.contains(it) } + + return currentUrl + .replace(oldValue = currentBaseUrl, newValue = newBaseUrl) + .toHttpUrl() } private fun Request.Builder.addHeaders(headers: Map>): Request.Builder { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt index 87de60fbd0..ee3a3cffd6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE +import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor @@ -42,6 +43,7 @@ import javax.inject.Singleton */ @Singleton internal class RetrofitApiBuilder @Inject constructor( + private val apiConfigs: ApiConfigs, private val apiConfigsManager: ApiConfigsManager, @NetworkMoshi private val moshi: Moshi, private val analyticsErrorHandler: AnalyticsErrorHandler, @@ -49,6 +51,8 @@ internal class RetrofitApiBuilder @Inject constructor( private val appLogsStore: AppLogsStore, ) { + private val configsBaseUrls: Map> = getConfigsBaseUrls() + /** * Builds a Retrofit API instance for the specified API configuration ID * @@ -95,13 +99,26 @@ internal class RetrofitApiBuilder @Inject constructor( val writeTimeoutSeconds: Long? = null, ) + private fun getConfigsBaseUrls(): Map> { + return apiConfigs.associate { config -> + val allBaseUrls = config.environmentConfigs.mapTo(hashSetOf(), ApiEnvironmentConfig::baseUrl) + + config.id to allBaseUrls + } + } + private fun OkHttpClient.Builder.applyApiConfig( apiConfigId: ApiConfig.ID, environmentConfig: ApiEnvironmentConfig, ): OkHttpClient.Builder { return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { addInterceptor( - interceptor = SwitchEnvironmentInterceptor(id = apiConfigId, apiConfigsManager = apiConfigsManager), + interceptor = SwitchEnvironmentInterceptor( + id = apiConfigId, + baseUrls = configsBaseUrls[apiConfigId] + ?: error("Base URLs for ApiConfig with id [$apiConfigId] not found"), + apiConfigsManager = apiConfigsManager, + ), ) } else { val headers = environmentConfig.headers From 853304539d7029383a5dc525169205018e08ee88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 16:23:18 +0400 Subject: [PATCH 28/87] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 13 ++-- core/res/src/main/res/values/strings.xml | 1 + .../DefaultAccountsCRUDRepository.kt | 6 ++ .../repository/AccountsCRUDRepository.kt | 9 ++- .../GetUnoccupiedAccountIndexUseCase.kt | 61 +++++++++++++++++ .../GetUnoccupiedAccountIndexUseCaseTest.kt | 59 +++++++++++++++++ features/account/impl/build.gradle.kts | 1 + .../createedit/AccountCreateEditModel.kt | 66 +++++++++++++++++-- .../createedit/entity/AccountCreateEditUM.kt | 14 +++- .../entity/AccountCreateEditUMBuilder.kt | 32 +++++++-- .../createedit/error/AccountFeatureError.kt | 30 +++++++++ .../createedit/ui/AccountCreateEditContent.kt | 21 +++--- 12 files changed, 285 insertions(+), 28 deletions(-) create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 17e09eb709..8cf8edf8d6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -1,10 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase -import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase -import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase -import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -44,4 +41,12 @@ internal object AccountDomainModule { ): RecoverCryptoPortfolioUseCase { return RecoverCryptoPortfolioUseCase(crudRepository = accountsCRUDRepository) } + + @Provides + @Singleton + fun provideGetUnoccupiedAccountIndexUseCase( + accountsCRUDRepository: AccountsCRUDRepository, + ): GetUnoccupiedAccountIndexUseCase { + return GetUnoccupiedAccountIndexUseCase(crudRepository = accountsCRUDRepository) + } } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4089de8ca8..446bdcb138 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Archive You are archiving this account, but you can always get it back. Account + Account #%s — used for address derivation. Add account Save Account name diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index 723da17196..a46b5d096a 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -53,6 +53,12 @@ internal class DefaultAccountsCRUDRepository( } } + override suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int { + val activeAccountsCount = runtimeStore.getSyncOrNull()?.size ?: 1 + + return activeAccountsCount + 1 + } + override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index a32796f0a1..a05f267633 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -43,15 +43,20 @@ interface AccountsCRUDRepository { * * @param accountList the list of accounts to be saved. */ - @Throws suspend fun saveAccounts(accountList: AccountList) + /** + * Retrieves the total count of accounts associated with a specific user wallet including archived accounts + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun getTotalAccountsCount(userWalletId: UserWalletId): Int + /** * Retrieves a user wallet by its unique identifier * * @param userWalletId the unique identifier of the user wallet * @return the [UserWallet] associated with the given identifier */ - @Throws fun getUserWallet(userWalletId: UserWalletId): UserWallet } \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt new file mode 100644 index 0000000000..c34240e22b --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case for retrieving the next unoccupied account index + * + * @property crudRepository repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetUnoccupiedAccountIndexUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Invokes the use case to calculate the next unoccupied account index + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val totalAccountsCount = getTotalAccountsCount(userWalletId = userWalletId) + + DerivationIndex(totalAccountsCount + 1).getOrElse { + raise(Error.InvalidDerivationIndex(it)) + } + } + + private suspend fun Raise.getTotalAccountsCount(userWalletId: UserWalletId): Int { + return catch( + block = { crudRepository.getTotalAccountsCount(userWalletId = userWalletId) }, + catch = { raise(Error.DataOperationFailed(cause = it)) }, + ) + } + + /** + * Represents possible errors that can occur in the use case + */ + sealed interface Error { + + val tag: String + get() = this::class.simpleName ?: "GetUnoccupiedAccountIndexUseCase.Error" + + /** Error indicating that the derivation index is invalid */ + data class InvalidDerivationIndex(val cause: DerivationIndex.Error) : Error { + override fun toString(): String = "$tag: Invalid derivation index: $cause" + } + + /** Error indicating that a data operation failed */ + data class DataOperationFailed(val cause: Throwable) : Error { + override fun toString(): String = "$tag: Data operation failed: ${cause.message ?: "Unknown error"}" + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt new file mode 100644 index 0000000000..ec8af7f2b7 --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.account.usecase + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetUnoccupiedAccountIndexUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetUnoccupiedAccountIndexUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should return next unoccupied index when repository returns count`() = runTest { + // Arrange + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } returns 3 + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = 4.right() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } + + @Test + fun `invoke should return error if repository throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + coEvery { crudRepository.getTotalAccountsCount(userWalletId) } throws exception + + // Act + val actual = useCase(userWalletId = userWalletId) + + // Assert + val expected = GetUnoccupiedAccountIndexUseCase.Error.DataOperationFailed(exception).left() + Truth.assertThat(actual).isEqualTo(expected) + + coVerify { crudRepository.getTotalAccountsCount(userWalletId) } + } +} \ No newline at end of file diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts index e68d20b144..dc77a38d28 100644 --- a/features/account/impl/build.gradle.kts +++ b/features/account/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.utils) implementation(projects.core.ui) + implementation(projects.core.error) implementation(projects.core.res) implementation(projects.core.decompose) implementation(projects.core.navigation) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index b4763b9c92..0f5a66dedf 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,5 +1,7 @@ package com.tangem.features.account.createedit +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,11 +11,14 @@ import com.tangem.core.res.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.utils.showErrorDialog import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase +import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.common.toDomain import com.tangem.features.account.createedit.entity.AccountCreateEditUM @@ -21,15 +26,20 @@ import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect +import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateDerivationIndex import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateIconSelect import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName +import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped +@Suppress("LongParameterList") internal class AccountCreateEditModel @Inject constructor( paramsContainer: ParamsContainer, private val messageSender: UiMessageSender, @@ -37,13 +47,21 @@ internal class AccountCreateEditModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val updateCryptoPortfolioUseCase: UpdateCryptoPortfolioUseCase, private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase, + private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model() { private val params = paramsContainer.require() private val umBuilder = AccountCreateEditUMBuilder(params) - val uiState: StateFlow get() = _uiState - private val _uiState = MutableStateFlow(value = getInitialState()) + val uiState: StateFlow + field = MutableStateFlow(value = getInitialState()) + + init { + if (params is AccountCreateEditComponent.Params.Create) { + updateDerivationInfo(userWalletId = params.userWalletId) + } + } private fun unsaveChangeDialog() { val secondAction = EventMessageAction( @@ -74,13 +92,16 @@ internal class AccountCreateEditModel @Inject constructor( private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) { val state = uiState.value - val name = AccountName(state.account.name).getOrNull() ?: return + val name = AccountName(value = state.account.name).getOrNull() ?: return val icon = state.account.portfolioIcon.toDomain() + val index = state.account.derivationInfo.index ?: return + val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return + addCryptoPortfolioUseCase( userWalletId = params.userWalletId, accountName = name, icon = icon, - derivationIndex = DerivationIndex.Main, // todo account + derivationIndex = derivationIndex, ) } @@ -100,19 +121,19 @@ internal class AccountCreateEditModel @Inject constructor( private fun onCloseClick() = unsaveChangeDialog() private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateIconSelect(icon) .validateNewState() } private fun onColorSelect(color: CryptoPortfolioIcon.Color) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateColorSelect(color) .validateNewState() } private fun onNameChange(name: String) { - _uiState.value = uiState.value + uiState.value = uiState.value .updateName(name) .validateNewState() } @@ -140,4 +161,35 @@ internal class AccountCreateEditModel @Inject constructor( onCloseClick = ::onCloseClick, ) } + + private fun updateDerivationInfo(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.default) { + getUnoccupiedAccountIndexUseCase(userWalletId = userWalletId) + .onRight { derivationIndex -> + uiState.update { + it.updateDerivationIndex(derivationIndex = derivationIndex.value) + } + } + .onLeft { + handleError( + error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex, + params = mapOf("userWalletId" to userWalletId.stringValue), + ) + + return@launch + } + } + } + + private fun handleError(error: AccountFeatureError, params: Map = mapOf()) { + val exception = IllegalStateException(error.toString()) + + Timber.e(exception) + + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent(exception = exception, params = params), + ) + + messageSender.showErrorDialog(universalError = error, onDismiss = router::pop) + } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index 4b133a4d97..df93dde4b9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -17,11 +17,23 @@ data class AccountCreateEditUM( data class Account( val name: String, val portfolioIcon: CryptoPortfolioIconUM, - val derivationInfo: TextReference, + val derivationInfo: DerivationInfo, val inputPlaceholder: TextReference, val onNameChange: (String) -> Unit, ) + sealed interface DerivationInfo { + val text: TextReference + val index: Int? + + data class Content(override val text: TextReference, override val index: Int) : DerivationInfo + + data object Empty : DerivationInfo { + override val text: TextReference = TextReference.EMPTY + override val index: Int? = null + } + } + data class Colors( val selected: CryptoPortfolioIcon.Color, val list: ImmutableList, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index 41f12322bb..bacbd306ab 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -3,15 +3,15 @@ package com.tangem.features.account.createedit.entity import com.tangem.core.res.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.common.toUM import kotlinx.collections.immutable.toImmutableList -import javax.inject.Inject -internal class AccountCreateEditUMBuilder @Inject constructor( - val params: AccountCreateEditComponent.Params, +internal class AccountCreateEditUMBuilder( + private val params: AccountCreateEditComponent.Params, ) { private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList() @@ -29,14 +29,16 @@ internal class AccountCreateEditUMBuilder @Inject constructor( is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account( name = "", portfolioIcon = createIcon, - derivationInfo = TextReference.EMPTY, + derivationInfo = AccountCreateEditUM.DerivationInfo.Empty, inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account), onNameChange = onNameChange, ) is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( name = params.account.name.value, portfolioIcon = params.account.portfolioIcon.toUM(), - derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ? + derivationInfo = createAccountDerivationInfo( + index = (params.account as Account.CryptoPortfolio).derivationIndex.value, + ), inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account), onNameChange = onNameChange, ) @@ -113,5 +115,25 @@ internal class AccountCreateEditUMBuilder @Inject constructor( fun AccountCreateEditUM.updateButton(isButtonEnabled: Boolean): AccountCreateEditUM { return this.copy(buttonState = this.buttonState.copy(isButtonEnabled = isButtonEnabled)) } + + fun AccountCreateEditUM.updateDerivationIndex(derivationIndex: Int): AccountCreateEditUM { + return this.copy( + account = this.account.copy( + derivationInfo = createAccountDerivationInfo(index = derivationIndex), + ), + ) + } + + private fun createAccountDerivationInfo(index: Int): AccountCreateEditUM.DerivationInfo { + val derivationIndexText = if (index.toString().length == 1) "0$index" else "$index" + + return AccountCreateEditUM.DerivationInfo.Content( + text = resourceReference( + id = R.string.account_form_account_index, + formatArgs = wrappedList(derivationIndexText), + ), + index = index, + ) + } } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt new file mode 100644 index 0000000000..9ab8549fc7 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/error/AccountFeatureError.kt @@ -0,0 +1,30 @@ +package com.tangem.features.account.createedit.error + +import com.tangem.core.error.UniversalError + +sealed interface AccountFeatureError : UniversalError { + + val subsystemCode: String + val specificErrorCode: String + + override val errorCode: Int + get() = "108$subsystemCode$specificErrorCode".toInt() + + sealed interface CreateAccount : AccountFeatureError { + + override val subsystemCode: String get() = "001" + + data object UnableToGetDerivationIndex : CreateAccount { + override val specificErrorCode: String = "001" + } + } + + sealed interface EditAccount : AccountFeatureError { + + override val subsystemCode: String get() = "002" + + data object RequiredCryptoPortfolio : EditAccount { + override val specificErrorCode: String = "001" + } + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 2f98a553bc..b94e9198fe 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -32,10 +32,7 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.fields.AutoSizeTextField -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.account.CryptoPortfolioIcon @@ -76,7 +73,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi SpacerH8() Text( modifier = Modifier.padding(horizontal = 8.dp), - text = state.account.derivationInfo.resolveReference(), + text = state.account.derivationInfo.text.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -93,7 +90,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi } @Composable -private fun AccountSummary(account: AccountCreateEditUM.Account) { +private fun AccountSummary(account: Account) { Column( modifier = Modifier .clip(RoundedCornerShape(16.dp)) @@ -126,7 +123,7 @@ private fun AccountSummary(account: AccountCreateEditUM.Account) { } @Composable -private fun AccountIcon(account: AccountCreateEditUM.Account) { +private fun AccountIcon(account: Account) { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -308,7 +305,10 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider Date: Fri, 15 Aug 2025 16:29:40 +0500 Subject: [PATCH 29/87] Updated on 2026-08-14 --- .../NavigationButtonsBlock.kt | 82 ++++++++--- .../NavigationButtonsState.kt | 3 +- .../preview/NavigationButtonsPreview.kt | 36 +++-- .../features/send/v2/common/ui/SendContent.kt | 15 +- .../v2/common/ui/SendNavigationButtons.kt | 135 ------------------ .../features/send/v2/common/ui/SendingText.kt | 51 ------- .../v2/send/confirm/model/SendConfirmModel.kt | 3 +- .../v2/send/confirm/ui/SendConfirmContent.kt | 2 +- .../success/ui/SendConfirmSuccessContent.kt | 30 ++-- .../confirm/model/NFTSendConfirmModel.kt | 3 +- .../confirm/ui/NFTSendConfirmContent.kt | 2 +- .../SetButtonsStateTransformer.kt | 44 +++--- .../confirm/model/SendWithSwapConfirmModel.kt | 3 + .../success/ui/SendWithSwapSuccessContent.kt | 70 +-------- .../sendviaswap/ui/SendWithSwapContent.kt | 32 ++--- 15 files changed, 155 insertions(+), 356 deletions(-) delete mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt delete mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index b612670c59..5f2d7ca019 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -22,18 +24,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.SecondaryButtonIconStart 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.components.buttons.common.contentColor import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty -import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import kotlinx.collections.immutable.ImmutableList +import com.tangem.core.ui.utils.singleEvent @Composable fun NavigationButtonsBlock( @@ -47,7 +48,7 @@ fun NavigationButtonsBlock( modifier = modifier.fillMaxWidth(), ) { InfoText(footerText) - ExtraButtons(state?.extraButtons, state?.txUrl) + DoneButtons(state?.extraButtons) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), @@ -58,9 +59,33 @@ fun NavigationButtonsBlock( } } +@Composable +fun NavigationButtonsBlockV2( + navigationUM: NavigationUM, + modifier: Modifier = Modifier, + footerText: TextReference? = null, +) { + val navigationUM = navigationUM as? NavigationUM.Content + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier.fillMaxWidth(), + ) { + InfoText(footerText) + DoneButtons(navigationUM?.secondaryPairButtonsUM) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + PreviousButton(navigationUM?.prevButton) + NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f)) + } + } +} + @Composable fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) { val wrappedButton by rememberNavigationButton(primaryButton) + val hapticFeedback = LocalHapticFeedback.current AnimatedContent( targetState = wrappedButton, transitionSpec = { navigationButtonsTransition() }, @@ -83,7 +108,12 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier TangemButton( text = button.textReference.resolveReference(), enabled = button.isEnabled, - onClick = button.onClick, + onClick = { + if (button.isHapticClick) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + button.onClick() + }, showProgress = button.showProgress, colors = color, textStyle = TangemTheme.typography.subtitle1, @@ -123,33 +153,39 @@ private fun PreviousButton(prevButton: NavigationButton?) { } @Composable -private fun ExtraButtons(extraButtons: ImmutableList?, txUrl: String?) { +fun DoneButtons(pairButtons: Pair?, modifier: Modifier = Modifier) { AnimatedVisibility( - visible = !txUrl.isNullOrBlank() && extraButtons != null, + visible = pairButtons != null, enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()), exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()), label = "Animate show sent state buttons", - modifier = Modifier.fillMaxWidth(), + modifier = modifier.fillMaxWidth(), ) { - val buttons = remember(this) { requireNotNull(extraButtons) } + val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtons) } Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), ) { - buttons.forEach { button -> - val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) } - ?: TangemButtonIconPosition.None - TangemButton( - text = button.textReference.resolveReference(), - icon = icon, - textStyle = TangemTheme.typography.subtitle1, - onClick = rememberHapticFeedback(state = button, onAction = button.onClick), - modifier = Modifier.weight(1f), - enabled = button.isEnabled, - showProgress = false, - colors = TangemButtonsDefaults.secondaryButtonColors, - ) - } + SecondaryButtonIconStart( + text = leftButton.textReference.resolveReference(), + iconResId = requireNotNull(leftButton.iconRes), + onClick = { + singleEvent { + leftButton.onClick() + } + }, + modifier = Modifier.weight(1f), + ) + SecondaryButtonIconStart( + text = rightButton.textReference.resolveReference(), + iconResId = requireNotNull(rightButton.iconRes), + onClick = { + singleEvent { + rightButton.onClick() + } + }, + modifier = Modifier.weight(1f), + ) } } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index e0bddfab66..772c493cdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -2,7 +2,6 @@ package com.tangem.common.ui.navigationButtons import androidx.annotation.DrawableRes import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList sealed class NavigationButtonsState { data object Empty : NavigationButtonsState() @@ -10,7 +9,7 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton?, val prevButton: NavigationButton?, - val extraButtons: ImmutableList, + val extraButtons: Pair?, val txUrl: String? = null, val onTextClick: (String) -> Unit, ) : NavigationButtonsState() diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 9d2e71fe85..c409bb5b95 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -5,29 +5,25 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import kotlinx.collections.immutable.persistentListOf internal object NavigationButtonsPreview { - private val extraButtons = persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = {}, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_tangem_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = {}, - ), + private val extraButtons = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_tangem_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = {}, ) private val prev = NavigationButton( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 289b609dff..0f860e75fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -5,9 +5,13 @@ import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.* +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.decompose.ComposableContentComponent @@ -45,7 +49,14 @@ internal fun SendContent( it.instance.Content(Modifier.weight(1f)) } if (stackState.active.configuration != CommonSendRoute.ConfirmSuccess) { - SendNavigationButtons(navigationUM = navigationUM) + NavigationButtonsBlockV2( + navigationUM = navigationUM, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt deleted file mode 100644 index 31cf103e3d..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendNavigationButtons.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.* -import androidx.compose.foundation.background -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.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.ui.navigationButtons.NavigationButton -import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.singleEvent - -@Composable -internal fun SendNavigationButtons(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val navigationUM = navigationUM as? NavigationUM.Content ?: return - - Column( - modifier = modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - SendDoneButtons(navigationUM.secondaryPairButtonsUM) - SendNavigationButton( - navigationUM = navigationUM, - ) - } -} - -@Composable -private fun SendNavigationButton(navigationUM: NavigationUM, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - val navigationUM = navigationUM as? NavigationUM.Content ?: return - val primaryButton = navigationUM.primaryButton - - Row(modifier = modifier) { - AnimatedVisibility( - visible = navigationUM.prevButton != null, - enter = expandHorizontally(expandFrom = Alignment.End), - exit = shrinkHorizontally(shrinkTowards = Alignment.End), - ) { - val wrappedNavigationUM = remember(this) { requireNotNull(navigationUM.prevButton) } - Row { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_back_24)), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(TangemTheme.colors.button.secondary) - .clickableSingle(onClick = wrappedNavigationUM.onClick) - .padding(12.dp), - ) - SpacerW12() - } - } - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = { - if (primaryButton.isHapticClick) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - primaryButton.onClick() - }, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - ) - } -} - -@Composable -private fun SendDoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row(modifier = Modifier.padding(bottom = 12.dp)) { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = leftButton.iconRes!!, - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = rightButton.iconRes!!, - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt deleted file mode 100644 index da368be38b..0000000000 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendingText.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.send.v2.common.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme - -@Composable -internal fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) { - var isVisibleProxy by remember { mutableStateOf(footerText != TextReference.EMPTY) } - val keyboard by keyboardAsState() - - // the text should appear when the keyboard is closed - LaunchedEffect(footerText != TextReference.EMPTY, keyboard) { - if (footerText != TextReference.EMPTY && keyboard is Keyboard.Opened) { - return@LaunchedEffect - } - isVisibleProxy = footerText != TextReference.EMPTY - } - - AnimatedVisibility( - visible = isVisibleProxy, - modifier = modifier, - enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), - exit = fadeOut(tween(durationMillis = 300)), - label = "Animate show sending state text", - ) { - Text( - text = footerText.resolveAnnotatedReference(), - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - ) - } -} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index a458d480c5..1fb318f5c1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -632,7 +632,8 @@ internal class SendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, onClick = { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 73b2a5a0fa..7f7c70235b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.TextReference @@ -22,7 +23,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.FeeSelectorBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 862d4e1483..179bc3f11b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -1,17 +1,18 @@ package com.tangem.features.send.v2.send.success.ui import androidx.compose.animation.* +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.scrollable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.ui.AmountBlock -import com.tangem.core.ui.components.SpacerHMax +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -20,7 +21,6 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent -import com.tangem.features.send.v2.common.ui.SendNavigationButtons import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM @@ -50,7 +50,11 @@ internal fun SendConfirmSuccessContent( exit = slideOutVertically().plus(fadeOut()), label = "Animate success content", ) { - Column { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { Column( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) @@ -81,9 +85,19 @@ internal fun SendConfirmSuccessContent( ) destinationBlockComponent.Content(modifier = Modifier) feeBlockComponent.Content(modifier = Modifier) + Spacer(Modifier.height(60.dp)) } - SpacerHMax() - SendNavigationButtons(navigationUM = sendUM.navigationUM) + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index ae95518260..522470a90e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -428,7 +428,8 @@ internal class NFTSendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconRes = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, onClick = { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index 9dbb04eb6d..c2a2de5774 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.transactions.TransactionDoneTitle @@ -20,7 +21,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent -import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 9c1ea68323..4c0166c007 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -13,8 +13,6 @@ import com.tangem.features.staking.impl.presentation.state.utils.getPendingActio import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal class SetButtonsStateTransformer( private val urlOpener: UrlOpener, @@ -23,12 +21,13 @@ internal class SetButtonsStateTransformer( override fun transform(prevState: StakingUiState): StakingUiState { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl val buttonsState = if (prevState.isButtonsVisible()) { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), prevButton = getPrevButton(prevState), - extraButtons = getExtraButtons(prevState), - txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl, + extraButtons = getExtraButtons(prevState).takeIf { txUrl != null }, + txUrl = txUrl, onTextClick = urlOpener::openUrl, ) } else { @@ -77,26 +76,23 @@ internal class SetButtonsStateTransformer( ).takeIf { prevState.currentStep.isPrevButtonVisible() } } - private fun getExtraButtons(prevState: StakingUiState): ImmutableList { - return persistentListOf( - NavigationButton( - textReference = resourceReference(R.string.common_explore), - iconRes = R.drawable.ic_web_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onExploreClick, - ), - NavigationButton( - textReference = resourceReference(R.string.common_share), - iconRes = R.drawable.ic_share_24, - isSecondary = true, - isIconVisible = true, - showProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onShareClick, - ), + private fun getExtraButtons(prevState: StakingUiState): Pair { + return NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + isSecondary = true, + isIconVisible = true, + showProgress = false, + isEnabled = true, + onClick = prevState.clickIntents::onShareClick, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index a5aa42ce31..cebaaba9dd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -406,6 +406,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( it.second is SendWithSwapRoute.Confirm }.onEach { (state, _) -> val confirmUM = state.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isTransactionInProcess params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( @@ -416,6 +417,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( primaryButton = NavigationButton( textReference = resourceReference(R.string.common_send), iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isHapticClick = isReadyToSend, isEnabled = confirmUM.isPrimaryButtonEnabled, onClick = { when (confirmUM) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 2ad404315d..e77b8efd44 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -1,7 +1,6 @@ package com.tangem.features.swap.v2.impl.sendviaswap.success.ui import android.content.res.Configuration -import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -10,14 +9,9 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll 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 -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.transaction.Fee @@ -25,9 +19,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.InputRowBestRate @@ -41,7 +35,6 @@ import com.tangem.core.ui.format.bigdecimal.format 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.singleEvent import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType @@ -64,8 +57,6 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -private const val GRADIENT_ALPHA = 0.3f - @Composable internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { if (sendWithSwapUM.navigationUM !is NavigationUM.Content) return @@ -112,24 +103,15 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { FeeBlock(feeSelectorUM = feeSelectorUM) Spacer(Modifier.height(60.dp)) } - DoneButtons( - pairButtonsUM = sendWithSwapUM.navigationUM.secondaryPairButtonsUM, + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = sendWithSwapUM.navigationUM, modifier = Modifier .align(Alignment.BottomCenter) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - TangemTheme.colors.background.tertiary.copy(GRADIENT_ALPHA), - TangemTheme.colors.background.tertiary, - ), - ), - ) .padding( - top = 24.dp, - bottom = 12.dp, start = 16.dp, end = 16.dp, + bottom = 16.dp, ), ) } @@ -289,46 +271,6 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m } } -// TODO remove [REDACTED_TASK_KEY] -@Composable -private fun DoneButtons(pairButtonsUM: Pair?, modifier: Modifier = Modifier) { - val hapticFeedback = LocalHapticFeedback.current - - AnimatedVisibility( - visible = pairButtonsUM != null, - modifier = modifier, - enter = slideInVertically().plus(fadeIn()), - exit = slideOutVertically().plus(fadeOut()), - label = "Animate show sent state buttons", - ) { - val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtonsUM) } - Row { - SecondaryButtonIconStart( - text = leftButton.textReference.resolveReference(), - iconResId = requireNotNull(leftButton.iconRes), - onClick = { - singleEvent { - leftButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = rightButton.textReference.resolveReference(), - iconResId = requireNotNull(rightButton.iconRes), - onClick = { - singleEvent { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - rightButton.onClick() - } - }, - modifier = Modifier.weight(1f), - ) - } - } -} - // region Preview @Suppress("LongMethod") @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt index e9d03433d4..3075a24638 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/ui/SendWithSwapContent.kt @@ -13,11 +13,9 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.plus import com.arkivanov.decompose.extensions.compose.stack.animation.slide import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton -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.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -57,26 +55,14 @@ internal fun SendWithSwapContent( ) { it.instance.Content(Modifier.weight(1f)) } - // TODO refactor [REDACTED_TASK_KEY] - val primaryButton = navigationUM.primaryButton - Row( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - ) { - TangemButton( - modifier = Modifier.fillMaxWidth(), - text = primaryButton.textReference.resolveReference(), - icon = primaryButton.iconRes?.let { - TangemButtonIconPosition.End(it) - } ?: TangemButtonIconPosition.None, - enabled = primaryButton.isEnabled, - onClick = primaryButton.onClick, - showProgress = false, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, + if (stackState.active.configuration != SendWithSwapRoute.Success) { + NavigationPrimaryButton( + navigationUM.primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), ) } } From 26ba1b1c4cec4ae6ab2d4f23354e48f97e300728 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Aug 2025 18:29:37 +0700 Subject: [PATCH 30/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 03c2b9d8b5..590764ebe8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1147" +tangemBlockchainSdk = "releases-5.27.0-1148" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.27.0-513" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 58b345126c045af04de02bbbf88c564f1be22904 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 20:10:29 +0400 Subject: [PATCH 31/87] Updated on 2026-08-14 --- .../wallet/child/wallet/model/WalletModel.kt | 15 ++++++++++++--- .../wallet/model/intents/WalletClickIntents.kt | 5 +++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index e62305cd29..467321629b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase @@ -383,9 +384,11 @@ internal class WalletModel @Inject constructor( val otherWallets = action.wallets.minus(action.selectedWallet) if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - otherWallets.onEach { userWallet -> - modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } - } + otherWallets + .filterNot(UserWallet::isLocked) + .onEach { userWallet -> + modelScope.launch { walletContentFetcher(userWalletId = userWallet.walletId) } + } } if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) { @@ -500,6 +503,10 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, coroutineScope = modelScope, ) + + action.unlockedWallets.onEach { userWallet -> + modelScope.launch { fetchWalletContent(userWallet = userWallet) } + } } private fun demonstrateWalletsScrollPreview(direction: Direction) { @@ -541,6 +548,8 @@ internal class WalletModel @Inject constructor( private suspend fun fetchWalletContent(userWallet: UserWallet) { if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + if (userWallet.isLocked) return + /* * Updating the balance of the current wallet is an essential part of InitializationWallets, * so the coroutine is launched in the current context diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index a7454d94db..55fd1893e3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -5,13 +5,14 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -87,7 +88,7 @@ internal class WalletClickIntents @Inject constructor( stateHolder.update { it.copy(selectedWalletIndex = index) } maybeUserWallet.onRight { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + if (tokensFeatureToggles.isWalletBalanceFetcherEnabled && !it.isLocked) { launch { walletContentFetcher(userWalletId = it.walletId) } } From c2082a822c74b3f767e7ba49cf91e8fbb40da519 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Aug 2025 19:11:29 +0500 Subject: [PATCH 32/87] Updated on 2026-08-14 --- .../provider/ProviderChooseCrypto.kt | 1 - .../ui/extensions/ComposeNavigationExt.kt | 49 ------------- .../com/tangem/core/ui/extensions/Fragment.kt | 43 ----------- .../tangem/core/ui/extensions/ModifierExt.kt | 19 +++-- .../ui/screen/ComposeBottomSheetFragment.kt | 72 ------------------- .../tangem/core/ui/screen/ComposeFragment.kt | 50 ------------- .../ui/OnboardingVisaChooseWallet.kt | 20 +----- .../ui/FeeSelectorModalBottomSheet.kt | 3 +- .../ui/SwapChooseProviderBottomSheet.kt | 5 +- .../SwapChooseProviderContentPreview.kt | 6 +- 10 files changed, 16 insertions(+), 252 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt index 0370272dcc..ace47189b0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -51,7 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { ConstraintLayout( modifier = modifier - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = providerChooseUM.isSelected) .clickable( enabled = !providerChooseUM.hasError(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt deleted file mode 100644 index 2dc5bcd17e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ComposeNavigationExt.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.core.ui.extensions - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.ViewModel -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavController -import timber.log.Timber - -/** - * The ViewModel is scoped to the parent route Navigation graph - * and is provided using the Hilt-generated ViewModel factory - * - * ``` - * val navController = rememberNavController() - * - * navigation( - * route = "parent", - * startDestination = "parent/1" - * ) { - * composable("route/1") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/2") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * composable("route/3") { entry -> - * val viewModel = entry.parentHiltViewModel(navController) - * } - * } - * ``` - * - * @param navController NavController within the common NavGraph - * @throws Exception if there is no parent route - */ -@Composable -inline fun NavBackStackEntry.parentHiltViewModel(navController: NavController): T { - val viewModelStoreOwner = remember(this) { - try { - navController.getBackStackEntry(this.destination.parent!!.id) - } catch (e: Exception) { - Timber.tag("scopedViewModel").e(e, "There is no parent route'") - throw e - } - } - - return hiltViewModel(viewModelStoreOwner) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt deleted file mode 100644 index e58126708e..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.core.ui.extensions - -import android.R -import android.content.Context -import android.graphics.Color.* -import android.view.WindowManager -import androidx.annotation.ColorRes -import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat -import androidx.fragment.app.Fragment -import kotlin.math.sqrt - -@Deprecated("Use only in legacy fragments") -fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) { - with(requireActivity().window) { - clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) - addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - statusBarColor = ContextCompat.getColor(requireContext(), colorResId) - val view = view ?: return - val windowInsetsController = WindowCompat.getInsetsController(this, view) - windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId) - } -} - -// TODO replace by android.graphics.luminance() after bump min API to 24 -@Suppress("MagicNumber") -fun luminance(context: Context, @ColorRes colorRes: Int): Boolean { - val color = context.resources.getColor(colorRes, null) - if (R.color.transparent == color) return true - var rtnValue = false - val rgb = intArrayOf(red(color), green(color), blue(color)) - val brightness = sqrt( - rgb[0] * rgb[0] * .241 + - rgb[1] * rgb[1] * .691 + - rgb[2] * rgb[2] * .068, - ).toInt() - - // color is light - if (brightness >= 200) { - rtnValue = true - } - return rtnValue -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index ef94c6b681..cab83ed0d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -72,26 +71,24 @@ fun Modifier.conditionalCompose( fun Modifier.selectedBorder( isSelected: Boolean, width: Dp = 2.5.dp, - color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + color: Color = TangemTheme.colors.text.accent, radius: Dp = 16.dp, ) = conditionalCompose( condition = isSelected, modifier = { - border( + outsetBorder( width = width, - color = color, - shape = RoundedCornerShape(radius), + color = color.copy(alpha = 0.15f), + shape = RoundedCornerShape(radius + 2.dp), ) - .padding(width) .border( width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(radius - 2.dp), + color = color, + shape = RoundedCornerShape(radius), ) - .clip(RoundedCornerShape(radius - 2.dp)) + .clip(RoundedCornerShape(radius)) }, otherModifier = { - padding(width) - .clip(RoundedCornerShape(radius - 2.dp)) + clip(RoundedCornerShape(radius)) }, ) \ 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 deleted file mode 100644 index c5c4285292..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.core.ui.screen - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.FloatRange -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Modifier -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.tangem.core.ui.R -import com.tangem.core.ui.res.TangemTheme - -/** - * An abstract base class for bottom sheet dialogs that use Compose for UI rendering. - * Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface. - */ -abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen { - - /** - * The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED]. - */ - open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED - - /** - * The fraction of the screen height that the bottom sheet should take when expanded. - * Default is `null`, indicating that the height will be determined by the content. - */ - @FloatRange(from = 0.0, to = 1.0) - open val expandedHeightFraction: Float? = null - - override val screenModifier: Modifier - @Composable - @ReadOnlyComposable - get() = Modifier - .fillMaxWidth() - .let { - if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it - } - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.bottomSheet, - ) - - override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView( - context = inflater.context, - activity = requireActivity(), - overrideSystemBarColors = false, - ) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) - - (dialog as BottomSheetDialog).behavior.apply { - state = initialBottomSheetState - skipCollapsed = true - } - - return dialog - } -} \ No newline at end of file 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 deleted file mode 100644 index 48565c82d7..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.core.ui.screen - -import android.content.res.Configuration -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.transition.TransitionInflater -import com.tangem.core.ui.R - -/** - * An abstract base class for fragments that use Compose for UI rendering. - * Extends [Fragment] and implements [ComposeScreen] interface. - */ -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, requireActivity()).also { - it.isTransitionGroup = isTransitionsInflated - } - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - /* - * We need to manually dispatch configuration changes to the Compose view. - * - - * `android:configChanges="uiMode"` is set in the manifest. - * */ - view?.dispatchConfigurationChanged(newConfig) - } - - /** - * Inflates transitions for the fragment. Override this method to customize - * enter and exit transitions for the fragment. - * - * @return `true` if transitions were inflated; `false` otherwise. - */ - protected open fun TransitionInflater.inflateTransitions(): Boolean { - enterTransition = inflateTransition(R.transition.fade) - exitTransition = inflateTransition(R.transition.fade) - - return true - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt index 5cd9a11f10..dff0ca06c5 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -1,15 +1,11 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon 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.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -19,9 +15,9 @@ import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.outsetBorder import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -112,17 +108,7 @@ private fun SelectableChainRow( RowContentContainer( modifier = modifier .heightIn(min = 48.dp) - .outsetBorder( - color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent, - width = 5.dp, - shape = RoundedCornerShape(size = 18.dp), - ) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .border( - width = 1.dp, - color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) + .selectedBorder(selected) .clickable(onClick = onClick) .padding(12.dp), icon = { @@ -163,7 +149,7 @@ private fun Preview() { ), ), selectedOption = SelectableChainRowUM( - event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet, + event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet, icon = R.drawable.ic_tangem_24, text = TextReference.Str("Tangem Wallet"), ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 5b1406dd0b..8f3ef2cb54 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -79,7 +79,7 @@ internal fun FeeSelectorModalBottomSheet( FeeSelectorItems( state = state, feeSelectorIntents = feeSelectorIntents, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 13.dp), + modifier = Modifier.padding(vertical = 4.dp, horizontal = 12.dp), ) }, footer = { @@ -141,7 +141,6 @@ private fun FeeSelectorItems( ) val itemModifier = Modifier .fillMaxWidth() - .background(TangemTheme.colors.background.primary) .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 9772ead6c8..77f8f0cbef 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -5,14 +5,12 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape 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.alpha -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -65,7 +63,7 @@ internal fun SwapChooseProviderContent( ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.padding(horizontal = 13.dp), + modifier = modifier.padding(horizontal = 12.dp), ) { Text( text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint), @@ -89,7 +87,6 @@ internal fun SwapChooseProviderContent( SwapProviderItem( state = provider.swapProviderState, modifier = Modifier - .clip(RoundedCornerShape(14.dp)) .selectedBorder(isSelected = provider.swapProviderState.isSelected) .clickable( enabled = provider.quote !is SwapQuoteUM.Error, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 0975e02bdf..bf7a8ef284 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -96,13 +96,13 @@ internal object SwapChooseProviderContentPreview { ), quote = quote2, swapProviderState = SwapProviderState.Content( - name = provider1.name, - type = provider1.type.typeName, + name = provider2.name, + type = provider2.type.typeName, iconUrl = "", subtitle = stringReference("1800 POL"), additionalBadge = SwapProviderState.AdditionalBadge.BestTrade, diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, - isSelected = true, + isSelected = false, ), ), ), From dbbbdd2d33e99a38aa0dbba0358d13c4dba35a51 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:27:38 +0500 Subject: [PATCH 33/87] Updated on 2026-08-14 --- .../DefaultFeeSelectorBlockComponent.kt | 5 +++- .../feeselector/ui/FeeSelectorBlockContent.kt | 24 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index 7bfbe898e3..5cb0f113e0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -18,6 +18,7 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import com.tangem.features.send.v2.feeselector.ui.FeeSelectorBlockContent +import com.tangem.utils.extensions.isSingleItem import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -73,11 +74,13 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( val state by model.uiState.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() + val isScreenSource = params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen + val isNotSingleFee = (state as? FeeSelectorUM.Content)?.feeItems?.isSingleItem() == false FeeSelectorBlockContent( state = state, onReadMoreClick = model::onReadMoreClicked, modifier = modifier - .conditional(params.feeDisplaySource == FeeSelectorParams.FeeDisplaySource.Screen) { + .conditional(isScreenSource && isNotSingleFee) { Modifier.clickable { model.showFeeSelector() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index b0f08978aa..2f403f5b0c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -39,6 +39,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.impl.R +import com.tangem.utils.extensions.isSingleItem import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -118,12 +119,11 @@ private fun FeeSelectorStaticPart(onReadMoreClick: () -> Unit, modifier: Modifie text = annotatedString, modifier = Modifier .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), + .size(TangemTheme.dimens.size16) + .clip(CircleShape), content = { contentModifier -> Icon( - modifier = contentModifier - .size(TangemTheme.dimens.size16) - .clip(CircleShape), + modifier = contentModifier.size(TangemTheme.dimens.size16), painter = painterResource(id = R.drawable.ic_token_info_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, @@ -176,12 +176,14 @@ private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifi textAlign = TextAlign.End, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), ) - Icon( - modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource(id = R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) + if (!state.feeItems.isSingleItem()) { + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } } } @@ -220,7 +222,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider { ), FeeSelectorUM.Content( isPrimaryButtonEnabled = false, - feeItems = persistentListOf(maxFeeItem), + feeItems = persistentListOf(lowFeeItem, maxFeeItem), selectedFeeItem = maxFeeItem, feeExtraInfo = FeeExtraInfo( isFeeApproximate = false, From d6f8549bd26eda1920985a5714eb7c996153ff8e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 16:27:45 +0500 Subject: [PATCH 34/87] Updated on 2026-08-14 --- .../model/converter/SwapQuoteUMConverter.kt | 2 + .../SwapAmountSetQuotesTransformer.kt | 24 +++- .../impl/amount/ui/SwapAmountBlockContent.kt | 125 ++++++++++++------ .../ui/preview/SwapAmountContentPreview.kt | 1 + .../ui/SwapChooseProviderContent.kt | 11 +- .../SwapChooseProviderContentPreview.kt | 2 + .../swap/v2/impl/common/entity/SwapQuoteUM.kt | 1 + 7 files changed, 118 insertions(+), 48 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index f5738cbd5e..c014e539e7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -57,6 +57,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } else { @@ -68,6 +69,7 @@ internal class SwapQuoteUMConverter( quote.toTokenAmount.toQuoteValue(), ), rate = annotatedReference(rateString), + isSingleProvider = false, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index b9d220ba0b..adfcc37a91 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.Differ import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.StringsSigns import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -26,12 +27,20 @@ internal class SwapAmountSetQuotesTransformer( override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + val isSingleProvider = quotes.filter { + it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance || + (it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + }.isSingleItem() + val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { prevState.selectedQuote } else { - (bestQuote as? SwapQuoteUM.Content)?.copy(diffPercent = DifferencePercent.Best) ?: bestQuote + (bestQuote as? SwapQuoteUM.Content)?.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) ?: bestQuote } val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( @@ -47,16 +56,23 @@ internal class SwapAmountSetQuotesTransformer( return updatedState.copy( isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote), + swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), ) } - private fun getQuotesWithDiff(sortedQuotes: List, bestQuote: SwapQuoteUM): ImmutableList { + private fun getQuotesWithDiff( + sortedQuotes: List, + bestQuote: SwapQuoteUM, + isSingleProvider: Boolean, + ): ImmutableList { return sortedQuotes.sortedWith(SwapQuotesComparator) .map { quote -> if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy(diffPercent = DifferencePercent.Best) + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { // current / selected - 1 val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index c68f7e3923..f7129e04bc 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.ConstraintLayoutScope import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.ui.extensions.TextReference @@ -34,6 +36,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -63,33 +66,11 @@ internal fun SwapAmountBlockContent( ), ) { val (from, to, separator, provider) = createRefs() - AmountBlockV2( - amountState = amountUM.primaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(from) { - top.linkTo(parent.top) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) }, - ) - AmountBlockV2( - amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( - title = resourceReference(R.string.send_with_swap_recipient_amount_title), - availableBalance = TextReference.EMPTY, - availableBalanceShort = TextReference.EMPTY, - ) ?: amountUM.secondaryAmount.amountField, - isClickDisabled = true, - isEditingDisabled = false, - modifier = Modifier.constrainAs(to) { - top.linkTo(from.bottom, 8.dp) - start.linkTo(parent.start) - end.linkTo(parent.end) - }, - extraContent = { - SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick) - }, + SwapAmountBlock( + amountUM = amountUM, + fromAmountRef = from, + toAmountRef = to, + onInfoClick = onInfoClick, ) SwapAmountDivider( modifier = Modifier.constrainAs(separator) { @@ -103,6 +84,7 @@ internal fun SwapAmountBlockContent( val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( isBestRate = isBestRate, + isSingleProvider = quoteContent?.isSingleProvider == true, showBestRateAnimation = amountUM.showBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, onClick = onProviderSelectClick, @@ -119,29 +101,88 @@ internal fun SwapAmountBlockContent( } @Composable -private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) { +private fun ConstraintLayoutScope.SwapAmountBlock( + amountUM: SwapAmountUM.Content, + fromAmountRef: ConstrainedLayoutReference, + toAmountRef: ConstrainedLayoutReference, + onInfoClick: () -> Unit, +) { + AmountBlockV2( + amountState = amountUM.primaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(fromAmountRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.primaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) + AmountBlockV2( + amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( + title = resourceReference(R.string.send_with_swap_recipient_amount_title), + availableBalance = TextReference.EMPTY, + availableBalanceShort = TextReference.EMPTY, + ) ?: amountUM.secondaryAmount.amountField, + isClickDisabled = true, + isEditingDisabled = false, + modifier = Modifier.constrainAs(toAmountRef) { + top.linkTo(fromAmountRef.bottom, 8.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + }, + extraContent = { + SwapPriceImpact( + amountFieldUM = amountUM.secondaryAmount, + selectedAmountType = amountUM.selectedAmountType, + onInfoClick = onInfoClick, + ) + }, + ) +} + +@Composable +private fun SwapPriceImpact( + amountFieldUM: SwapAmountFieldUM, + selectedAmountType: SwapAmountType, + onInfoClick: () -> Unit, +) { + if (amountFieldUM.amountType == selectedAmountType) return + val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact + val iconColor = if (priceImpact != null) { + TangemTheme.colors.icon.attention + } else { + TangemTheme.colors.icon.informative + } + if (priceImpact != null) { Text( text = priceImpact.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.attention, ) - Icon( - painter = rememberVectorPainter( - ImageVector.vectorResource(R.drawable.ic_information_24), - ), - tint = TangemTheme.colors.icon.attention, - contentDescription = null, - modifier = Modifier - .size(20.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = onInfoClick, - ), - ) } + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(R.drawable.ic_information_24), + ), + tint = iconColor, + contentDescription = null, + modifier = Modifier + .size(20.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + onClick = onInfoClick, + ), + ) } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 627af48d84..02419e0cd6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -69,6 +69,7 @@ internal data object SwapAmountContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) val emptyState = SwapAmountUM.Content( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt index 6751315374..a853552187 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderContent.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.delay @Composable fun SwapChooseProviderContent( expressProvider: ExpressProvider?, + isSingleProvider: Boolean, isBestRate: Boolean, showBestRateAnimation: Boolean, onClick: () -> Unit, @@ -62,6 +63,7 @@ fun SwapChooseProviderContent( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, + enabled = !isSingleProvider, ), ) { HorizontalDivider( @@ -88,6 +90,7 @@ fun SwapChooseProviderContent( ProviderInfo( expressProvider = expressProvider, isBestRate = isBestRate, + isSingleProvider = isSingleProvider, showBestRateAnimation = showBestRateAnimation, onFinishAnimation = onFinishAnimation, ) @@ -125,6 +128,7 @@ private fun FcaProviderWarning(modifier: Modifier = Modifier) { private fun ProviderInfo( expressProvider: ExpressProvider?, isBestRate: Boolean, + isSingleProvider: Boolean, showBestRateAnimation: Boolean, onFinishAnimation: () -> Unit, modifier: Modifier = Modifier, @@ -166,6 +170,7 @@ private fun ProviderInfo( start.linkTo(imageRef.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) + end.linkTo(iconRef.start, goneMargin = 12.dp) }, ) Icon( @@ -180,12 +185,13 @@ private fun ProviderInfo( start.linkTo(nameRef.end) top.linkTo(parent.top) bottom.linkTo(parent.bottom) - end.linkTo(parent.end, 12.dp) + end.linkTo(parent.end, margin = 12.dp) + visibility = if (isSingleProvider) Visibility.Gone else Visibility.Visible }, ) BestRateBadge( showBestRateAnimation = showBestRateAnimation, - isBestRate = isBestRate, + isBestRate = isBestRate && !isSingleProvider, ref = imageRef, onFinishAnimation = onFinishAnimation, ) @@ -325,6 +331,7 @@ private fun SwapChooseProviderContent_Preview() { modifier = Modifier.background(TangemTheme.colors.background.tertiary), ) { SwapChooseProviderContent( + isSingleProvider = false, isBestRate = true, showBestRateAnimation = true, expressProvider = ExpressProvider( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 0975e02bdf..f1b3d9f609 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -39,6 +39,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("123"), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, + isSingleProvider = false, ) private val quote2 = SwapQuoteUM.Content( @@ -47,6 +48,7 @@ internal object SwapChooseProviderContentPreview { quoteAmountValue = stringReference("13.12"), rate = stringReference("1 USD ≈ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, + isSingleProvider = false, ) val state = SwapChooseProviderBottomSheetContent( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index ff661ba3ee..c8b7e6f038 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -34,6 +34,7 @@ internal sealed class SwapQuoteUM { val quoteAmount: BigDecimal, val quoteAmountValue: TextReference, val diffPercent: DifferencePercent, + val isSingleProvider: Boolean, val rate: TextReference, ) : SwapQuoteUM() { sealed class DifferencePercent { From 48b8c77f5d19b5833278cfba63a61b9058c8e09c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 14:19:48 +0500 Subject: [PATCH 35/87] Updated on 2026-08-14 --- .../main/res/drawable/ic_passcode_lock_32.xml | 13 +++ .../main/res/drawable/ic_passcode_lock_56.xml | 20 +++++ .../port/entity/AddExistingWalletImportUM.kt | 2 - .../model/AddExistingWalletImportModel.kt | 33 +++++++ .../model/ImportSeedPhraseUiStateBuilder.kt | 18 +--- .../port/ui/AddExistingWalletImportContent.kt | 4 - .../im/port/ui/PassphraseInfoBottomSheet.kt | 90 ------------------- .../walletbackup/entity/WalletBackupUM.kt | 1 + .../walletbackup/model/WalletBackupModel.kt | 40 ++++++++- .../walletbackup/ui/WalletBackupContent.kt | 3 + 10 files changed, 111 insertions(+), 113 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_passcode_lock_32.xml create mode 100644 core/ui/src/main/res/drawable/ic_passcode_lock_56.xml delete mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml new file mode 100644 index 0000000000..b610863fbc --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_32.xml @@ -0,0 +1,13 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml new file mode 100644 index 0000000000..ecf6f9754c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_passcode_lock_56.xml @@ -0,0 +1,20 @@ + + + + diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt index 667d538a14..efd6c04702 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/entity/AddExistingWalletImportUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.entity import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -18,6 +17,5 @@ internal data class AddExistingWalletImportUM( val importWalletClick: () -> Unit, val suggestionsList: ImmutableList, val onSuggestionClick: (String) -> Unit, - val infoBottomSheetConfig: TangemBottomSheetConfig, val readyToImport: Boolean, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index f11d60b7bc..29196737e8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,8 +1,18 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase @@ -19,6 +29,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletImportModel @Inject constructor( paramsContainer: ParamsContainer, @@ -27,12 +38,29 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder + private val passphraseInfoAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_56) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.common_passphrase) + body = resourceReference(R.string.onboarding_bottom_sheet_passphrase_description) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + init { importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder( modelScope = modelScope, @@ -45,6 +73,7 @@ internal class AddExistingWalletImportModel @Inject constructor( passphrase = passphrase, ) }, + onPassphraseInfoClick = ::onPassphraseInfoClick, ) } @@ -73,4 +102,8 @@ internal class AddExistingWalletImportModel @Inject constructor( } } } + + private fun onPassphraseInfoClick() { + uiMessageSender.send(passphraseInfoAlertBS) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt index b09bdd3265..7f89025442 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/ImportSeedPhraseUiStateBuilder.kt @@ -4,7 +4,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult @@ -24,6 +23,7 @@ internal class ImportSeedPhraseUiStateBuilder( private val readyToImport: (Boolean) -> Unit, private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit, private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit, + private val onPassphraseInfoClick: () -> Unit, ) { private val wordsCheckJobHolder = JobHolder() private var importedMnemonic: Mnemonic? = null @@ -49,11 +49,10 @@ internal class ImportSeedPhraseUiStateBuilder( passphrase = it.text updateUiState { state -> state.copy(passPhrase = it) } }, - onPassphraseInfoClick = ::showInfoBS, + onPassphraseInfoClick = onPassphraseInfoClick, importWalletClick = ::onCreateWallet, onSuggestionClick = { word -> addSuggestedWord(word) }, readyToImport = false, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, ) } @@ -168,19 +167,6 @@ internal class ImportSeedPhraseUiStateBuilder( } } - private fun showInfoBS() { - updateUiState { state -> - state.copy( - infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty.copy( - isShown = true, - onDismissRequest = { - updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) } - }, - ), - ) - } - } - companion object { private const val MINIMUM_WORD_LENGTH = 2 private const val WORDS_INTERCEPT_DELAY_MS = 500L diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index 9d1d73601c..d7bb9338b0 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -37,7 +37,6 @@ import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.OutlineTextFieldWithIcon import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation @@ -113,8 +112,6 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) } } - - PassphraseInfoBottomSheet(state.infoBottomSheetConfig) } @Composable @@ -233,7 +230,6 @@ private fun PreviewAddExistingWalletImportContent() { importWalletClick = {}, suggestionsList = persistentListOf(), onSuggestionClick = {}, - infoBottomSheetConfig = TangemBottomSheetConfig.Empty, readyToImport = false, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt deleted file mode 100644 index 76521aab7a..0000000000 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/PassphraseInfoBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.hotwallet.addexistingwallet.im.port.ui - -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.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 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.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Composable -fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.primary, - ) { _: TangemBottomSheetConfigContent.Empty -> - PassphraseInfoBottomSheetContent(config.onDismissRequest) - } -} - -@Composable -fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - ) { - Icon( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = TangemTheme.dimens.size40) - .size(TangemTheme.dimens.size48), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - - Text( - text = stringResourceSafe(id = R.string.common_passphrase), - modifier = Modifier - .padding(top = TangemTheme.dimens.size40) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - - Text( - text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), - modifier = Modifier - .padding(top = TangemTheme.dimens.size16) - .padding(horizontal = TangemTheme.dimens.size24) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - ) - - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.size16) - .padding(top = TangemTheme.dimens.size40) - .padding(bottom = TangemTheme.dimens.size32) - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_ok), - onClick = onDismiss, - ) - } -} - -@Preview -@Composable -private fun PassphraseInfoBottomSheetContentPreview() { - TangemThemePreview { - PassphraseInfoBottomSheetContent({ }) - } -} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt index 5df45d62a0..f3f17cd9d3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/entity/WalletBackupUM.kt @@ -8,6 +8,7 @@ internal data class WalletBackupUM( val googleDriveStatus: LabelUM?, val onRecoveryPhraseClick: () -> Unit, val onGoogleDriveClick: () -> Unit, + val backedUp: Boolean, ) internal sealed class BackupStatus { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 18e601c406..37c2dd7948 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -1,13 +1,21 @@ package com.tangem.features.hotwallet.walletbackup.model +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.WalletBackupComponent @@ -22,6 +30,7 @@ internal class WalletBackupModel @Inject constructor( getWalletUseCase: GetUserWalletUseCase, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: WalletBackupComponent.Params = paramsContainer.require() @@ -38,11 +47,31 @@ internal class WalletBackupModel @Inject constructor( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), - onRecoveryPhraseClick = { }, + onRecoveryPhraseClick = ::onRecoveryPhraseClick, onGoogleDriveClick = { }, + backedUp = false, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + closeBs() + // TODO [REDACTED_TASK_KEY] + } + } + } + init { getWalletUseCase.invokeFlow(params.userWalletId) .map { it.getOrNull() } @@ -80,5 +109,14 @@ internal class WalletBackupModel @Inject constructor( text = resourceReference(R.string.common_coming_soon), style = LabelStyle.REGULAR, ), + backedUp = userWallet.backedUp, ) + + private fun onRecoveryPhraseClick() { + if (uiState.value.backedUp) { + // TODO [REDACTED_TASK_KEY] + } else { + uiMessageSender.send(makeBackupAtFirstAlertBS) + } + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt index 66e0c92d43..f5d25b7280 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/ui/WalletBackupContent.kt @@ -96,6 +96,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider Date: Mon, 18 Aug 2025 12:34:18 +0300 Subject: [PATCH 36/87] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../DefaultUserWalletsListRepository.kt | 4 +- .../tangem/tap/routing/utils/ChildFactory.kt | 31 ++- .../common/ui/userwallet/UserWalletItem.kt | 28 +++ .../converter/UserWalletItemUMConverter.kt | 41 ++-- .../ui/userwallet/state/UserWalletItemUM.kt | 2 + .../res/drawable/ic_mobile_wallet_icon_24.xml | 19 ++ .../walletmanager/WalletManagerFactory.kt | 3 +- .../data/wallets/hot/HotWalletAccessor.kt | 5 +- .../wallets/builder/HotUserWalletBuilder.kt | 2 +- .../usecase/GetIsBiometricsEnabledUseCase.kt | 4 + .../wallets/usecase/SaveWalletUseCase.kt | 6 +- .../details/model/UserWalletListModel.kt | 1 + .../hotwallet/accesscode/AccessCodeModel.kt | 46 +++- .../CreateMobileWalletModel.kt | 5 +- .../wallet/utils/UserWalletsFetcher.kt | 1 + .../wallet/utils/DefaultUserWalletsFetcher.kt | 12 +- .../connections/utils/WcUserWalletsFetcher.kt | 1 + features/welcome/impl/build.gradle.kts | 4 + .../welcome/impl/model/WelcomeModel.kt | 217 +++++++++++++++++- .../features/welcome/impl/ui/Welcome.kt | 41 ++-- .../welcome/impl/ui/WelcomeEnterAccessCode.kt | 94 -------- .../features/welcome/impl/ui/WelcomePlain.kt | 12 + .../welcome/impl/ui/WelcomeSelectWallet.kt | 79 +------ .../welcome/impl/ui/state/WalletUM.kt | 22 -- .../welcome/impl/ui/state/WelcomeUM.kt | 10 +- tangem-android-tools | 2 +- 27 files changed, 432 insertions(+), 262 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml delete mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt delete mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 23aae9e349..3ac868e93f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 23aae9e3496d89a021ac9a0833b54b49635bb193 +Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index c1cb92a998..6d907c384b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -77,7 +77,9 @@ internal class DefaultUserWalletsListRepository( override suspend fun userWalletsSync(): List { load() - return userWallets.value!! + return requireNotNull(userWallets.value) { + "This should never happen" + } } override suspend fun selectedUserWalletSync(): UserWallet? { 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 49f217bf18..71f7c77d8b 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 @@ -19,6 +19,7 @@ import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -52,6 +53,7 @@ import com.tangem.tap.routing.component.RoutingComponent.Child import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent +import com.tangem.features.welcome.WelcomeComponent as NewWelcomeComponent @ActivityScoped @Suppress("LongParameterList", "LargeClass") @@ -70,6 +72,7 @@ internal class ChildFactory @Inject constructor( private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory, private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, private val welcomeComponentFactory: WelcomeComponent.Factory, + private val newWelcomeComponentFactory: NewWelcomeComponent.Factory, private val storiesComponentFactory: StoriesComponent.Factory, private val stakingComponentFactory: StakingComponent.Factory, private val swapComponentFactory: SwapComponent.Factory, @@ -102,6 +105,7 @@ internal class ChildFactory @Inject constructor( private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -138,14 +142,25 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.Welcome -> { - createComponentChild( - context = context, - params = WelcomeComponent.Params( - launchMode = route.launchMode, - intent = route.intent, - ), - componentFactory = welcomeComponentFactory, - ) + if (hotWalletFeatureToggles.isHotWalletEnabled) { + createComponentChild( + context = context, + params = NewWelcomeComponent.Params( + launchMode = route.launchMode, + intent = route.intent, + ), + componentFactory = newWelcomeComponentFactory, + ) + } else { + createComponentChild( + context = context, + params = WelcomeComponent.Params( + launchMode = route.launchMode, + intent = route.intent, + ), + componentFactory = welcomeComponentFactory, + ) + } } is AppRoute.WalletSettings -> { createComponentChild( diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 97a5063ec8..31c66358b4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -3,7 +3,9 @@ package com.tangem.common.ui.userwallet import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CardColors import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -22,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.common.ui.R @@ -184,6 +187,19 @@ fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modi radius = TangemTheme.dimens.size2, ) } + is UserWalletItemUM.ImageState.MobileWallet -> { + Image( + modifier = Modifier + .size(36.dp) + .background( + color = TangemTheme.colors.field.focused, + shape = RoundedCornerShape(10.dp), + ) + .padding(6.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_mobile_wallet_icon_24), + contentDescription = null, + ) + } is UserWalletItemUM.ImageState.Image -> { val verifiedArtwork = imageState.artwork.verifiedArtwork if (verifiedArtwork != null) { @@ -364,6 +380,18 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider { @@ -48,24 +49,38 @@ class UserWalletItemUMConverter( name = stringReference(name), information = getInfo(userWallet = this), balance = getBalanceInfo(userWallet = this), - isEnabled = !isLocked, + isEnabled = isEnabled(userWallet = this), endIcon = endIcon, onClick = { onClick(value.walletId) }, - imageState = artwork?.let { - UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it)) - } ?: UserWalletItemUM.ImageState.Loading, - label = if (this is UserWallet.Hot && !this.backedUp) { - LabelUM( - text = resourceReference(R.string.hw_backup_no_backup), - style = LabelStyle.WARNING, - ) - } else { - null - }, + imageState = getImageState(userWallet = value), + label = getLabelOrNull(userWallet = this), ) } } + private fun isEnabled(userWallet: UserWallet): Boolean { + return authMode || userWallet.isLocked.not() + } + + private fun getLabelOrNull(userWallet: UserWallet): LabelUM? { + return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) { + LabelUM( + text = resourceReference(R.string.hw_backup_no_backup), + style = LabelStyle.WARNING, + ) + } else { + null + } + } + + private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState { + return when { + userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet + artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork)) + else -> UserWalletItemUM.ImageState.Loading + } + } + private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { val text = when (userWallet) { is UserWallet.Cold -> { diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt index e9f98863b8..c0abb2b485 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/state/UserWalletItemUM.kt @@ -56,6 +56,8 @@ data class UserWalletItemUM( data object Loading : ImageState() + data object MobileWallet : ImageState() + data class Image( val artwork: ArtworkUM, ) : ImageState() diff --git a/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml new file mode 100644 index 0000000000..101239eb7d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_mobile_wallet_icon_24.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index eff1e685f6..2ab20f0a42 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp +import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet @@ -41,7 +42,7 @@ internal class WalletManagerFactory( blockchain: Blockchain, derivationPath: DerivationPath?, ): WalletManager? { - val curve = blockchain.getSupportedCurves().first() + val curve = Wallet2CardConfig.primaryCurve(blockchain) val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } ?: return null return try { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index c7569b5a3b..9c26e8a3ee 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -46,9 +46,8 @@ class HotWalletAccessor @Inject constructor( auth = auth, block = { blockAuth -> block(blockAuth).also { - // TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code - // if user has biometry enabled, we set it as the new auth method - if (blockAuth is HotAuth.Password /*&& has biometry enabled */) { + // Update biometry auth if the original auth was password + if (blockAuth is HotAuth.Password) { tangemHotSdk.changeAuth( unlockHotWallet = UnlockHotWallet( walletId = hotWalletId, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index 4707a09f81..eaa1c7dc7c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet + val allNetworks = Blockchain.entries.filter { it.isTestnet().not() } val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt index b57e8d3ec8..2c3f0deaba 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt @@ -8,4 +8,8 @@ class GetIsBiometricsEnabledUseCase @Inject constructor( ) { operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false + + fun canUseBiometry(): Boolean { + return tangemSdkManager.canUseBiometry + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index b34885eb98..23b10bf3c9 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -46,7 +46,11 @@ class SaveWalletUseCase( UserWalletsListRepository.LockMethod.NoLock, ) } - }.mapLeft { SaveWalletError.DataError(null) }.bind() + }.mapLeft { + SaveWalletError.DataError(null) + }.map { + userWalletsListRepository.select(userWallet.walletId) + }.bind() } } } else { diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 6af5b54fe3..d1d008e54f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -36,6 +36,7 @@ internal class UserWalletListModel @Inject constructor( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = false, + authMode = false, onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) }, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index a7e9eec930..373af1cf05 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -5,10 +5,10 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth @@ -27,7 +27,7 @@ internal class AccessCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, - private val saveWalletUseCase: SaveWalletUseCase, + private val userWalletsListRepository: UserWalletsListRepository, private val tangemHotSdk: TangemHotSdk, ) : Model() { @@ -77,15 +77,41 @@ internal class AccessCodeModel @Inject constructor( runCatching { val userWallet = getUserWalletUseCase(userWalletId) .getOrElse { error("User wallet with id $userWalletId not found") } - if (userWallet is UserWallet.Hot) { - val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) - val updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = unlockHotWallet, + if (userWallet !is UserWallet.Hot) return@launch + + val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth) + var updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = unlockHotWallet, + auth = HotAuth.Password(accessCode.toCharArray()), + ) + + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = updatedHotWalletId, auth = HotAuth.Password(accessCode.toCharArray()), - ) - saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId), canOverride = true) - params.callbacks.onAccessCodeConfirmed(params.userWalletId) - } + ), + auth = HotAuth.Biometry, + ) + + userWalletsListRepository.saveWithoutLock( + userWallet.copy( + hotWalletId = updatedHotWalletId, + backedUp = true, + ), + canOverride = true, + ) + + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), + ) + + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + + params.callbacks.onAccessCodeConfirmed(params.userWalletId) }.onFailure { Timber.e(it) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index d59cf2e39f..0b3261b4f3 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -45,9 +45,8 @@ internal class CreateMobileWalletModel @Inject constructor( runCatching { val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) - saveUserWalletUseCase( - hotUserWalletBuilder.build(), - ) + val userWallet = hotUserWalletBuilder.build() + saveUserWalletUseCase(userWallet) router.replaceAll(AppRoute.Wallet) }.onFailure { Timber.e(it) diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt index 3f30b7eebe..674541796f 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/utils/UserWalletsFetcher.kt @@ -14,6 +14,7 @@ interface UserWalletsFetcher { fun create( messageSender: UiMessageSender, onlyMultiCurrency: Boolean, + authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): UserWalletsFetcher } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index ce71cf24a6..7a3b9accba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -43,7 +43,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @Assisted private val onWalletClick: (UserWalletId) -> Unit, @Assisted private val messageSender: UiMessageSender, - @Assisted private val onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean, + @Assisted("authMode") private val authMode: Boolean, private val getCardImageUseCase: GetCardImageUseCase, dispatchers: CoroutineDispatcherProvider, ) : UserWalletsFetcher { @@ -54,7 +55,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override val userWallets: Flow> = walletsFlow.transformLatest { wallets -> - val uiModels = UserWalletItemUMConverter(onClick = onWalletClick).convertList(wallets) + val uiModels = UserWalletItemUMConverter( + onClick = onWalletClick, + authMode = authMode, + ).convertList(wallets) .toImmutableList() emit(uiModels) @@ -132,6 +136,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( balance = balance, isBalanceHidden = balanceHidingSettings.isBalanceHidden, artwork = artworks[userWallet.walletId], + authMode = authMode, ) .convert(userWallet) } @@ -149,7 +154,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( interface Factory : UserWalletsFetcher.Factory { override fun create( messageSender: UiMessageSender, - onlyMultiCurrency: Boolean, + @Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean, + @Assisted("authMode") authMode: Boolean, onWalletClick: (UserWalletId) -> Unit, ): DefaultUserWalletsFetcher } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index bcfd5129b1..727e85c485 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -29,6 +29,7 @@ internal class WcUserWalletsFetcher( private val userWalletsFetcher = userWalletsFetcherFactory.create( messageSender = messageSender, onlyMultiCurrency = true, + authMode = false, onWalletClick = { onWalletSelected(it) }, ) diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 499c0ab82e..8a2a3c3c3f 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -13,11 +13,13 @@ android { dependencies { implementation(projects.features.welcome.api) + implementation(projects.features.wallet.api) /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.analytics) implementation(projects.common.routing) implementation(projects.common.ui) @@ -30,6 +32,7 @@ dependencies { /** Domain */ implementation(projects.domain.appCurrency) implementation(projects.domain.wallets) + implementation(projects.domain.card) /** DI */ implementation(deps.hilt.android) @@ -54,4 +57,5 @@ dependencies { implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index da1f8c9c21..b81a8806c5 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -1,18 +1,233 @@ package com.tangem.features.welcome.impl.model +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.core.wallets.error.UnlockWalletError +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase +import com.tangem.features.wallet.utils.UserWalletsFetcher +import com.tangem.features.welcome.impl.R +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM +import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.* import com.tangem.features.welcome.impl.ui.state.WelcomeUM +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val uiMessageSender: UiMessageSender, + private val userWalletsFetcherFactory: UserWalletsFetcher.Factory, + private val userWalletsListRepository: UserWalletsListRepository, + private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase, ) : Model() { + // TODO add intent handling + // val params val uiState: StateFlow - field = MutableStateFlow(WelcomeUM.Plain) + field = MutableStateFlow(WelcomeUM.Plain) + + private val walletsFetcher = userWalletsFetcherFactory.create( + messageSender = uiMessageSender, + onlyMultiCurrency = false, + authMode = true, + onWalletClick = { walletId -> + modelScope.launch { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first { it.walletId == walletId } + onUserWalletClick(userWallet) + } + }, + ) + private val walletsFetcherJobHolder = JobHolder() + private val wallets = MutableStateFlow>(persistentListOf()) + + init { + modelScope.launch { + userWalletsListRepository.load() + wallets.value = walletsFetcher.userWallets.first() + + launch { + walletsFetcher.userWallets + .collectLatest { wallets.value = it } + } + + tryToUnlockRightAway() + } + } + + private fun tryToUnlockRightAway() { + modelScope.launch { + if (canUnlockWithBiometrics()) { + userWalletsListRepository.unlockAllWallets() + .onRight { + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() }) + setSelectWalletState() + } + } else { + tryToUnlockWithAccessCodeRightAway() + setSelectWalletState() + } + } + } + + private fun tryToUnlockWithAccessCodeRightAway() = modelScope.launch { + if (onlyOneHotWalletWithAccessCode()) { + val userWallets = userWalletsListRepository.userWalletsSync() + val userWallet = userWallets.first() + unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode) + } + } + + private fun setSelectWalletState() { + modelScope.launch { + uiState.value = WelcomeUM.SelectWallet( + wallets = walletsFetcher.userWallets.first(), + showUnlockWithBiometricButton = canUnlockWithBiometrics(), + addWalletClick = ::addWalletClick, + onUnlockWithBiometricClick = { + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onRight { + router.replaceAll(AppRoute.Wallet) + } + .onLeft { + it.handle(null, onUserCancelled = { /* ignore */ }) + } + } + }, + ) + + wallets.collectLatest { wallets -> + updateSelectState { + it.copy(wallets = wallets) + } + } + }.saveIn(walletsFetcherJobHolder) + } + + private fun addWalletClick() { + updateSelectState { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + content = AddWalletBottomSheetContentUM( + onOptionClick = ::onAddWalletOptionClick, + ), + onDismissRequest = { + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + }, + ), + ) + } + } + + private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) { + when (option) { + Create -> router.push(AppRoute.CreateWalletSelection) + Add -> router.push(AppRoute.AddExistingWallet) + Buy -> { + } + } + } + + private suspend fun onlyOneHotWalletWithAccessCode(): Boolean { + val userWalletsWithLock = userWalletsListRepository.userWalletsSync().filter { it.isLocked } + if (userWalletsWithLock.size != 1) return false + val wallet = userWalletsWithLock.first() + return wallet is UserWallet.Hot && wallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword + } + + private fun onUserWalletClick(userWallet: UserWallet) = modelScope.launch { + if (userWallet.isLocked.not()) { + // If the wallet is not locked, we can proceed to the wallet screen directly + userWalletsListRepository.select(userWallet.walletId) + router.replaceAll(AppRoute.Wallet) + return@launch + } + + val unlockMethod = when (userWallet) { + is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan + is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode + } + + unlockWallet(userWallet.walletId, unlockMethod) + } + + private fun canUnlockWithBiometrics(): Boolean { + return getIsBiometricsEnabledUseCase.canUseBiometry() + } + + suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { + userWalletsListRepository.unlock(userWalletId, unlockMethod) + .onRight { + userWalletsListRepository.select(userWalletId) + router.replaceAll(AppRoute.Wallet) + } + .onLeft { error -> + error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ }) + } + } + + suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: () -> Unit = { }) { + when (this) { + UnlockWalletError.AlreadyUnlocked -> { + // this should not happen, as we check for locked state before this + specificWalletId?.let { userWalletsListRepository.select(it) } + router.replaceAll(AppRoute.Wallet) + } + UnlockWalletError.ScannedCardWalletNotMatched -> { + // TODO Scanned card does not match the wallet + } + UnlockWalletError.UnableToUnlock -> { + // TODO Unable to unlock the wallet" + } + UnlockWalletError.UserCancelled -> onUserCancelled() + UnlockWalletError.UserWalletNotFound -> { + // This should never happen in this flow, as we always check for the wallet existence before unlocking + Timber.e("User wallet not found for unlock: $specificWalletId") + uiMessageSender.send( + SnackbarMessage(TextReference.Res(R.string.generic_error)), + ) + } + } + } + + private fun updateSelectState(block: (WelcomeUM.SelectWallet) -> WelcomeUM.SelectWallet) { + uiState.update { currentState -> + if (currentState is WelcomeUM.SelectWallet) { + block(currentState) + } else { + currentState + } + } + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt index caf6ecd9bd..3427544768 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -10,10 +10,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.extensions.TextReference +import com.tangem.common.ui.userwallet.state.UserWalletItemUM +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.welcome.impl.ui.state.WalletUM +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.persistentListOf @@ -34,10 +35,6 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { state = st, modifier = modifier, ) - is WelcomeUM.EnterAccessCode -> WelcomeEnterAccessCode( - state = st, - modifier = modifier, - ) } } } @@ -49,22 +46,30 @@ private fun Preview() { TangemThemePreview { val state = WelcomeUM.SelectWallet( wallets = persistentListOf( - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("3 cards"), - imageState = WalletUM.ImageState.Loading, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Loading, + balance = UserWalletItemUM.Balance.Loaded( + value = "1.2345 BTC", + isFlickering = false, + ), + isEnabled = true, onClick = {}, ), - WalletUM( - name = TextReference.Str("Wallet 1"), - subtitle = TextReference.Str("Mobile wallet"), - imageState = WalletUM.ImageState.MobileWallet, + UserWalletItemUM( + id = UserWalletId("user_wallet_3".encodeToByteArray()), + name = stringReference("Multi Card"), + information = UserWalletItemUM.Information.Failed, + imageState = UserWalletItemUM.ImageState.MobileWallet, + balance = UserWalletItemUM.Balance.Locked, + isEnabled = true, onClick = {}, ), ), ) - var currentState by remember { mutableStateOf(WelcomeUM.EnterAccessCode()) } + var currentState by remember { mutableStateOf(WelcomeUM.SelectWallet()) } Box { Welcome(currentState) @@ -74,11 +79,7 @@ private fun Preview() { onClick = { currentState = when (currentState) { is WelcomeUM.Plain -> state - is WelcomeUM.SelectWallet -> WelcomeUM.EnterAccessCode( - value = "", - onValueChange = {}, - ) - is WelcomeUM.EnterAccessCode -> WelcomeUM.Plain + is WelcomeUM.SelectWallet -> WelcomeUM.Plain } }, ) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt deleted file mode 100644 index 3cd6085b58..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeEnterAccessCode.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.tangem.features.welcome.impl.ui - -import androidx.compose.animation.AnimatedContentScope -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -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.unit.dp -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.appbar.TopAppBarButton -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.welcome.impl.ui.state.WelcomeUM - -@Suppress("MagicNumber") -@Composable -internal fun AnimatedContentScope.WelcomeEnterAccessCode( - state: WelcomeUM.EnterAccessCode, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxSize() - .statusBarsPadding(), - ) { - Column { - TopAppBarButton( - modifier = Modifier - .padding(12.dp), - button = TopAppBarButtonUM.Back(onBackClicked = state.onBackClick), - tint = TangemTheme.colors.icon.primary1, - ) - - SpacerH(68.dp) - - Text( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .align(Alignment.CenterHorizontally), - text = "Enter Access Code", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - - SpacerH24() - - Box( - modifier = Modifier - .animateEnterExit( - enter = slideInVertically( - tween(delayMillis = 300), - initialOffsetY = { it + 200 }, - ) + fadeIn(tween(delayMillis = 300)), - exit = fadeOut(), - ) - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - PinTextField( - length = 6, - isPasswordVisual = true, - value = state.value, - onValueChange = state.onValueChange, - ) - } - } - - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .imePadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Log in with biometric", - onClick = state.onUnlockWithBiometricClick, - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt index 13aaae14db..5b80e192ee 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomePlain.kt @@ -9,8 +9,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.welcome.impl.R @Composable @@ -26,4 +28,14 @@ internal fun WelcomePlain(modifier: Modifier = Modifier) { contentDescription = null, ) } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + WelcomePlain( + modifier = Modifier.fillMaxSize(), + ) + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index ed41fe5ad4..b24c0a7b55 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -5,12 +5,9 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -20,14 +17,13 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp -import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.WalletUM import com.tangem.features.welcome.impl.ui.state.WelcomeUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -45,7 +41,7 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal TitleText() SpacerH12() - var actualWallets by remember { mutableStateOf>(persistentListOf()) } + var actualWallets by remember { mutableStateOf>(persistentListOf()) } Box(modifier = Modifier.weight(1f)) { LazyColumn( @@ -62,9 +58,12 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal verticalArrangement = Arrangement.spacedBy(8.dp), ) { itemsIndexed(actualWallets) { index, walletState -> - WalletItem( + UserWalletItem( + modifier = Modifier.fillMaxWidth(), state = walletState, - modifier = Modifier, + blockColors = TangemBlockCardColors.copy( + containerColor = TangemTheme.colors.field.primary, + ), ) } } @@ -165,66 +164,4 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { color = TangemTheme.colors.text.secondary, ) } -} - -@Suppress("MagicNumber") -@Composable -private fun WalletItem(state: WalletUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.secondary, TangemTheme.shapes.roundedCornersXMedium) - .clickable(onClick = state.onClick) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - WalletImage(state.imageState) - - SpacerW12() - - Column(Modifier.weight(1f)) { - Text( - text = state.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - - Text( - text = state.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun WalletImage(state: WalletUM.ImageState, modifier: Modifier = Modifier) { - when (state) { - WalletUM.ImageState.MobileWallet -> { - Box( - modifier = modifier - .size(36.dp) - .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_wallet_filled_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - } - } - else -> { - CardImage( - imageState = when (state) { - is WalletUM.ImageState.Image -> UserWalletItemUM.ImageState.Image(state.artwork) - WalletUM.ImageState.Loading -> UserWalletItemUM.ImageState.Loading - else -> error("") - }, - modifier = modifier, - ) - } - } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt deleted file mode 100644 index 216a28f7d2..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WalletUM.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.welcome.impl.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.TextReference -import javax.annotation.concurrent.Immutable - -internal data class WalletUM( - val name: TextReference, - val subtitle: TextReference, - val imageState: ImageState, - val onClick: () -> Unit, -) { - - @Immutable - sealed class ImageState { - data object MobileWallet : ImageState() - data object Loading : ImageState() - data class Image( - val artwork: ArtworkUM, - ) : ImageState() - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt index 1e2e509d58..390446ba73 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.welcome.impl.ui.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -11,17 +12,10 @@ internal sealed class WelcomeUM { data object Plain : WelcomeUM() data class SelectWallet( - val wallets: ImmutableList = persistentListOf(), + val wallets: ImmutableList = persistentListOf(), val showUnlockWithBiometricButton: Boolean = false, val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, val onUnlockWithBiometricClick: () -> Unit = {}, val addWalletClick: () -> Unit = {}, ) : WelcomeUM() - - data class EnterAccessCode( - val value: String = "", - val onUnlockWithBiometricClick: () -> Unit = {}, - val onValueChange: (String) -> Unit = {}, - val onBackClick: () -> Unit = {}, - ) : WelcomeUM() } \ No newline at end of file diff --git a/tangem-android-tools b/tangem-android-tools index 794a8187e6..bc4cd43085 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a +Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 From c729a406df7fff7043d5c4ad594dcb7082c6ade4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 12:52:02 +0300 Subject: [PATCH 37/87] Updated on 2026-08-14 --- .../tangem/plugin/configuration/model/BuildConfigField.kt | 7 ------- .../com/tangem/plugin/configuration/model/BuildType.kt | 5 ----- 2 files changed, 12 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt index 17f61ecdd2..058a4ccb06 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildConfigField.kt @@ -15,13 +15,6 @@ internal sealed class BuildConfigField(val type: String, val name: String, val v value = "\"$value\"", ) - // TODO remove - class TestActionEnabled(isEnabled: Boolean) : BuildConfigField( - type = "Boolean", - name = "TEST_ACTION_ENABLED", - value = isEnabled.toString(), - ) - class LogEnabled(isEnabled: Boolean) : BuildConfigField( type = "Boolean", name = "LOG_ENABLED", diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index bac38b3402..d7f00aa00a 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -25,7 +25,6 @@ internal enum class BuildType( appIdSuffix = "debug", configFields = listOf( BuildConfigField.Environment(value = "dev"), - BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = false), @@ -47,7 +46,6 @@ internal enum class BuildType( versionSuffix = "mocked", configFields = listOf( BuildConfigField.Environment(value = "dev"), - BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = false), BuildConfigField.MockDataSource(isEnabled = true), @@ -71,7 +69,6 @@ internal enum class BuildType( versionSuffix = "internal", configFields = listOf( BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = true), BuildConfigField.LogEnabled(isEnabled = true), BuildConfigField.TesterMenuAvailability(isEnabled = true), BuildConfigField.MockDataSource(isEnabled = false), @@ -92,7 +89,6 @@ internal enum class BuildType( versionSuffix = "external", configFields = listOf( BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), BuildConfigField.MockDataSource(isEnabled = false), @@ -111,7 +107,6 @@ internal enum class BuildType( id = "release", configFields = listOf( BuildConfigField.Environment(value = "prod"), - BuildConfigField.TestActionEnabled(isEnabled = false), BuildConfigField.LogEnabled(isEnabled = false), BuildConfigField.TesterMenuAvailability(isEnabled = false), BuildConfigField.MockDataSource(isEnabled = false), From 3ac26a45b79c9c8785197bcb344292e5a9a737b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 13:37:51 +0300 Subject: [PATCH 38/87] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt | 3 +++ gradle/tangem_dependencies.toml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt index 9cc8db4191..af00ff8af8 100644 --- a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt +++ b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt @@ -37,6 +37,9 @@ class TangemHotSDKProxy @Inject constructor() : TangemHotSdk { override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId = callSdk { changeAuth(unlockHotWallet, auth) } + override suspend fun removeBiometryAuthIfPresented(id: HotWalletId): HotWalletId = + callSdk { removeBiometryAuthIfPresented(id) } + override suspend fun derivePublicKey( unlockHotWallet: UnlockHotWallet, request: DeriveWalletRequest, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b1088cbc11..14441c2e44 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-511" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-461" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 294df69e93cf6607e44868c0c7086d4eb63a7bdd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 16:24:15 +0500 Subject: [PATCH 39/87] Updated on 2026-08-14 --- .../CardContextInterceptor.kt | 4 +- .../tap/di/domain/WalletsDomainModule.kt | 6 + .../welcome/redux/WelcomeMiddleware.kt | 2 +- .../domain/card}/analytics/AnalyticsParam.kt | 2 +- .../card}/analytics/IntroductionProcess.kt | 2 +- .../analytics}/ParamCardCurrencyConverter.kt | 16 +- .../com/tangem/domain/card}/analytics/Shop.kt | 4 +- .../wallets/error/SaveFirstColdWalletError.kt | 7 + domain/wallets/build.gradle.kts | 5 + .../GenerateBuyTangemCardLinkUseCase.kt | 23 +++ .../wallets/usecase/SelectWalletUseCase.kt | 2 +- .../impl/build.gradle.kts | 6 + .../CreateWalletSelectionModel.kt | 148 +++++++++++++++- .../ui/CreateWalletSelectionContent.kt | 18 +- features/home/impl/build.gradle.kts | 3 - .../analytics/ParamCardCurrencyConverter.kt | 23 --- .../features/home/impl/model/HomeModel.kt | 67 +++----- features/hot-wallet/impl/build.gradle.kts | 1 + .../start/AddExistingWalletStartModel.kt | 158 +++++++++++++++++- .../start/entity/AddExistingWalletStartUM.kt | 1 + .../start/ui/AddExistingWalletStartContent.kt | 29 +++- 21 files changed, 427 insertions(+), 100 deletions(-) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/AnalyticsParam.kt (86%) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/IntroductionProcess.kt (91%) rename {app/src/main/java/com/tangem/tap/common/analytics/converters => domain/card/src/main/kotlin/com/tangem/domain/card/analytics}/ParamCardCurrencyConverter.kt (58%) rename {features/home/impl/src/main/kotlin/com/tangem/features/home/impl => domain/card/src/main/kotlin/com/tangem/domain/card}/analytics/Shop.kt (74%) create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt delete mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 8f14501047..805090e6c0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -2,12 +2,12 @@ package com.tangem.tap.common.analytics.paramsInterceptor import com.tangem.core.analytics.api.ParamsInterceptor import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.features.home.impl.analytics.IntroductionProcess -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.extensions.inject import com.tangem.tap.features.demo.DemoHelper diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index eff26615ae..5b21957999 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -131,6 +131,12 @@ internal object WalletsDomainModule { ) } + @Provides + @Singleton + fun providesOpenBuyTangemCardUseCase(): GenerateBuyTangemCardLinkUseCase { + return GenerateBuyTangemCardLinkUseCase() + } + @Provides @Singleton fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { 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 37690ee484..8a842072d8 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 @@ -11,13 +11,13 @@ 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.domain.card.analytics.ParamCardCurrencyConverter import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.* -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt similarity index 86% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt index def2ff2645..40ce107753 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/AnalyticsParam.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/AnalyticsParam.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics internal sealed class AnalyticsParam { diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt similarity index 91% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 6115389bfd..0cb4a3685c 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -1,4 +1,4 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt similarity index 58% rename from app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt index c5c7e0ea3c..2398da3650 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ParamCardCurrencyConverter.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/ParamCardCurrencyConverter.kt @@ -1,18 +1,14 @@ -package com.tangem.tap.common.analytics.converters +package com.tangem.domain.card.analytics import com.tangem.blockchain.common.Blockchain +import com.tangem.core.analytics.models.AnalyticsParam.WalletType import com.tangem.domain.card.CardTypesResolver -import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam -/** -[REDACTED_AUTHOR] - */ -class ParamCardCurrencyConverter : Converter { +class ParamCardCurrencyConverter : Converter { - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency + override fun convert(value: CardTypesResolver): WalletType? { + if (value.isMultiwalletAllowed()) return WalletType.MultiCurrency val type = when { value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) @@ -22,6 +18,6 @@ class ParamCardCurrencyConverter : Converter null } ?: return null - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) + return WalletType.SingleCurrency(type.value) } } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt similarity index 74% rename from features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt rename to domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt index 632cc66ef2..62c02a56ab 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/Shop.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/Shop.kt @@ -1,8 +1,8 @@ -package com.tangem.features.home.impl.analytics +package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent -internal sealed class Shop( +sealed class Shop( event: String, params: Map = mapOf(), ) : AnalyticsEvent("Shop", event, params) { diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt new file mode 100644 index 0000000000..24c29fd411 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/error/SaveFirstColdWalletError.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.core.wallets.error + +sealed interface SaveFirstColdWalletError { + data object CreateWalletError : SaveFirstColdWalletError + data class SaveError(val error: SaveWalletError) : SaveFirstColdWalletError + data class SelectError(val error: SelectWalletError) : SaveFirstColdWalletError +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 8fbf7b49dd..a467663bf5 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -37,6 +37,11 @@ dependencies { implementation(tangemDeps.hot.core) // endregion + /** Other libraries */ + implementation(platform(deps.firebase.bom)) + implementation(deps.firebase.analytics) + implementation(deps.timber) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt new file mode 100644 index 0000000000..7c80707cb6 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.wallets.usecase + +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class GenerateBuyTangemCardLinkUseCase { + + suspend operator fun invoke(): String = suspendCoroutine { cont -> + Firebase.analytics.appInstanceId + .addOnSuccessListener { id -> + cont.resume("$NEW_BUY_WALLET_URL&app_instance_id=$id") + } + .addOnFailureListener { + cont.resume(NEW_BUY_WALLET_URL) + } + } + + companion object { + private const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index e0654f947c..21a6a76755 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -4,9 +4,9 @@ import arrow.core.Either import arrow.core.raise.either import arrow.core.right import com.tangem.common.CompletionResult +import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.core.wallets.error.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.core.wallets.UserWalletsListRepository diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 477a6af3ea..da58579502 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -18,6 +18,12 @@ dependencies { /** Hot Wallet Feature */ implementation(projects.features.hotWallet.api) + /** Project - Domain */ + implementation(projects.domain.card) + implementation(projects.domain.settings) + implementation(projects.domain.wallets) + implementation(projects.domain.models) + /** Core modules */ implementation(projects.core.configToggles) implementation(projects.core.analytics) diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt index 242046b568..23d94a6c78 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/CreateWalletSelectionModel.kt @@ -1,19 +1,63 @@ package com.tangem.features.createwalletselection +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class CreateWalletSelectionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { internal val uiState: StateFlow @@ -31,10 +75,110 @@ internal class CreateWalletSelectionModel @Inject constructor( } private fun onHardwareWalletClick() { - // TODO open card order web page + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onScanClick() { - // TODO open card scanning + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) } } \ No newline at end of file diff --git a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt index 0c777f9a97..4106c3d8c6 100644 --- a/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt +++ b/features/create-wallet-selection/impl/src/main/kotlin/com/tangem/features/createwalletselection/ui/CreateWalletSelectionContent.kt @@ -5,10 +5,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -18,6 +20,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonSize import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -179,6 +182,9 @@ private fun AlreadyHaveTangemWalletBlock( isScanInProgress: Boolean, modifier: Modifier = Modifier, ) { + var buttonWidth by remember { mutableStateOf(0) } + val density = LocalDensity.current + Row( modifier = modifier .fillMaxWidth() @@ -201,9 +207,17 @@ private fun AlreadyHaveTangemWalletBlock( style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, ) + TangemButton( modifier = Modifier - .wrapContentWidth(), + .conditional(buttonWidth > 0) { + width(with(density) { buttonWidth.toDp() }) + } + .onGloballyPositioned { coordinates -> + if (buttonWidth == 0) { + buttonWidth = coordinates.size.width + } + }, text = stringResourceSafe(R.string.wallet_create_scan_title), onClick = onScanClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index 4a4f17d3bf..d06bedde20 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -51,9 +51,6 @@ dependencies { implementation(deps.compose.coil) implementation(deps.decompose.ext.compose) - /** Firebase */ - implementation(deps.firebase.analytics) - /** Tangem libraries */ implementation(tangemDeps.card.android) implementation(tangemDeps.card.core) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt deleted file mode 100644 index 16c104323b..0000000000 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/analytics/ParamCardCurrencyConverter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.features.home.impl.analytics - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.card.CardTypesResolver -import com.tangem.utils.converter.Converter -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam - -internal class ParamCardCurrencyConverter : Converter { - - override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? { - if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency - - val type = when { - value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) - value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin) - value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain()) - value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!) - else -> null - } ?: return null - - return CoreAnalyticsParam.WalletType.SingleCurrency(type.value) - } -} \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 61614e3a62..70636eda60 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -1,7 +1,5 @@ package com.tangem.features.home.impl.model -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.ktx.Firebase import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute @@ -23,20 +21,22 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.home.api.HomeComponent -import com.tangem.features.home.impl.analytics.IntroductionProcess -import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter -import com.tangem.features.home.impl.analytics.Shop import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.getRestrictedStories @@ -64,16 +64,17 @@ internal class HomeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, - private val saveWalletUseCase: SaveWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, - private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val router: Router, - private val selectWalletUseCase: SelectWalletUseCase, private val appRouter: AppRouter, private val getUserCountryUseCase: GetUserCountryUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { @@ -135,10 +136,9 @@ internal class HomeModel @Inject constructor( private fun onShopClick() { analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) analyticsEventHandler.send(Shop.ScreenOpened) - - Firebase.analytics.appInstanceId - .addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") } - .addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) } + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } private fun onSearchTokensClick() { @@ -198,24 +198,17 @@ internal class HomeModel @Inject constructor( saveWalletUseCase(userWallet).fold( ifLeft = { - Timber.e(it.toString(), "Unable to save user wallet") + delay(HIDE_PROGRESS_DELAY) setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } }, ifRight = { + setLoading(false) sendSignedInCardAnalyticsEvent(scanResponse) - - // Select the wallet using new mechanism - selectWalletUseCase(userWallet.walletId).fold( - ifLeft = { - Timber.e("Unable to select user wallet: $it") - setLoading(false) - }, - ifRight = { - delay(HIDE_PROGRESS_DELAY) - setLoading(false) - appRouter.replaceAll(AppRoute.Wallet) - }, - ) + appRouter.replaceAll(AppRoute.Wallet) }, ) } @@ -228,7 +221,7 @@ internal class HomeModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = "1", + walletsCount = userWalletsListManager.walletsCount.toString(), hasBackup = scanResponse.card.backupStatus?.isActive, ), ) @@ -241,15 +234,9 @@ internal class HomeModel @Inject constructor( fun handleScanError(error: TangemError) { when (error) { - is TangemSdkError.NfcFeatureIsUnavailable -> { - handleNfcFeatureUnavailable() - } - is TangemSdkError -> { - Timber.e(error, "Scan error occurred") - } - else -> { - Timber.e(error, "Error happened") - } + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") } } @@ -261,8 +248,4 @@ internal class HomeModel @Inject constructor( ), ) } - - companion object { - const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app" - } } \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index ccc57778cd..2f25fc3478 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.datasource) /** Domain */ + implementation(projects.domain.card) implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt index 58ea450355..dab15c9946 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/AddExistingWalletStartModel.kt @@ -1,18 +1,63 @@ package com.tangem.features.hotwallet.addexistingwallet.start +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic.SignedIn +import com.tangem.core.analytics.models.Basic.SignedIn.SignInType +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.ParamCardCurrencyConverter +import com.tangem.domain.card.analytics.Shop +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.error.SaveWalletError +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject +private const val HIDE_PROGRESS_DELAY = 400L + +@Suppress("LongParameterList") @ModelScoped internal class AddExistingWalletStartModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val saveWalletUseCase: SaveWalletUseCase, + private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val settingsRepository: SettingsRepository, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val userWalletsListManager: UserWalletsListManager, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { private val params: AddExistingWalletStartComponent.Params = paramsContainer.require() @@ -20,10 +65,119 @@ internal class AddExistingWalletStartModel @Inject constructor( internal val uiState: StateFlow field = MutableStateFlow( AddExistingWalletStartUM( + isScanInProgress = false, onBackClick = params.callbacks::onBackClick, onImportPhraseClick = params.callbacks::onImportPhraseClick, - onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ }, - onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ }, + onScanCardClick = ::onScanClick, + onBuyCardClick = ::onShopClick, ), ) + + private fun onShopClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards) + analyticsEventHandler.send(Shop.ScreenOpened) + modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + + private fun onScanClick() { + analyticsEventHandler.send(IntroductionProcess.ButtonScanCard) + scanCard() + } + + private fun scanCard() { + modelScope.launch { + setLoading(true) + + val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes() + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = shouldSaveAccessCodes, + ) + + val analyticsSource = AnalyticsParam.ScreensSources.Intro + + scanCardProcessor.scan( + analyticsSource = analyticsSource, + onProgressStateChange = { showProgress -> + if (!showProgress) { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + } else { + setLoading(true) + } + }, + onFailure = { error -> + handleScanError(error) + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + }, + onSuccess = { scanResponse -> + proceedWithScanResponse(scanResponse) + }, + ) + } + } + + private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { + val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() + + if (userWallet == null) { + Timber.e("User wallet not created") + setLoading(false) + return + } + + saveWalletUseCase(userWallet).fold( + ifLeft = { + delay(HIDE_PROGRESS_DELAY) + setLoading(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) + } + }, + ifRight = { + setLoading(false) + sendSignedInCardAnalyticsEvent(scanResponse) + appRouter.replaceAll(AppRoute.Wallet) + }, + ) + } + + private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { + val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver) + if (currency != null) { + analyticsEventHandler.send( + SignedIn( + currency = currency, + batch = scanResponse.card.batchId, + signInType = SignInType.Card, + walletsCount = userWalletsListManager.walletsCount.toString(), + hasBackup = scanResponse.card.backupStatus?.isActive, + ), + ) + } + } + + private fun setLoading(isLoading: Boolean) { + uiState.update { it.copy(isScanInProgress = isLoading) } + } + + fun handleScanError(error: TangemError) { + when (error) { + is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() + is TangemSdkError -> Timber.e(error, "Scan error occurred") + else -> Timber.e(error, "Error happened") + } + } + + private fun handleNfcFeatureUnavailable() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(R.string.nfc_error_unavailable), + title = resourceReference(id = R.string.common_error), + ), + ) + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt index f898f9c1b7..37a5113f35 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/entity/AddExistingWalletStartUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.entity internal data class AddExistingWalletStartUM( + val isScanInProgress: Boolean, val onBackClick: () -> Unit, val onImportPhraseClick: () -> Unit, val onScanCardClick: () -> Unit, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt index eef14e30c9..6bf693e36b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/start/ui/AddExistingWalletStartContent.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -74,14 +75,25 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi title = stringResourceSafe(R.string.wallet_import_scan_title), description = stringResourceSafe(R.string.wallet_import_scan_description), badge = { - Icon( - modifier = Modifier - .padding(top = 2.dp) - .size(20.dp), - painter = painterResource(R.drawable.ic_tangem_24), - contentDescription = null, - tint = TangemTheme.colors.icon.secondary, - ) + if (state.isScanInProgress) { + CircularProgressIndicator( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp) + .padding(2.dp), + color = TangemTheme.colors.text.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } else { + Icon( + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp), + painter = painterResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors.icon.secondary, + ) + } }, onClick = state.onScanCardClick, enabled = true, @@ -160,6 +172,7 @@ private fun PreviewCreateWalletContent() { TangemThemePreview { AddExistingWalletStartContent( state = AddExistingWalletStartUM( + isScanInProgress = true, onBackClick = {}, onImportPhraseClick = {}, onScanCardClick = {}, From 9278f6f6277609d692ac7046f11ac9df3298751a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 17:05:51 +0500 Subject: [PATCH 40/87] Updated on 2026-08-14 --- .../impl/presentation/state/InnerYieldBalanceState.kt | 1 + .../presentation/state/converters/BalanceItemConverter.kt | 1 + .../state/converters/RewardsValidatorStateConverter.kt | 1 + .../state/helpers/StakingFeeTransactionLoader.kt | 8 ++++---- .../state/previewdata/InitialStakingStatePreview.kt | 1 + .../validator/ValidatorSelectChangeTransformer.kt | 4 ++++ 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index a4b3ea96fd..7b669ddab9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -34,6 +34,7 @@ internal data class BalanceState( val validator: Yield.Validator?, val pendingActions: ImmutableList, val isPending: Boolean, + val validatorAddress: String?, ) @Immutable diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index 6b747f4dbd..8c08dc5acf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -68,6 +68,7 @@ internal class BalanceItemConverter( pendingActions = value.pendingActions.toPersistentList(), isClickable = value.isClickable(), isPending = value.isPending, + validatorAddress = value.validatorAddress, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 4fefa8ca21..1a23a879e8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -91,6 +91,7 @@ internal class RewardsValidatorStateConverter( isClickable = true, type = balance.type, isPending = balance.isPending, + validatorAddress = balance.validatorAddress, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index fe8e42f2a8..1b9528d027 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -57,8 +57,10 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( val state = stateController.value val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data ?: error("Illegal state") - val validatorState = state.validatorState as? StakingStates.ValidatorState.Data - ?: error("No validator provided") + + val validatorAddress = (state.validatorState as? StakingStates.ValidatorState.Data)?.chosenValidator?.address + ?: state.balanceState?.validatorAddress + ?: error("No validator address provided") val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") @@ -66,8 +68,6 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( val pendingAction = confirmationState.pendingAction val pendingActions = confirmationState.pendingActions - val validatorAddress = validatorState.chosenValidator.address - val isEnter = state.actionType is StakingActionCommonType.Enter val isApprovalNeeded = confirmationState.isApprovalNeeded val isAllowanceNotEnough = confirmationState.allowance < amount diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index d7af6496e2..7ad9912c1a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -100,6 +100,7 @@ internal object InitialStakingStatePreview { type = BalanceType.STAKED, subtitle = null, isPending = false, + validatorAddress = "", ), ), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index f9f71dd51e..9ef8d57a41 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -33,6 +33,10 @@ internal class ValidatorSelectChangeTransformer( selectedValidator } + if (selectedValidator == null && yield.preferredValidators.isEmpty()) { + return prevState + } + return prevState.copy( validatorState = StakingStates.ValidatorState.Data( chosenValidator = selectedValidator ?: yield.preferredValidators.first(), From 59f76deacafd9b9092a6090662cddcf4314058b9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Aug 2025 18:02:06 +0500 Subject: [PATCH 41/87] Updated on 2026-08-14 --- .../ui/amountScreen/utils/FormatterUtils.kt | 27 ++-- .../marketprice/MarketPriceBlock.kt | 4 +- .../format/bigdecimal/BigDecimalFiatFormat.kt | 5 +- .../core/ui/utils/BigDecimalFormatter.kt | 147 ------------------ .../impl/model/MarketsTokenDetailsModel.kt | 39 +++-- .../converters/ExchangeItemStateConverter.kt | 15 +- .../converters/PricePerformanceConverter.kt | 15 +- .../impl/model/formatter/Formatters.kt | 15 +- .../impl/model/state/QuotesStateUpdater.kt | 10 +- .../block/impl/model/TokenMarketBlockModel.kt | 18 ++- .../converters/MarketsTokenItemConverter.kt | 17 +- .../RewardsValidatorStateConverter.kt | 13 +- .../ShowApprovalBottomSheetTransformer.kt | 13 +- .../presentation/ui/block/StakingFeeBlock.kt | 4 +- .../swap/converters/TokensDataConverter.kt | 13 +- .../tangem/feature/swap/ui/StateBuilder.kt | 9 +- .../ui/components/TokenDetailsBalanceBlock.kt | 6 +- .../SingleWalletMarketPriceConverter.kt | 19 ++- .../VisaTxDetailsBottomSheetConverter.kt | 15 +- .../VisaTxHistoryItemStateConverter.kt | 15 +- 20 files changed, 157 insertions(+), 262 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index 9ee08290a8..af9627c4a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -2,9 +2,11 @@ package com.tangem.common.ui.amountScreen.utils import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN +import com.tangem.core.ui.format.bigdecimal.approximateAmount +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.utils.StringsSigns.DASH_SIGN import java.math.BigDecimal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { @@ -19,12 +21,19 @@ fun getFiatString( appCurrency: AppCurrency, approximate: Boolean = false, ): String { - if (value == null || rate == null) return EMPTY_BALANCE_SIGN + if (value == null || rate == null) return DASH_SIGN val feeValue = value.multiply(rate) - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - withApproximateSign = approximate, - ) + return feeValue.format { + if (approximate) { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).approximateAmount() + } else { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 52bdac7eab..204f816c64 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.utils.StringsSigns.DASH_SIGN /** * Market price block @@ -120,7 +120,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { ) } } else { - Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) + Price(price = DASH_SIGN, modifier = priceModifier) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 1834e10f45..aef02f2fe3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -121,7 +121,10 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { +/** + * Returns amount with correct scale + */ +fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt deleted file mode 100644 index 744a2d9bf3..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.core.ui.utils - -import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.LOWER_SIGN -import com.tangem.utils.StringsSigns.TILDE_SIGN -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.NumberFormat -import java.util.Currency -import java.util.Locale - -@Suppress("LargeClass") -@Deprecated("Use BigDecimal.format") -object BigDecimalFormatter { - - const val EMPTY_BALANCE_SIGN = DASH_SIGN - private const val CAN_BE_LOWER_SIGN = LOWER_SIGN - - private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") - - private const val FIAT_MARKET_DEFAULT_DIGITS = 2 - private const val FIAT_MARKET_EXTENDED_DIGITS = 6 - private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 - - private val usdCurrency = Currency.getInstance("USD") - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmount( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - decimals: Int = FIAT_MARKET_DEFAULT_DIGITS, - locale: Locale = Locale.getDefault(), - withApproximateSign: Boolean = false, - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - - val formatterCurrency = getCurrency(fiatCurrencyCode) - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = decimals - minimumFractionDigits = decimals - roundingMode = RoundingMode.HALF_UP - } - - return if (fiatAmount.checkFiatThreshold()) { - buildString { - append(CAN_BE_LOWER_SIGN) - append( - formatter.format(FIAT_FORMAT_THRESHOLD) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), - ) - } - } else { - val formattedAmount = formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - - if (withApproximateSign) { - buildString { - append(TILDE_SIGN) - append(formattedAmount) - } - } else { - formattedAmount - } - } - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatAmountUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val digits = if (fiatAmount.checkFiatThreshold()) { - FIAT_MARKET_EXTENDED_DIGITS - } else { - FIAT_MARKET_DEFAULT_DIGITS - } - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = digits - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(fiatAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun formatFiatPriceUncapped( - fiatAmount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - val formatterCurrency = getCurrency(fiatCurrencyCode) - - val (formattedAmount, finalScale) = getFiatPriceUncappedWithScale(value = fiatAmount) - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - maximumFractionDigits = finalScale - minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(formattedAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - @Deprecated("Use BigDecimal.format") - fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { - return if (value < BigDecimal.ONE) { - val leadingZeroes = value.scale() - value.precision() - val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES - - val amount = value - .setScale(scale, RoundingMode.HALF_UP) - .stripTrailingZeros() - - amount to amount.scale() - } else { - value to FIAT_MARKET_DEFAULT_DIGITS - } - } - - private fun getCurrency(code: String): Currency { - return runCatching { Currency.getInstance(code) } - .getOrElse { e -> - // Currency code is not valid ISO 4217 code - if (e is IllegalArgumentException) { - usdCurrency - } else { - throw e - } - } - } - - private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index 5e4e7a4d4d..0778b806c6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -18,9 +18,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType 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.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -164,11 +165,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( type = percentChangeType.toChartType(), xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), yAxisFormatter = { value -> - BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = value, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ) + value.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + } }, ) } @@ -196,11 +198,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( val state = MutableStateFlow( MarketsTokenDetailsUM( tokenName = params.token.name, - priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = params.token.tokenQuotes.currentPrice, - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + priceText = params.token.tokenQuotes.currentPrice.format { + fiat( + fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, dateTimeText = resourceReference(R.string.common_today), priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), @@ -403,7 +406,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( state.update { it.copy( - priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value), + priceText = newInfo.quotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + }, priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( interval = it.selectedInterval, ), @@ -490,7 +498,12 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: getDefaultDateTimeString(currentState.selectedInterval) - val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value) + val priceText = (price ?: currentQuotes.value.currentPrice).format { + fiat( + fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencyCode = currentAppCurrency.value.code, + ).price() + } val percent = price?.let { getChangePercentBetween( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt index 0ddafb4e83..eb55abd9dd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt @@ -5,7 +5,9 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.markets.TokenMarketExchange import com.tangem.domain.markets.TokenMarketExchange.TrustScore import com.tangem.features.markets.impl.R @@ -29,11 +31,12 @@ internal object ExchangeItemStateConverter : Converter h24ChangePercent @@ -66,8 +57,8 @@ internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: Bi } internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = currentPrice).first - val updated = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = updatedPrice).first + val current = getFiatPriceAmountWithScale(value = currentPrice).first + val updated = getFiatPriceAmountWithScale(value = updatedPrice).first return when { updated > current -> PriceChangeType.UP diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt index 4bf8b58e27..74d10c5742 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt @@ -3,6 +3,9 @@ package com.tangem.features.markets.details.impl.model.state import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo @@ -57,7 +60,12 @@ internal class QuotesStateUpdater( state.update { stateToUpdate -> stateToUpdate.copy( - priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()), + priceText = newQuotes.currentPrice.format { + fiat( + fiatCurrencySymbol = currentAppCurrency().symbol, + fiatCurrencyCode = currentAppCurrency().code, + ).price() + }, priceChangePercentText = newQuotes.getFormattedPercentByInterval( interval = stateToUpdate.selectedInterval, ), diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index 372309d5ea..bb1496cfe6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -11,9 +11,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.price import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.GetCurrencyQuotesUseCase @@ -86,13 +87,14 @@ internal class TokenMarketBlockModel @Inject constructor( ) state.value = state.value.copy( - currentPrice = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = res.fiatRate, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencyCode = currentAppCurrency.value.code, - // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ), + currentPrice = res.fiatRate.format { + fiat( + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencyCode = currentAppCurrency.value.code, + // TODO get currency from quotes use case [REDACTED_TASK_KEY] + fiatCurrencySymbol = currentAppCurrency.value.symbol, + ).price() + }, h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index d7352826b5..22712a972c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -7,11 +7,7 @@ import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket import com.tangem.features.markets.impl.R @@ -94,11 +90,12 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotesShort?.currentPrice - val priceText = BigDecimalFormatter.formatFiatPriceUncapped( - fiatAmount = tokenQuotesShort.currentPrice, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } val changeType = if (prevPrice != null) { if (tokenQuotesShort.currentPrice > prevPrice) { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index 3643ba7e27..4ab5ef020a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -2,8 +2,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -69,11 +69,12 @@ internal class RewardsValidatorStateConverter( }, ) val formattedFiatAmount = stringReference( - BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ) return BalanceState( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index c70f2da7a7..c39ac9a72f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -6,8 +6,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.R @@ -38,11 +38,12 @@ internal class ShowApprovalBottomSheetTransformer( val feeCryptoValue = fee.amount.value.format { crypto(fee.amount.currencySymbol, fee.amount.decimals) } - val feeFiatValue = BigDecimalFormatter.formatFiatAmount( - fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), - fiatCurrencyCode = appCurrencyProvider().code, - fiatCurrencySymbol = appCurrencyProvider().symbol, - ) + val feeFiatValue = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value).format { + fiat( + fiatCurrencyCode = appCurrencyProvider().code, + fiatCurrencySymbol = appCurrencyProvider().symbol, + ) + } return prevState.copy( bottomSheetConfig = TangemBottomSheetConfig( isShown = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index bce18ef1ec..9c5085c6b6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -26,9 +26,9 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.utils.StringsSigns.DASH_SIGN import java.math.BigDecimal @Composable @@ -126,7 +126,7 @@ private fun BoxScope.FeeError(feeState: FeeState) { ) { if (it == FeeState.Error) { Text( - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d31fdcffe5..86245f0585 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -3,8 +3,8 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -111,10 +111,11 @@ class TokensDataConverter( } private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = cryptoCurrencyStatus.value.fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return cryptoCurrencyStatus.value.fiatAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 9f250ee99b..0de047350a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -13,8 +13,8 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency @@ -1273,7 +1273,12 @@ internal class StateBuilder( private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() - return BigDecimalFormatter.formatFiatAmount(amount, appCurrency.code, appCurrency.symbol) + return amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { 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 ae100c11a3..d1110e97fa 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 @@ -20,11 +20,11 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.toImmutableList @Suppress("DestructuringDeclarationWithTooManyEntries") @@ -124,7 +124,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -154,7 +154,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Error -> Text( modifier = modifier, - text = BigDecimalFormatter.EMPTY_BALANCE_SIGN.orMaskWithStars(isBalanceHidden), + text = DASH_SIGN.orMaskWithStars(isBalanceHidden), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 8dadf5a69e..a2dc7210ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -4,11 +4,13 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter internal class SingleWalletMarketPriceConverter( @@ -47,17 +49,18 @@ internal class SingleWalletMarketPriceConverter( } private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { - val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatRate = status.fiatRate ?: return DASH_SIGN - return BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = fiatRate, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return fiatRate.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).uncapped() + } } private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { - val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val priceChange = status.priceChange ?: return DASH_SIGN return priceChange.format { percent() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index 99b787897e..f1ea722bd8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -2,13 +2,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxDetails -import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.feature.wallet.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.VisaTxDetailsBottomSheetConfig import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList import org.joda.time.DateTimeZone @@ -71,11 +71,12 @@ internal class VisaTxDetailsBottomSheetConverter( } private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String { - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = amount, - fiatCurrencyCode = fiatCurrency.currencyCode, - fiatCurrencySymbol = fiatCurrency.symbol, - ) + return amount.format { + fiat( + fiatCurrencyCode = fiatCurrency.currencyCode, + fiatCurrencySymbol = fiatCurrency.symbol, + ) + } } private companion object { 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 62d00e4c3c..0ec063a2f8 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 @@ -4,13 +4,13 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters 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.child.wallet.model.intents.VisaWalletIntents +import com.tangem.feature.wallet.impl.R import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -29,11 +29,12 @@ internal class VisaTxHistoryItemStateConverter( txHash = value.id, amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }, // Show tx fiat amount instead of tx time - time = BigDecimalFormatter.formatFiatAmount( - fiatAmount = value.fiatAmount, - fiatCurrencyCode = value.fiatCurrency.currencyCode, - fiatCurrencySymbol = value.fiatCurrency.symbol, - ), + time = value.fiatAmount.format { + fiat( + fiatCurrencyCode = value.fiatCurrency.currencyCode, + fiatCurrencySymbol = value.fiatCurrency.symbol, + ) + }, status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.INCOMING, iconRes = R.drawable.ic_arrow_up_24, From c39f617dd6a5bd9dd23426ea566bcc26664cf600 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 18:06:28 +0500 Subject: [PATCH 42/87] Updated on 2026-08-14 --- .../wallets/usecase/GetIsBiometricsEnabledUseCase.kt | 11 ----------- .../feature/wallet/child/wallet/model/WalletModel.kt | 10 ++++++++-- 2 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt deleted file mode 100644 index b57e8d3ec8..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsBiometricsEnabledUseCase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.sdk.api.TangemSdkManager -import javax.inject.Inject - -class GetIsBiometricsEnabledUseCase @Inject constructor( - private val tangemSdkManager: TangemSdkManager, -) { - - operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 467321629b..14324845f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -85,7 +85,7 @@ internal class WalletModel @Inject constructor( private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val notificationsFeatureToggles: NotificationsFeatureToggles, - private val getIsBiometryIsEnabledUseCase: GetIsBiometricsEnabledUseCase, + private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -213,9 +213,15 @@ internal class WalletModel @Inject constructor( modelScope.launch { val shouldAskPermission = shouldAskPermissionUseCase(PUSH_PERMISSION) val afterUpdate = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() - val isBiometricsEnabled = getIsBiometryIsEnabledUseCase() + val isBiometricsEnabled = shouldSaveUserWalletsSyncUseCase() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() val shouldShowBottomSheet = shouldAskPermission || afterUpdate + Timber.d( + "push BS afterUpdate: $afterUpdate," + + "shouldAskPermission $shouldAskPermission," + + "isBiometricsEnabled $isBiometricsEnabled," + + "isHuaweiDevice $isHuaweiDevice", + ) if (!isBiometricsEnabled) return@launch if (isHuaweiDevice) return@launch if (!shouldShowBottomSheet) return@launch From 7fe2a4d82326cd19a32d7dbdf83edaee45d34ca9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 18:13:29 +0300 Subject: [PATCH 43/87] Updated on 2026-08-14 --- .../core/ui/components/fields/PinTextField.kt | 75 ++++++++++++++----- .../hotwallet/accesscode/ui/AccessCode.kt | 2 + .../DefaultHotAccessCodeRequestComponent.kt | 3 +- .../HotAccessCodeRequestModel.kt | 18 ++++- .../entity/HotAccessCodeRequestUM.kt | 4 +- .../HotAccessCodeRequestFullScreenContent.kt | 15 +++- 6 files changed, 89 insertions(+), 28 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index fa00fb9959..0a593a7a8d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields import androidx.compose.animation.* import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -36,9 +37,9 @@ fun PinTextField( value: String, length: Int, isPasswordVisual: Boolean, + pinTextColor: PinTextColor, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, - wrongCode: Boolean = false, ) { val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { @@ -72,7 +73,7 @@ fun PinTextField( CellDecoration( length = length, isPasswordVisual = isPasswordVisual, - wrongCode = wrongCode, + pinTextColor = pinTextColor, value = value, ) }, @@ -84,17 +85,25 @@ fun PinTextField( } } -@Suppress("MagicNumber") +enum class PinTextColor { + Primary, + WrongCode, + Success, +} + +@Suppress("MagicNumber", "LongMethod") @Composable private fun CellDecoration( length: Int, - wrongCode: Boolean, + pinTextColor: PinTextColor, value: String, modifier: Modifier = Modifier, isPasswordVisual: Boolean = false, ) { val textMeasurer = rememberTextMeasurer() - val width = textMeasurer.measure("0") + val minSize = textMeasurer.measure("0") + val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell + val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell Row( modifier = modifier, @@ -107,6 +116,18 @@ private fun CellDecoration( "" } + val color = when (pinTextColor) { + PinTextColor.Primary -> { + if (isPasswordVisual) { + TangemTheme.colors.icon.informative + } else { + TangemTheme.colors.text.primary1 + } + } + PinTextColor.WrongCode -> TangemTheme.colors.icon.warning + PinTextColor.Success -> TangemTheme.colors.icon.accent + } + Box( modifier = Modifier .background( @@ -119,26 +140,34 @@ private fun CellDecoration( targetState = char, transitionSpec = { ( - fadeIn(animationSpec = tween(220, delayMillis = 90)) + - slideInVertically(animationSpec = tween(330, delayMillis = 0)) + fadeIn(animationSpec = tween(90, delayMillis = 90)) + + slideInVertically(animationSpec = tween(220, delayMillis = 0)) ) .togetherWith( fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)), ) }, ) { text -> - Text( - modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), - text = text, - style = TangemTheme.typography.h3, - color = if (wrongCode) { - TangemTheme.colors.text.warning - } else { - TangemTheme.colors.text.primary1 - }, - textAlign = TextAlign.Center, - lineHeight = 48.sp, - ) + if (isPasswordVisual && text.isNotEmpty()) { + Canvas( + Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + ) { + drawCircle( + color = color, + radius = 4.dp.toPx(), + center = center, + ) + } + } else { + Text( + modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight), + text = text, + style = TangemTheme.typography.h3, + color = color, + textAlign = TextAlign.Center, + lineHeight = 48.sp, + ) + } } } } @@ -152,10 +181,18 @@ private fun Preview() { var text by remember { mutableStateOf("123") } Column { + PinTextField( + value = text, + onValueChange = { text = it }, + isPasswordVisual = true, + pinTextColor = PinTextColor.Success, + length = 6, + ) PinTextField( value = text, onValueChange = { text = it }, isPasswordVisual = false, + pinTextColor = PinTextColor.Primary, length = 6, ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 987c94c953..230f97bab4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.res.R import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -75,6 +76,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { length = state.accessCodeLength, isPasswordVisual = true, value = state.accessCode, + pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, ) } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index 5bacfc9fef..6777d34243 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -26,8 +26,7 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( } override suspend fun successfulAuthentication() { - // TODO handle successful authentication - // TODO add delay + model.successfulAuthentication() } override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 305bd305bb..f210270b41 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM @@ -45,13 +46,23 @@ internal class HotAccessCodeRequestModel @Inject constructor( suspend fun wrongAccessCode() { uiState.update { it.copy( - wrongAccessCode = true, + accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, ) } delay(timeMillis = 500) // Delay to show the wrong access code state } + suspend fun successfulAuthentication() { + uiState.update { + it.copy( + accessCodeColor = PinTextColor.Success, + onAccessCodeChange = {}, + ) + } + delay(timeMillis = 200) // Delay to show the success state + } + private fun getInitialState() = HotAccessCodeRequestUM( onDismiss = ::dismiss, onAccessCodeChange = ::onAccessCodeChange, @@ -66,7 +77,10 @@ internal class HotAccessCodeRequestModel @Inject constructor( if (accessCode.length > ACCESS_CODE_LENGTH) return uiState.update { - it.copy(accessCode = accessCode, wrongAccessCode = false) + it.copy( + accessCode = accessCode, + accessCodeColor = PinTextColor.Primary, + ) } if (accessCode.length == ACCESS_CODE_LENGTH) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt index 82b7e51ec8..62f2b4d051 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.hotwallet.accesscoderequest.entity +import com.tangem.core.ui.components.fields.PinTextColor + internal data class HotAccessCodeRequestUM( val isShown: Boolean = false, val accessCode: String = "", - val wrongAccessCode: Boolean = false, + val accessCodeColor: PinTextColor = PinTextColor.Primary, val useBiometricVisible: Boolean = true, val useBiometricClick: () -> Unit = {}, val onAccessCodeChange: (String) -> Unit = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index bc64b660c4..9ae3230ec4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect @@ -85,7 +86,7 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM length = 6, isPasswordVisual = true, value = state.accessCode, - wrongCode = state.wrongAccessCode, + pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) } @@ -106,9 +107,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM val hapticManager = LocalHapticManager.current - LaunchedEffect(state.wrongAccessCode) { - if (state.wrongAccessCode) { - hapticManager.perform(TangemHapticEffect.View.Reject) + LaunchedEffect(state.accessCodeColor) { + when (state.accessCodeColor) { + PinTextColor.WrongCode -> { + hapticManager.perform(TangemHapticEffect.View.Reject) + } + PinTextColor.Success -> { + hapticManager.perform(TangemHapticEffect.View.Confirm) + } + else -> Unit } } } From ad19b86697b4a973e249b6ea2a7652053d5eb5f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 11:52:58 +0500 Subject: [PATCH 44/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 12 +++++++++--- core/res/src/main/res/values-es/strings.xml | 7 ++++--- core/res/src/main/res/values-fr/strings.xml | 5 ++++- core/res/src/main/res/values-ja/strings.xml | 14 +++++++++++--- core/res/src/main/res/values-ru/strings.xml | 11 +++++++---- core/res/src/main/res/values-uk-rUA/strings.xml | 8 +++++--- core/res/src/main/res/values/strings.xml | 14 +++++++++++--- .../transaction/model/WcSignTransactionModel.kt | 6 ++++-- 8 files changed, 55 insertions(+), 22 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7bc007eb36..c9acb51a42 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -118,6 +118,7 @@ Zugang verweigert Zum Portfolio hinzufügen Token hinzufügen + Vertragsadresse Alle Erlauben Betrag @@ -946,9 +947,13 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Tauschen und senden + Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht. + Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust. + Wähle das richtige Empfängernetzwerk Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. Wird an den Empfänger gesendet Zu erhaltender Betrag + Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht. Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. @@ -1403,11 +1408,12 @@ Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. + Tangem Wallet unterstützt derzeit nicht %s Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s + Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. %s Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem @@ -1431,7 +1437,7 @@ Daten kopieren Benutzerdefinierter Freibetrag Alle trennen - Text über die Trennung aller dApps + Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein. Alle dApps trennen Versuchen Sie erneut, mit einer neuen URI zu koppeln Ungültige dApp-Domain @@ -1455,7 +1461,7 @@ Transaktionsanfrage Transaktionsanfrage Unbegrenzte Menge - Wallet verbinden + WalletConnect Verwerfen Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? Ja, fortsetzen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 8d38231daf..3810328a13 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -115,6 +115,7 @@ Acceso denegado Añadir al portafolio Agregar token + Dirección Todos Autorizar Montante @@ -1353,7 +1354,7 @@ Hemos encontrado un error desconocido Actualmente, Tangem no es compatible con una red requerida por %s. Redes no compatibles - Tangem soporta una red requerida por %s + Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. %s Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app Tenemos algún tipo de problema @@ -1381,7 +1382,7 @@ Asignación personalizada dApp desconectada Desconectar todo - Texto sobre desconexión de todas las dApps + Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp. Desconectar todas las dApps Intente emparejar nuevamente con una URI nueva Dominio de dApp no válido @@ -1417,7 +1418,7 @@ Cantidad ilimitada Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único URI ya utilizado - Wallet connect + WalletConnect Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 00b0250a97..a24b04d401 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -97,6 +97,7 @@ Accès refusé Ajouter au portfolio Ajouter un jeton + Adresse Tous Permettre Montant @@ -1323,6 +1324,7 @@ dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue + Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes.%s Autoriser à dépenser Adresse Chargement @@ -1332,7 +1334,7 @@ Contenu Copier les données Déconnecter tout - Texte sur la déconnexion de toutes les dApps + Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. Déconnecter toutes les dApps Essayez de jumeler à nouveau avec un nouvel URI Domaine dApp invalide @@ -1353,6 +1355,7 @@ Demande de transaction Demande de transaction Montant illimité + WalletConnect Ignorer Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? Oui, reprendre diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index ff32b8bf8b..a66d2ba966 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,10 +12,13 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + 回復する + アーカイブ済み アカウントをアーカイブする アーカイブ このアカウントをアーカイブしますが、いつでも復元できます。 アカウント + アカウント番号%s — アドレス導出に使用されます。 アカウントを追加 保存 アカウント名 @@ -146,6 +149,7 @@ アクセスが拒否されました ポートフォリオに追加 トークンを追加 + アドレス すべて 許可する 金額 @@ -286,6 +290,7 @@ 取引状況 取引 送金 + データを読み込めません… わかりました エラーが発生しました。もう一度お試しください。 アクセスできません @@ -983,10 +988,13 @@ スワップして送信 変換を続行しますか? これにより以前のデータは消去されます。 変換を確定 + その他の通貨を送信すると、取り返しのつかない損失が発生します。 + 正しい受信者ネットワークを選択してください トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります 受取人へ 受取金額 + 受信者は%sを取得します 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 変換を削除 スワップして送信 @@ -1447,7 +1455,7 @@ 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク - Tangemは%sで必要なネットワークをサポートします + このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。%s 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています @@ -1475,7 +1483,7 @@ 使用可能量の設定 dAppが接続解除されました すべての接続を解除する - すべてのdAppsの接続解除に関するテキスト + すべてのdAppセッションが切断されます。ウォレットはどのdAppにも接続されなくなります。 すべてのdAppを接続解除する 新しいURIで、再度ペアリングを試してください 無効なdAppドメイン @@ -1511,7 +1519,7 @@ 無制限 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています - ウォレットコネクト + WalletConnect 不審な取引 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5b238275be..6bef7c8991 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -94,6 +94,7 @@ Доступ запрещен Добавить в портфель Добавить токен + Адрес Все Разрешить Сумма @@ -230,6 +231,7 @@ Статус транзакции Транзакции Перевод + Невозможно загрузить данные… Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно @@ -1316,12 +1318,12 @@ Код ошибки: %s. Если проблема сохраняется, обратитесь в нашу службу поддержки. Если проблема сохраняется, обратитесь в нашу службу поддержки Мы обнаружили неизвестную ошибку - Кошелек Tangem.в настоящий момент не поддерживает %s + Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp Мы обнаружили неизвестную ошибку Tangem в настоящее время не поддерживает необходимую сеть для %s Неподдерживаемые сети - Tangem поддерживает сеть, необходимую для %s + Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. %s Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема @@ -1349,6 +1351,7 @@ Настраиваемый лимит dApp отключен Отключить все + Все сессии dApp будут отключены. Ваш кошелёк больше не будет связан ни с одним dApp. Отключить все dApp Попробуйте соединиться снова, используя новый URI Недействительный домен dApp @@ -1356,7 +1359,7 @@ Нет сетей Пожалуйста, сгенерируйте новый URI и попробуйте подключиться снова Предложение подключения истекло - Предварительные изменения + Прогнозируемые изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. Оценка не поддерживается для %s Предложено %s @@ -1383,7 +1386,7 @@ Безлимитное количество Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. URI уже используется - Подключение кошелька + WalletConnect Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 79d1d3c092..e8b9b18fbc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -94,6 +94,7 @@ Доступ заборонено Додати у портфель Додати токен + Адреса Усе Дозволити Сума @@ -1301,9 +1302,10 @@ Сеанс Wallet Connect було завершено Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою + Tangem Wallet наразі не підтримує %s Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі - Tangem підтримує мережу, необхідну для %s + Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s Верифікований домен Обрана не вірна картка або кільце Схоже, виникла проблема @@ -1327,7 +1329,7 @@ Копіювати дані dApp відключено Розʼєднати все - Відключити всі dApps + Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. Відключити всі dApps Спробуйте ще раз з новим URI Недійсний домен dApp @@ -1353,7 +1355,7 @@ До Запит транзакції Запит транзакції - Підключення гаманця + WalletConnect Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 4089de8ca8..c81754117c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Archive You are archiving this account, but you can always get it back. Account + Account #%s — used for address derivation. Add account Save Account name @@ -70,8 +71,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. + You’ll be asked for your wallet’s access code later so we can securely store it for future use This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. Require Access Code This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction. Save Access Code @@ -151,6 +154,7 @@ Access denied Add to portfolio Add token + Address All Allow Amount @@ -295,6 +299,7 @@ Transaction status Transactions Transfer + Unable to load the data… I understand There was an error. Please try again. Unreachable @@ -490,6 +495,7 @@ Stay up to date with the latest features and news Seed phrase backup Create Mobile Wallet + This recovery phrase has already been imported Mobile Wallet This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation @@ -1001,6 +1007,8 @@ Swap and send Proceed with conversion? This will clear your previous data. Confirm Conversion + Sending any other currency will result in its irreversible loss. + Select the correct recipient network Send any token, and we’ll convert it on the way. Your recipient gets exactly what they need—seamlessly. Recipient will receive To recipient @@ -1515,7 +1523,7 @@ We\'ve encountered unknown error Tangem does not currently support a required network by %s. Unsupported networks - Tangem support a required network by %s + This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem @@ -1543,7 +1551,7 @@ Custom allowance dApp disconnected Disconnect all - Text about discnected all dApps + All dApp sessions will be disconnected. Your wallet will no longer be linked to any dApps. Disconect All dApps Try pairing again with a fresh URI Invalid dApp domain @@ -1580,7 +1588,7 @@ Unlimited Amount Ensure that each pairing attempt uses a fresh and unique URI URI already used - Wallet connect + WalletConnect Suspicious transaction Discard You have an interrupted backup. Do you want to resume? diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index ccc4413ef4..9f3a686357 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -131,8 +131,10 @@ internal class WcSignTransactionModel @Inject constructor( } private fun signingIsDone(signState: WcSignState<*>): Boolean { - (signState.domainStep as? WcSignStep.Result)?.result?.let { - showSuccessSignMessage() + (signState.domainStep as? WcSignStep.Result)?.result?.let { result -> + if (result.isRight()) { + showSuccessSignMessage() + } router.pop() return true } From bb954202243cc1e5487672c7cbbdef44863d29e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 09:55:03 +0300 Subject: [PATCH 45/87] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 3ac868e93f..7d225a195e 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98 +Subproject commit 7d225a195eb001f9f4ce88aa4a6fa2d965b9159c From e409fc4e0edd875bcceff2a0fe0b2ac29a4df93d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 12:57:51 +0500 Subject: [PATCH 46/87] Updated on 2026-08-14 --- .../buttons/small/TangemIconButton.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt index 39a042f707..c0d2dac09e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/small/TangemIconButton.kt @@ -3,10 +3,11 @@ package com.tangem.core.ui.components.buttons.small import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -41,20 +42,19 @@ fun TangemIconButton( background: Color = TangemTheme.colors.button.secondary, iconTint: Color = TangemTheme.colors.icon.secondary, ) { - IconButton( - onClick = onClick, + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), + contentDescription = "", + tint = iconTint, modifier = modifier + .size(24.dp) .clip(shape) .background(background) - .size(24.dp), - ) { - Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)), - contentDescription = "", - tint = iconTint, - modifier = Modifier.size(16.dp), - ) - } + .padding(4.dp) + .clickable( + onClick = onClick, + ), + ) } // region Preview From 694c215bb1a5ab439af9faa8d8b14394b974ecde Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 19:36:50 +0500 Subject: [PATCH 47/87] Updated on 2026-08-14 --- .../components/inputrow/InputRowRecipient.kt | 2 +- .../ui/components/rows/SelectorRowItem.kt | 9 +- .../send/v2/api/entity/FeeSelectorUM.kt | 35 ++++- .../features/send/v2/common/ui/FeeBlock.kt | 70 +++++++++ .../features/send/v2/common/ui/SendContent.kt | 8 +- .../ui/FeeSelectorModalBottomSheet.kt | 140 +++++------------- .../send/v2/send/DefaultSendComponent.kt | 21 --- .../success/SendConfirmSuccessComponent.kt | 4 - .../success/ui/SendConfirmSuccessContent.kt | 10 +- .../send/v2/subcomponents/fee/ui/FeeBlock.kt | 32 ++-- .../fee/ui/SendSpeedSelectorItem.kt | 3 +- .../presentation/ui/block/StakingFeeBlock.kt | 7 +- .../success/ui/SendWithSwapSuccessContent.kt | 9 +- .../feature/swap/ui/ChooseFeeBottomSheet.kt | 6 +- 14 files changed, 178 insertions(+), 178 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index 418341d870..259553da0b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -76,7 +76,7 @@ fun InputRowRecipient( val (titleText, color) = if (isError && error != null) { error to TangemTheme.colors.text.warning } else { - title to TangemTheme.colors.text.secondary + title to TangemTheme.colors.text.tertiary } DividerContainer( modifier = modifier, 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 1499aaecaf..be5201fa5c 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 @@ -2,7 +2,6 @@ package com.tangem.core.ui.components.rows import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -23,7 +22,7 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe +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.test.SelectNetworkFeeBottomSheetTestTags @@ -31,7 +30,7 @@ import com.tangem.utils.StringsSigns @Composable fun SelectorRowItem( - @StringRes titleRes: Int, + title: TextReference, @DrawableRes iconRes: Int, modifier: Modifier = Modifier, paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12), @@ -80,7 +79,7 @@ fun SelectorRowItem( contentDescription = null, ) Text( - text = stringResourceSafe(titleRes), + text = title.resolveReference(), style = textStyle, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), @@ -151,7 +150,7 @@ private fun RowScope.SelectorValueContent( private fun SelectorRowItemPreview() { TangemThemePreview { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_slow, + title = resourceReference(R.string.common_fee_selector_option_slow), iconRes = R.drawable.ic_tortoise_24, preDot = TextReference.Str("1000 ETH"), postDot = TextReference.Str("1000 $"), diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index ca50585ecf..89ddb338b6 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -5,8 +5,10 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.R import com.tangem.features.send.v2.api.entity.FeeItem.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -72,14 +74,37 @@ sealed class FeeNonce { @Immutable sealed class FeeItem { abstract val fee: Fee + abstract val title: TextReference + abstract val iconRes: Int fun isSameClass(other: FeeItem): Boolean { return this::class == other::class } - data class Suggested(val title: TextReference, override val fee: Fee) : FeeItem() - data class Slow(override val fee: Fee) : FeeItem() - data class Market(override val fee: Fee) : FeeItem() - data class Fast(override val fee: Fee) : FeeItem() - data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() + data class Suggested( + override val title: TextReference, + override val fee: Fee, + ) : FeeItem() { + override val iconRes: Int = R.drawable.ic_star_mini_24 + } + + data class Slow(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_slow) + override val iconRes: Int = R.drawable.ic_tortoise_24 + } + + data class Market(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_market) + override val iconRes: Int = R.drawable.ic_bird_24 + } + + data class Fast(override val fee: Fee) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_fee_selector_option_fast) + override val iconRes: Int = R.drawable.ic_hare_24 + } + + data class Custom(override val fee: Fee, val customValues: ImmutableList) : FeeItem() { + override val title: TextReference = resourceReference(R.string.common_custom) + override val iconRes: Int = R.drawable.ic_edit_v2_24 + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt new file mode 100644 index 0000000000..473a7b235e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/FeeBlock.kt @@ -0,0 +1,70 @@ +package com.tangem.features.send.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +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 com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R + +@Composable +internal fun FeeBlock(feeSelectorUM: FeeSelectorUM) { + if (feeSelectorUM !is FeeSelectorUM.Content) return + val feeExtraInfo = feeSelectorUM.feeExtraInfo + val feeFiatRateUM = feeSelectorUM.feeFiatRateUM + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + + Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount + SelectorRowItem( + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, + preDot = remember { + stringReference( + feeAmount.value.format { + crypto( + symbol = feeAmount.currencySymbol, + decimals = feeAmount.decimals, + ).fee(canBeLower = feeExtraInfo.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeExtraInfo.isFeeConvertibleToFiat && feeFiatRateUM != null) { + getFiatReference(feeAmount.value, feeFiatRateUM.rate, feeFiatRateUM.appCurrency) + } else { + null + } + }, + ellipsizeOffset = feeAmount.currencySymbol.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt index 0f860e75fc..610020a85c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/ui/SendContent.kt @@ -18,8 +18,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.v2.common.CommonSendRoute -import com.tangem.features.send.v2.send.confirm.SendConfirmComponent -import com.tangem.features.send.v2.send.success.SendConfirmSuccessComponent @Composable internal fun SendContent( @@ -38,9 +36,9 @@ internal fun SendContent( Children( stack = stackState, animation = stackAnimation { child -> - when (child.instance) { - is SendConfirmSuccessComponent -> fade(minAlpha = 1.0f) - is SendConfirmComponent -> fade() + when (child.configuration) { + is CommonSendRoute.ConfirmSuccess -> fade(minAlpha = 1.0f) + is CommonSendRoute.Confirm -> fade() else -> slide() } }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 8f3ef2cb54..36ae4fc32f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -115,7 +115,6 @@ private fun FeeTitle(feeDisplaySource: FeeSelectorParams.FeeDisplaySource, onDis } } -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun FeeSelectorItems( state: FeeSelectorUM.Content, @@ -144,110 +143,6 @@ private fun FeeSelectorItems( .selectedBorder(isSelected = isSelected) .clickableSingle(onClick = { feeSelectorIntents.onFeeItemSelected(item) }) when (item) { - is FeeItem.Suggested -> RegularFeeItemContent( - modifier = itemModifier, - title = item.title, - iconRes = R.drawable.ic_star_mini_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Slow -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_slow), - iconRes = R.drawable.ic_tortoise_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Market -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_market), - iconRes = R.drawable.ic_bird_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) - is FeeItem.Fast -> RegularFeeItemContent( - modifier = itemModifier, - title = resourceReference(R.string.common_fee_selector_option_fast), - iconRes = R.drawable.ic_hare_24, - iconBackgroundColor = iconBackgroundColor, - iconTint = iconTint, - preDot = stringReference( - item.fee.amount.value.format { - crypto( - symbol = item.fee.amount.currencySymbol, - decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) - }, - ), - postDot = if (feeFiatRateUM != null) { - getFiatReference( - value = item.fee.amount.value, - rate = feeFiatRateUM.rate, - appCurrency = feeFiatRateUM.appCurrency, - ) - } else { - null - }, - ellipsizeOffset = item.fee.amount.currencySymbol.length, - showDivider = !isSelected && !lastItem, - ) is FeeItem.Custom -> CustomFeeBlock( modifier = itemModifier, customFee = item, @@ -257,6 +152,32 @@ private fun FeeSelectorItems( onValueChange = feeSelectorIntents::onCustomFeeValueChange, nonce = state.feeNonce, ) + else -> RegularFeeItemContent( + modifier = itemModifier, + title = item.title, + iconRes = item.iconRes, + iconBackgroundColor = iconBackgroundColor, + iconTint = iconTint, + preDot = stringReference( + item.fee.amount.value.format { + crypto( + symbol = item.fee.amount.currencySymbol, + decimals = item.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + }, + ), + postDot = if (feeFiatRateUM != null) { + getFiatReference( + value = item.fee.amount.value, + rate = feeFiatRateUM.rate, + appCurrency = feeFiatRateUM.appCurrency, + ) + } else { + null + }, + ellipsizeOffset = item.fee.amount.currencySymbol.length, + showDivider = !isSelected && !lastItem, + ) } } } @@ -491,7 +412,14 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< fee = Fee.Common(Amount(value = BigDecimal("0.1"), blockchain = Blockchain.Ethereum)), ), FeeItem.Slow(fee = Fee.Common(Amount(value = BigDecimal("0.01"), blockchain = Blockchain.Ethereum))), - FeeItem.Market(fee = Fee.Common(Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum))), + FeeItem.Market( + fee = Fee.Common( + Amount( + value = BigDecimal("0.02"), + blockchain = Blockchain.Ethereum, + ), + ), + ), FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))), customFeeItem, ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 9ac55d3a43..74c7fa09ce 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -38,7 +38,6 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationBlockComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import dagger.assisted.Assisted @@ -254,7 +253,6 @@ internal class DefaultSendComponent @AssistedInject constructor( val destinationAddress = (state.destinationUM as? DestinationUM.Content)?.addressTextField?.value val txUrl = (state.confirmUM as? ConfirmUM.Success)?.txUrl val cryptoCurrencyStatus = model.cryptoCurrencyStatusFlow.value - val feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatusFlow.value if (sendAmount == null || destinationAddress == null || @@ -279,29 +277,10 @@ internal class DefaultSendComponent @AssistedInject constructor( onClick = {}, ) - val feeBlockComponent = SendFeeBlockComponent( - appComponentContext = child("sendConfirmFeeBlock"), - params = SendFeeComponentParams.FeeBlockParams( - state = model.uiState.value.feeUM, - analyticsCategoryName = model.analyticCategoryName, - userWallet = model.userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - appCurrency = model.appCurrency, - sendAmount = sendAmount, - destinationAddress = destinationAddress, - blockClickEnableFlow = MutableStateFlow(true), - onLoadFee = model::loadFee, - ), - onResult = { }, - onClick = {}, - ) - return SendConfirmSuccessComponent( appComponentContext = factoryContext, params = SendConfirmSuccessComponent.Params( sendUMFlow = model.uiState, - feeBlockComponent = feeBlockComponent, destinationBlockComponent = destinationBlockComponent, analyticsCategoryName = model.analyticCategoryName, currentRoute = model.currentRoute, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt index c9fa321928..5468cbf45e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/SendConfirmSuccessComponent.kt @@ -12,7 +12,6 @@ import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.send.success.model.SendConfirmSuccessModel import com.tangem.features.send.v2.send.success.ui.SendConfirmSuccessContent import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +22,6 @@ internal class SendConfirmSuccessComponent( private val model: SendConfirmSuccessModel = getOrCreateModel(params = params) private val destinationBlockComponent: SendDestinationBlockComponent = params.destinationBlockComponent - private val feeBlockComponent: SendFeeBlockComponent = params.feeBlockComponent @Composable override fun Content(modifier: Modifier) { @@ -31,14 +29,12 @@ internal class SendConfirmSuccessComponent( SendConfirmSuccessContent( sendUM = state, destinationBlockComponent = destinationBlockComponent, - feeBlockComponent = feeBlockComponent, ) } data class Params( val sendUMFlow: StateFlow, val destinationBlockComponent: SendDestinationBlockComponent, - val feeBlockComponent: SendFeeBlockComponent, val analyticsCategoryName: String, val currentRoute: Flow, val txUrl: String, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 179bc3f11b..636828e73f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -21,18 +21,14 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toPx import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.common.ui.FeeBlock import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.send.ui.state.SendUM -import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import kotlinx.coroutines.delay @Composable -internal fun SendConfirmSuccessContent( - sendUM: SendUM, - destinationBlockComponent: SendDestinationBlockComponent, - feeBlockComponent: SendFeeBlockComponent, -) { +internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent: SendDestinationBlockComponent) { var visible by remember { mutableStateOf(false) } LaunchedEffect(Unit) { @@ -84,7 +80,7 @@ internal fun SendConfirmSuccessContent( onClick = {}, ) destinationBlockComponent.Content(modifier = Modifier) - feeBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = sendUM.feeSelectorUM) Spacer(Modifier.height(60.dp)) } BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt index 10284bde92..5aae6a80e5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/FeeBlock.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* 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,6 +15,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee 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.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN @@ -62,20 +64,24 @@ internal fun FeeBlock(feeUM: FeeUM, isClickEnabled: Boolean, onClick: () -> Unit R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 } SelectorRowItem( - titleRes = title, + title = resourceReference(title), iconRes = icon, - preDot = stringReference( - feeAmount?.value.format { - crypto( - symbol = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - ).fee(canBeLower = feeUM.isFeeApproximate) - }, - ), - postDot = if (feeUM.isFeeConvertibleToFiat) { - getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) - } else { - null + preDot = remember { + stringReference( + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeUM.isFeeApproximate) + }, + ) + }, + postDot = remember { + if (feeUM.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeUM.rate, feeUM.appCurrency) + } else { + null + } }, ellipsizeOffset = feeAmount?.currencySymbol?.length, isSelected = true, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt index 3cc6d6042b..09ae784965 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendSpeedSelectorItem.kt @@ -16,6 +16,7 @@ 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 +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.EMPTY_BALANCE_SIGN import com.tangem.core.ui.format.bigdecimal.crypto @@ -52,7 +53,7 @@ internal fun SendSpeedSelectorItem( .clickable { onSelect() }, ) { SelectorRowItem( - titleRes = titleRes, + title = resourceReference(titleRes), iconRes = iconRes, onSelect = onSelect, modifier = modifier, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 9c5085c6b6..0ca152acfb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -19,6 +19,7 @@ import com.tangem.common.ui.R 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.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.format.bigdecimal.crypto @@ -51,7 +52,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { is FeeState.Content -> { val feeAmount = feeState.fee?.amount SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = stringReference( feeAmount?.value.format { @@ -75,7 +76,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Loading -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), @@ -85,7 +86,7 @@ internal fun StakingFeeBlock(feeState: FeeState) { } is FeeState.Error -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, isSelected = true, paddingValues = PaddingValues(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index e77b8efd44..6e5406b9e6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -202,16 +202,17 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) { .padding(TangemTheme.dimens.spacing12), ) { Text( - text = stringResourceSafe(com.tangem.common.ui.R.string.common_network_fee_title), + text = stringResourceSafe(R.string.common_network_fee_title), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) Box(modifier = Modifier.padding(top = TangemTheme.dimens.spacing8)) { - val feeAmount = feeSelectorUM.selectedFeeItem.fee.amount + val feeItemUM = feeSelectorUM.selectedFeeItem + val feeAmount = feeItemUM.fee.amount SelectorRowItem( - titleRes = com.tangem.common.ui.R.string.common_fee_selector_option_market, - iconRes = com.tangem.common.ui.R.drawable.ic_bird_24, + title = feeItemUM.title, + iconRes = feeItemUM.iconRes, preDot = stringReference( feeAmount.value.format { crypto( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt index 1932d52634..3946ea9358 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt @@ -14,8 +14,8 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -110,7 +110,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { when (feeItem.feeType) { FeeType.NORMAL -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_market, + title = resourceReference(R.string.common_fee_selector_option_market), iconRes = R.drawable.ic_bird_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), @@ -122,7 +122,7 @@ private fun FeeItemsBlock(content: ChooseFeeBottomSheetConfig) { } FeeType.PRIORITY -> { SelectorRowItem( - titleRes = R.string.common_fee_selector_option_fast, + title = resourceReference(R.string.common_fee_selector_option_fast), iconRes = R.drawable.ic_hare_24, preDot = TextReference.Str(preDotText), postDot = TextReference.Str(postDot), From 143dcc5a6287579c15e896ad97b6a0f625a0b932 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:17:04 +0300 Subject: [PATCH 48/87] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 9 ++ .../java/com/tangem/tap/TangemApplication.kt | 12 +++ .../common/redux/legacy/LegacyMiddleware.kt | 15 ++- .../tap/di/domain/WalletsDomainModule.kt | 2 + .../di/UserWalletsListManagerModule.kt | 3 +- .../DefaultUserWalletsListRepository.kt | 85 +++++++++++------ .../UserWalletEncryptionKeysRepository.kt | 26 +++-- .../details/redux/DetailsMiddleware.kt | 95 +++++++++++++++++++ .../features/details/redux/DetailsReducer.kt | 17 ++++ .../features/details/redux/DetailsState.kt | 7 +- .../appsettings/AppSettingsDialogsFactory.kt | 34 +++++++ .../ui/appsettings/AppSettingsItemsFactory.kt | 36 +++++++ .../ui/appsettings/model/AppSettingsModel.kt | 86 +++++++++++++++-- .../tap/proxy/redux/DaggerGraphState.kt | 6 ++ .../local/preferences/PreferencesKeys.kt | 4 + core/res/src/main/res/values-de/strings.xml | 8 +- core/res/src/main/res/values-es/strings.xml | 6 +- core/res/src/main/res/values-fr/strings.xml | 3 +- core/res/src/main/res/values-ja/strings.xml | 13 ++- core/res/src/main/res/values-ru/strings.xml | 8 +- .../src/main/res/values-uk-rUA/strings.xml | 4 +- core/res/src/main/res/values/strings.xml | 9 +- .../data/wallets/DefaultWalletsRepository.kt | 65 +++++++++++++ .../data/wallets/hot/HotWalletAccessor.kt | 55 +++++++++-- .../core/wallets/UserWalletsListRepository.kt | 15 ++- .../repositories/SettingsRepository.kt | 2 + .../wallets/repository/WalletsRepository.kt | 10 ++ .../wallets/usecase/SaveWalletUseCase.kt | 20 ++-- features/biometry/impl/build.gradle.kts | 1 + .../biometry/impl/model/AskBiometryModel.kt | 28 +++++- .../hotwallet/accesscode/AccessCodeModel.kt | 28 +++--- .../model/AddExistingWalletImportModel.kt | 2 +- .../welcome/impl/model/WelcomeModel.kt | 10 +- .../welcome/impl/ui/WelcomeSelectWallet.kt | 22 +++-- 34 files changed, 632 insertions(+), 114 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ed9d07c577..aa3d4f2a9a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -38,7 +39,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.hot.sdk.TangemHotSdk import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles @@ -142,4 +145,10 @@ interface ApplicationEntryPoint { fun getApiConfigsManager(): ApiConfigsManager fun getUserTokensResponseStore(): UserTokensResponseStore + + fun getUserWalletsListRepository(): UserWalletsListRepository + + fun getTangemHotSdk(): TangemHotSdk + + fun getHotWalletFeatureToggles(): HotWalletFeatureToggles } \ 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 ef784f8347..09c556cae1 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -227,6 +227,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val userTokensResponseStore: UserTokensResponseStore get() = entryPoint.getUserTokensResponseStore() + private val userWalletsListRepository + get() = entryPoint.getUserWalletsListRepository() + + private val tangemHotSdk + get() = entryPoint.getTangemHotSdk() + + private val hotWalletFeatureToggles + get() = entryPoint.getHotWalletFeatureToggles() + // endregion private val appScope = MainScope() @@ -364,6 +373,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat uiMessageSender = uiMessageSender, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, userTokensResponseStore = userTokensResponseStore, + userWalletsListRepository = userWalletsListRepository, + tangemHotSdk = tangemHotSdk, + hotWalletFeatureToggles = hotWalletFeatureToggles, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 15530c62b3..f944a89717 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -26,10 +26,9 @@ internal object LegacyMiddleware { { action -> when (action) { is LegacyAction.PrepareDetailsScreen -> { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - userWalletsListManager.selectedUserWallet + selectedUserWallet() .distinctUntilChanged() .onEach { selectedUserWallet -> val initializedAppSettingsStateContent = initializeAppSettingsState( @@ -52,6 +51,16 @@ internal object LegacyMiddleware { } } + private fun selectedUserWallet(): Flow { + val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) + return if (hotWalletFeatureToggles.isHotWalletEnabled) { + store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull() + } else { + val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) + userWalletsListManager.selectedUserWallet + } + } + /** * LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking * previously it was initialized in runBlocking and blocked details screen @@ -64,6 +73,8 @@ internal object LegacyMiddleware { selectedAppCurrency = store.state.globalState.appCurrency, selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, + requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(), + useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(), isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository) .getBalanceHidingSettings().isHidingEnabledInSettings, needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 5b21957999..44dd702fc3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -123,10 +123,12 @@ internal object WalletsDomainModule { userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, hotWalletFeatureToggles: HotWalletFeatureToggles, + walletsRepository: WalletsRepository, ): SaveWalletUseCase { return SaveWalletUseCase( userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, + walletsRepository = walletsRepository, useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index f6323a20de..d8173d8bc7 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -119,6 +119,7 @@ internal object UserWalletsListManagerModule { @ApplicationContext applicationContext: Context, dispatchers: CoroutineDispatcherProvider, passwordRequester: HotWalletPasswordRequester, + appPreferencesStore: AppPreferencesStore, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -162,8 +163,8 @@ internal object UserWalletsListManagerModule { passwordRequester = passwordRequester, userWalletEncryptionKeysRepository = userWalletEncryptionKeysRepository, tangemSdkManagerProvider = Provider { tangemSdkManager }, + appPreferencesStore = appPreferencesStore, savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now - // TODO add a settings toggle to disable saving persistent information ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 6d907c384b..d900b3f687 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -8,6 +8,9 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.flatMap import com.tangem.common.map +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked @@ -43,6 +46,7 @@ internal class DefaultUserWalletsListRepository( private val userWalletEncryptionKeysRepository: UserWalletEncryptionKeysRepository, private val tangemSdkManagerProvider: Provider, private val savePersistentInformation: ProviderSuspend, + private val appPreferencesStore: AppPreferencesStore, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -129,38 +133,46 @@ internal class DefaultUserWalletsListRepository( userWallet } - override suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either = - either { - val userWallet = userWallets.value?.find { it.walletId == userWalletId } - ?: raise(SetLockError.UserWalletNotFound) + override suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean, + ): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + ?: raise(SetLockError.UserWalletNotFound) - val encryptionKey = userWallet.encryptionKey - ?: raise(SetLockError.UserWalletLocked) + val encryptionKey = userWallet.encryptionKey + ?: raise(SetLockError.UserWalletLocked) - runCatching { - userWalletEncryptionKeysRepository.save( - encryptionKey = UserWalletEncryptionKey( - walletId = userWalletId, - encryptionKey = encryptionKey, - ), - method = when (lockMethod) { - is LockMethod.AccessCode -> { - UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode) + runCatching { + userWalletEncryptionKeysRepository.save( + encryptionKey = UserWalletEncryptionKey( + walletId = userWalletId, + encryptionKey = encryptionKey, + ), + removeUnsecured = changeUnsecured, + method = when (lockMethod) { + is LockMethod.AccessCode -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Password(lockMethod.accessCode) + } + LockMethod.Biometric -> { + UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric + } + LockMethod.NoLock -> { + if (userWallet is UserWallet.Cold) { + raise(SetLockError.UserWalletNotFound) } - LockMethod.Biometric -> { - UserWalletEncryptionKeysRepository.EncryptionMethod.Biometric - } - LockMethod.NoLock -> { - if (userWallet is UserWallet.Cold) { - raise(SetLockError.UserWalletNotFound) - } - UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured - } - }, - ) - }.onFailure { raise(SetLockError.UnableToSetLock(it)) } - } + UserWalletEncryptionKeysRepository.EncryptionMethod.Unsecured + } + }, + ) + }.onFailure { raise(SetLockError.UnableToSetLock(it)) } + } + + override suspend fun removeBiometricLock(userWalletId: UserWalletId) { + userWalletEncryptionKeysRepository.removeBiometricKey(userWalletId) + } override suspend fun delete(userWalletIds: List): Either = either { if (userWalletIds.isEmpty()) return Unit.right() @@ -269,9 +281,11 @@ internal class DefaultUserWalletsListRepository( } val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() - val allKeys = biometricKeys + unsecuredKeys + val allKeys = (biometricKeys + unsecuredKeys).distinct() + val unlockedWallets = allKeys.map { it.walletId } - if (allKeys.all { it.walletId in userWalletIds }.not()) { + // if we cant unlock all wallets + if (userWalletIds.all { it in unlockedWallets }.not()) { raise(UnlockWalletError.UnableToUnlock) } @@ -309,7 +323,7 @@ internal class DefaultUserWalletsListRepository( biometryFallback: suspend () -> Either, ): Either { val result = passwordRequester.requestPassword( - hasBiometry = tangemSdkManagerProvider.invoke().canUseBiometry, + hasBiometry = hasBiometry(), ) return when (result) { @@ -339,6 +353,15 @@ internal class DefaultUserWalletsListRepository( } } + private suspend fun hasBiometry(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + default = false, + ) + + return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication + } + /** * Find the nearest available wallet that can be selected * diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt index 43a5b4916f..104cfc0945 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletEncryptionKeysRepository.kt @@ -25,8 +25,14 @@ internal class UserWalletEncryptionKeysRepository( Types.newParameterizedType(List::class.java, UserWalletId::class.java), ) - suspend fun save(encryptionKey: UserWalletEncryptionKey, method: EncryptionMethod) = withContext(dispatchers.io) { - secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name) + suspend fun save( + encryptionKey: UserWalletEncryptionKey, + removeUnsecured: Boolean = true, + method: EncryptionMethod, + ) = withContext(dispatchers.io) { + if (removeUnsecured) { + secureStorage.delete(StorageKey.UserWalletEncryptionKeyUnsecured(encryptionKey.walletId).name) + } when (method) { EncryptionMethod.Unsecured -> { @@ -35,12 +41,6 @@ internal class UserWalletEncryptionKeysRepository( data = encryptionKey.encode(), ) } - EncryptionMethod.Biometric -> { - authenticatedStorage.store( - keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, - data = encryptionKey.encode(), - ) - } is EncryptionMethod.Password -> { val encodedWithPass = AESEncryptionProtocol.encryptWithPassword( password = method.password, @@ -51,11 +51,21 @@ internal class UserWalletEncryptionKeysRepository( data = encodedWithPass, ) } + EncryptionMethod.Biometric -> { + authenticatedStorage.store( + keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, + data = encryptionKey.encode(), + ) + } } storeUserWalletId(userWalletId = encryptionKey.walletId) } + fun removeBiometricKey(userWalletId: UserWalletId) { + authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) + } + suspend fun getAllUnsecured(): List = withContext(dispatchers.io) { getUserWalletsIds().mapNotNull { userWalletId -> secureStorage.get(account = StorageKey.UserWalletEncryptionKeyUnsecured(userWalletId).name).decodeToKey() 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 3aece96fa9..9281cbc507 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 @@ -6,7 +6,9 @@ import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -64,6 +66,14 @@ class DetailsMiddleware { when (action.setting) { AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) + AppSetting.RequireAccessCode -> toggleRequireAccessCode( + state = state, + enable = action.enable, + ) + AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication( + state = state, + enable = action.enable, + ) } } is DetailsAction.AppSettings.CheckBiometricsStatus -> { @@ -90,6 +100,91 @@ class DetailsMiddleware { } } + private fun toggleBiometricsAuthentication(state: DetailsState, enable: Boolean) { + scope.launch { + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + + // Nothing to change + if (walletsRepository.useBiometricAuthentication() == enable) { + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + return@launch + } + + toggleRequireAccessCode( + state = state, + enable = true, + ) + + if (enable) { + setBiometricLockForAllWallets() + } else { + // Remove all biometric-related data + removeAllBiometricData() + } + + walletsRepository.setUseBiometricAuthentication(value = enable) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + } + } + + private fun toggleRequireAccessCode(state: DetailsState, enable: Boolean) { + scope.launch { + val walletsRepository = store.inject(DaggerGraphState::walletsRepository) + + // Nothing to change + if (walletsRepository.requireAccessCode() == enable) { + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + return@launch + } + + if (enable) { + // Remove all biometric sign data + removeAllBiometricSingData() + toggleSaveAccessCodes(state, enable = false) + } else { + toggleSaveAccessCodes(state, enable = true) + } + + walletsRepository.setRequireAccessCode(value = enable) + store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) + } + } + + private suspend fun setBiometricLockForAllWallets() { + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val userWallets = userWalletsListRepository.userWalletsSync() + userWallets.forEach { + userWalletsListRepository.setLock( + userWalletId = it.walletId, + lockMethod = LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + + private suspend fun removeAllBiometricData() { + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + userWalletsListRepository.userWalletsSync().forEach { + userWalletsListRepository.removeBiometricLock(it.walletId) + } + removeAllBiometricSingData() + } + + private suspend fun removeAllBiometricSingData() { + deleteSavedAccessCodes() + val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) + val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk) + userWalletsListRepository.userWalletsSync().forEach { + if (it is UserWallet.Hot) { + userWalletsListRepository.saveWithoutLock( + userWallet = it.copy( + hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(it.hotWalletId), + ), + ) + } + } + } + private fun observeBiometricsStatusChanges(scope: CoroutineScope) { val needEnrollBiometricsFlow = flow { do { 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 a6f2b28868..e783c18e37 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 @@ -33,6 +33,7 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta ) } +@Suppress("LongMethod", "CyclomaticComplexMethod") private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { return when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( @@ -46,6 +47,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail saveWallets = true, // User can't enable access codes saving without wallets saving saveAccessCodes = action.enable, ) + AppSetting.RequireAccessCode -> state.appSettingsState.copy( + isInProgress = true, + requireAccessCode = action.enable, + ) + AppSetting.BiometricAuthentication -> state.appSettingsState.copy( + isInProgress = true, + useBiometricAuthentication = action.enable, + ) }, ) is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy( @@ -63,6 +72,14 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail isInProgress = false, saveAccessCodes = action.prevState, ) + AppSetting.RequireAccessCode -> state.appSettingsState.copy( + isInProgress = false, + requireAccessCode = action.prevState, + ) + AppSetting.BiometricAuthentication -> state.appSettingsState.copy( + isInProgress = false, + needEnrollBiometrics = action.prevState, + ) }, ) is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index bc7f17a7b6..980cacb286 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -12,9 +12,14 @@ data class DetailsState( ) : StateType data class AppSettingsState( + @Deprecated("Delete after hot wallet release") val saveWallets: Boolean = false, + @Deprecated("Delete after hot wallet release") val saveAccessCodes: Boolean = false, + @Deprecated("Delete after hot wallet release") val isBiometricsAvailable: Boolean = false, + val requireAccessCode: Boolean = false, + val useBiometricAuthentication: Boolean = false, val needEnrollBiometrics: Boolean = false, val isHidingEnabled: Boolean = false, val isInProgress: Boolean = false, @@ -25,5 +30,5 @@ data class AppSettingsState( enum class SecurityOption { LongTap, PassCode, AccessCode } enum class AppSetting { - SaveWallets, SaveAccessCode + SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 43e66b065a..73459bc7b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @@ -55,4 +56,37 @@ internal class AppSettingsDialogsFactory { onDismiss = onDismiss, ) } + + fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference( + R.string.app_settings_off_biometrics_alert_message, + wrappedList(resourceReference(R.string.common_biometrics)), + ), + confirmText = resourceReference(R.string.common_disable), + onConfirm = onDisable, + onDismiss = onDismiss, + ) + } + + fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_on_require_access_code_alert_message), + confirmText = resourceReference(R.string.common_enable), + onConfirm = { onEnable() }, + onDismiss = onDismiss, + ) + } + + fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_require_access_code_alert_message), + confirmText = resourceReference(R.string.common_disable), + onConfirm = { onDisable() }, + onDismiss = onDismiss, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 47bbb8376f..bde3cf116f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item import com.tangem.wallet.R @@ -33,6 +34,39 @@ internal class AppSettingsItemsFactory { ) } + fun createUseBiometricsSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = ID_USE_BIOMETRICS_SWITCH, + title = resourceReference(R.string.app_settings_enable_biometrics_title), + description = resourceReference( + R.string.app_settings_biometrics_footer, + wrappedList(resourceReference(R.string.common_biometrics)), + ), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createRequireAccessCodeSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = ID_REQUIRE_ACCESS_CODE_SWITCH, + title = resourceReference(R.string.app_settings_require_access_code), + description = resourceReference(R.string.app_settings_require_access_code_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + fun createSaveAccessCodeSwitch( isChecked: Boolean, isEnabled: Boolean, @@ -96,5 +130,7 @@ internal class AppSettingsItemsFactory { const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch" const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button" const val ID_SELECT_THEME_MODE_BUTTON = "select_theme_mode_button" + const val ID_USE_BIOMETRICS_SWITCH = "use_biometrics_switch" + const val ID_REQUIRE_ACCESS_CODE_SWITCH = "require_access_code_switch" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index ee93849f31..8b9f0e5a01 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -13,6 +13,7 @@ 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.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -51,6 +52,7 @@ internal class AppSettingsModel @Inject constructor( private val appThemeModeRepository: AppThemeModeRepository, private val settingsRepository: SettingsRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model(), StoreSubscriber { private val itemsFactory = AppSettingsItemsFactory() @@ -109,20 +111,36 @@ internal class AppSettingsModel @Inject constructor( onClick = ::showAppCurrencySelector, ).let(::add) - if (state.isBiometricsAvailable) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress - itemsFactory.createSaveWalletsSwitch( - isChecked = state.saveWallets, + itemsFactory.createUseBiometricsSwitch( + isChecked = state.useBiometricAuthentication, isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveWalletsToggled, + onCheckedChange = ::onBiometricAuthenticationToggled, ).let(::add) - itemsFactory.createSaveAccessCodeSwitch( - isChecked = state.saveAccessCodes, - isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveAccessCodesToggled, + itemsFactory.createRequireAccessCodeSwitch( + isChecked = state.requireAccessCode, + isEnabled = canUseBiometrics && state.useBiometricAuthentication, + onCheckedChange = ::onRequireAccessCodeToggled, ).let(::add) + } else { + if (state.isBiometricsAvailable) { + val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress + + itemsFactory.createSaveWalletsSwitch( + isChecked = state.saveWallets, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveWalletsToggled, + ).let(::add) + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = state.saveAccessCodes, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveAccessCodesToggled, + ).let(::add) + } } itemsFactory.createFlipToHideBalanceSwitch( @@ -168,6 +186,56 @@ internal class AppSettingsModel @Inject constructor( } } + private fun onBiometricAuthenticationToggled(isChecked: Boolean) { + // TODO : Uncomment and implement analytics event when ready + // val param = AnalyticsParam.OnOffState(isChecked) + // analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param)) + if (isChecked) { + onSettingsToggled(AppSetting.BiometricAuthentication, enable = true) + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDisableBiometricAuthenticationAlert( + onDisable = { + onSettingsToggled(AppSetting.BiometricAuthentication, enable = false) + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onRequireAccessCodeToggled(isChecked: Boolean) { + // TODO : Uncomment and implement analytics event when ready + // val param = AnalyticsParam.OnOffState(isChecked) + // analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param)) + updateContentState { + copy( + dialog = if (isChecked) { + dialogsFactory.createEnableRequireAccessCodeAlert( + onEnable = { + onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ) + } else { + dialogsFactory.createDisableRequireAccessCodeAlert( + onDisable = { + onSettingsToggled(AppSetting.RequireAccessCode, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ) + }, + ) + } + } + private fun onSaveWalletsToggled(isChecked: Boolean) { if (isChecked) { onSettingsToggled(AppSetting.SaveWallets, enable = true) @@ -236,6 +304,8 @@ internal class AppSettingsModel @Inject constructor( saveWallets = walletsRepository.shouldSaveUserWalletsSync(), saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), isBiometricsAvailable = canUseBiometryUseCase(), + useBiometricAuthentication = walletsRepository.useBiometricAuthentication(), + requireAccessCode = walletsRepository.requireAccessCode(), isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, 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 1c696ac842..e0b2794648 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 @@ -21,6 +21,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -31,7 +32,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.hot.sdk.TangemHotSdk import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository @@ -77,4 +80,7 @@ data class DaggerGraphState( val cardArworksProvider: CardArtworksProvider? = null, val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, val userTokensResponseStore: UserTokensResponseStore? = null, + val userWalletsListRepository: UserWalletsListRepository? = null, + val hotWalletFeatureToggles: HotWalletFeatureToggles? = null, + val tangemHotSdk: TangemHotSdk? = null, ) : StateType \ 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 a737416dd5..6c0e164b4e 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 @@ -84,6 +84,10 @@ object PreferencesKeys { val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") } + val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") } + + val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") } + val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") } val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 554f254397..38278a60f0 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -947,9 +947,13 @@ Die Gebühr geht über die Bilanz hinaus Der Gesamtbetrag geht über die Bilanz hinaus Tauschen und senden + Mit der Konvertierung fortfahren? Dadurch werden Deine vorherigen Daten gelöscht. + Das Senden einer anderen Währung führt zu deren unwiderruflichem Verlust. + Wähle das richtige Empfängernetzwerk Sende uns ein Token, und wir konvertieren es unterwegs. Dein Empfänger erhält genau das, was er braucht – nahtlos. Wird an den Empfänger gesendet Zu erhaltender Betrag + Möchtest Du die Konvertierung wirklich abbrechen? Deine bisherigen Daten werden gelöscht. Senden mit Swap Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. @@ -1408,7 +1412,7 @@ Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Tangem unterstützt ein erforderliches Netzwerk um %s + Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. %s Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem @@ -1432,7 +1436,7 @@ Daten kopieren Benutzerdefinierter Freibetrag Alle trennen - Text über die Trennung aller dApps + Alle dApp-Sitzungen werden getrennt. Ihre Wallet wird nicht mehr mit dApps verbunden sein. Alle dApps trennen Versuchen Sie erneut, mit einer neuen URI zu koppeln Ungültige dApp-Domain diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b5eeeccbc8..3810328a13 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1354,7 +1354,7 @@ Hemos encontrado un error desconocido Actualmente, Tangem no es compatible con una red requerida por %s. Redes no compatibles - Tangem soporta una red requerida por %s + Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. %s Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app Tenemos algún tipo de problema @@ -1382,7 +1382,7 @@ Asignación personalizada dApp desconectada Desconectar todo - Texto sobre desconexión de todas las dApps + Todas las sesiones de dApp se desconectarán. Su billetera ya no estará vinculada a ninguna dApp. Desconectar todas las dApps Intente emparejar nuevamente con una URI nueva Dominio de dApp no válido @@ -1418,7 +1418,7 @@ Cantidad ilimitada Asegúrese de que cada intento de emparejamiento utiliza un URI nuevo y único URI ya utilizado - Wallet connect + WalletConnect Transacción sospechosa Ignorar Tiene un backup interrumpido. ¿Quiere reanudarlo? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 8e7d9898e8..900aa6723d 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1324,6 +1324,7 @@ dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue + Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes.%s Autoriser à dépenser Adresse Chargement @@ -1333,7 +1334,7 @@ Contenu Copier les données Déconnecter tout - Texte sur la déconnexion de toutes les dApps + Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. Déconnecter toutes les dApps Essayez de jumeler à nouveau avec un nouvel URI Domaine dApp invalide diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 2986582d9d..a66d2ba966 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -12,10 +12,13 @@ ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 アクセスコードの作成 アクセスコード + 回復する + アーカイブ済み アカウントをアーカイブする アーカイブ このアカウントをアーカイブしますが、いつでも復元できます。 アカウント + アカウント番号%s — アドレス導出に使用されます。 アカウントを追加 保存 アカウント名 @@ -287,6 +290,7 @@ 取引状況 取引 送金 + データを読み込めません… わかりました エラーが発生しました。もう一度お試しください。 アクセスできません @@ -984,10 +988,13 @@ スワップして送信 変換を続行しますか? これにより以前のデータは消去されます。 変換を確定 + その他の通貨を送信すると、取り返しのつかない損失が発生します。 + 正しい受信者ネットワークを選択してください トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者は受け取ります 受取人へ 受取金額 + 受信者は%sを取得します 変換をキャンセルしてもよろしいですか?以前のデータは消去されます。 変換を削除 スワップして送信 @@ -1448,7 +1455,7 @@ 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク - Tangemは%sで必要なネットワークをサポートします + このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。%s 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています @@ -1476,7 +1483,7 @@ 使用可能量の設定 dAppが接続解除されました すべての接続を解除する - すべてのdAppsの接続解除に関するテキスト + すべてのdAppセッションが切断されます。ウォレットはどのdAppにも接続されなくなります。 すべてのdAppを接続解除する 新しいURIで、再度ペアリングを試してください 無効なdAppドメイン @@ -1512,7 +1519,7 @@ 無制限 各ペアリング試行で、新しくユニークなURIが使用されていることを確認します URIはすでに使用されています - ウォレットコネクト + WalletConnect 不審な取引 破棄 バックアップが中断されました。再開しますか? diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 32374c0f84..f9fdb1b3f2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -231,6 +231,7 @@ Статус транзакции Транзакции Перевод + Невозможно загрузить данные… Я понял Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно @@ -1317,12 +1318,12 @@ Код ошибки: %s. Если проблема сохраняется, обратитесь в нашу службу поддержки. Если проблема сохраняется, обратитесь в нашу службу поддержки Мы обнаружили неизвестную ошибку - Кошелек Tangem.в настоящий момент не поддерживает %s + Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp Мы обнаружили неизвестную ошибку Tangem в настоящее время не поддерживает необходимую сеть для %s Неподдерживаемые сети - Tangem поддерживает сеть, необходимую для %s + Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. %s Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема @@ -1350,6 +1351,7 @@ Настраиваемый лимит dApp отключен Отключить все + Все сессии dApp будут отключены. Ваш кошелёк больше не будет связан ни с одним dApp. Отключить все dApp Попробуйте соединиться снова, используя новый URI Недействительный домен dApp @@ -1384,7 +1386,7 @@ Безлимитное количество Убедитесь, что каждая попытка сопряжения использует новый и уникальный URI. URI уже используется - Подключение кошелька + WalletConnect Подозрительная транзакция Отказаться Вы не закончили резервное копирование. Хотите продолжить? diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 0b17da86ca..952d5a7dd6 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1304,7 +1304,7 @@ Ми зіткнулися з невідомою помилкою Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі - Tangem підтримує мережу, необхідну для %s + Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s Верифікований домен Обрана не вірна картка або кільце Схоже, виникла проблема @@ -1328,7 +1328,7 @@ Копіювати дані dApp відключено Розʼєднати все - Відключити всі dApps + Усі сесії dApp буде відключено. Ваш гаманець більше не буде пов’язаний із жодним dApp. Відключити всі dApps Спробуйте ще раз з новим URI Недійсний домен dApp diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f0624f96ea..a76469f95b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -71,8 +71,10 @@ Go to settings to enable biometric authentication in the Tangem App Enable biometric authentication Disabling %1$s will require you to enter your passcode to unlock the app and to interact with your wallet. + You’ll be asked for your wallet’s access code later so we can securely store it for future use This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. Removing the saved devices deletes all the saved wallets and their access codes from the app. + This will delete all the saved wallet access codes. Any further interaction with the wallet will require submitting the access code. Require Access Code This option disables biometric authentication for sensitive actions. You will be required to enter your access code every time, such as when signing a transaction. Save Access Code @@ -297,6 +299,7 @@ Transaction status Transactions Transfer + Unable to load the data… I understand There was an error. Please try again. Unreachable @@ -1519,7 +1522,7 @@ We\'ve encountered unknown error Tangem does not currently support a required network by %s. Unsupported networks - Tangem support a required network by %s + This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem @@ -1547,7 +1550,7 @@ Custom allowance dApp disconnected Disconnect all - Text about discnected all dApps + All dApp sessions will be disconnected. Your wallet will no longer be linked to any dApps. Disconect All dApps Try pairing again with a fresh URI Invalid dApp domain @@ -1584,7 +1587,7 @@ Unlimited Amount Ensure that each pairing attempt uses a fresh and unique URI URI already used - Wallet connect + WalletConnect Suspicious transaction Discard You have an interrupted backup. Do you want to resume? diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 8808bd336a..2127d9b80b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -17,6 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFI import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.wallet.UserWallet @@ -47,14 +48,78 @@ internal class DefaultWalletsRepository( return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override fun shouldSaveUserWallets(): Flow { return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) } + @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") override suspend fun saveShouldSaveUserWallets(item: Boolean) { appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item) } + override suspend fun useBiometricAuthentication(): Boolean { + val useBiometricAuthentication = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + ) + + if (useBiometricAuthentication != null) { + return useBiometricAuthentication + } + + val legacySaveWalletsInTheApp = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SAVE_USER_WALLETS_KEY, + ) + + if (legacySaveWalletsInTheApp != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, + value = legacySaveWalletsInTheApp, + ) + return legacySaveWalletsInTheApp + } else { + // Default value for new users + setUseBiometricAuthentication(false) + return false + } + } + + override suspend fun setUseBiometricAuthentication(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, value = value) + } + + override suspend fun requireAccessCode(): Boolean { + val requireAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + ) + + if (requireAccessCode != null) { + return requireAccessCode + } + + val legacyShouldSaveAccessCode = appPreferencesStore.getSyncOrNull( + key = PreferencesKeys.SHOULD_SAVE_ACCESS_CODES_KEY, + ) + + if (legacyShouldSaveAccessCode != null) { + // Migrate legacy setting to new one + appPreferencesStore.store( + key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, + value = legacyShouldSaveAccessCode.not(), + ) + return legacyShouldSaveAccessCode.not() + } else { + // Default value for new users + setRequireAccessCode(true) + return true + } + } + + override suspend fun setRequireAccessCode(value: Boolean) { + appPreferencesStore.store(key = PreferencesKeys.REQUIRE_ACCESS_CODE_KEY, value = value) + } + override suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean { return appPreferencesStore .getSyncOrDefault(key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, default = emptySet()) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index 9c26e8a3ee..b53cd193d1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -1,7 +1,11 @@ package com.tangem.data.wallets.hot import com.tangem.common.core.TangemSdkError +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.copy import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* @@ -9,7 +13,9 @@ import javax.inject.Inject class HotWalletAccessor @Inject constructor( private val tangemHotSdk: TangemHotSdk, + private val userWalletsListRepository: UserWalletsListRepository, private val hotWalletPasswordRequester: HotWalletPasswordRequester, + private val walletsRepository: WalletsRepository, ) { suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List = @@ -23,10 +29,18 @@ class HotWalletAccessor @Inject constructor( } private suspend fun hotSdkRequest(hotWalletId: HotWalletId, block: suspend (unlock: UnlockHotWallet) -> T): T { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth HotWalletId.AuthType.Password -> requestPassword(false) - HotWalletId.AuthType.Biometry -> HotAuth.Biometry + HotWalletId.AuthType.Biometry -> { + if (isAccessCodeRequired) { + requestPassword(false) + } else { + HotAuth.Biometry + } + } } return runCatchingSdkErrors(hotWalletId, auth) { @@ -47,20 +61,41 @@ class HotWalletAccessor @Inject constructor( block = { blockAuth -> block(blockAuth).also { // Update biometry auth if the original auth was password - if (blockAuth is HotAuth.Password) { - tangemHotSdk.changeAuth( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = blockAuth, - ), - auth = HotAuth.Biometry, - ) - } + updateBiometryAuthIfNeeded( + hotWalletId = hotWalletId, + originalAuth = blockAuth, + ) } }, ) } + private suspend fun updateBiometryAuthIfNeeded(hotWalletId: HotWalletId, originalAuth: HotAuth) { + val isAccessCodeRequired = walletsRepository.requireAccessCode() + + if (originalAuth is HotAuth.Password && isAccessCodeRequired.not()) { + val userWallet = userWalletsListRepository.userWalletsSync() + .find { it is UserWallet.Hot && it.hotWalletId == hotWalletId } + as? UserWallet.Hot + ?: return + + val newHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = originalAuth, + ), + auth = HotAuth.Biometry, + ) + + userWalletsListRepository.saveWithoutLock( + userWallet = userWallet.copy( + hotWalletId = newHotWalletId, + ), + canOverride = true, + ) + } + } + private suspend fun runCatchingWrongPassInternal( originalAuth: HotAuth, auth: HotAuth, diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt index f0f9ee988a..fbcfeb9a0c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/wallets/UserWalletsListRepository.kt @@ -73,8 +73,21 @@ interface UserWalletsListRepository { * If the wallet is not found, it returns [SetLockError.UserWalletNotFound] * If the wallet is locked, it returns [SetLockError.UserWalletLocked] * If the lock method is not supported, it returns [SetLockError.UnableToSetLock]. + * + * @param userWalletId The ID of the user wallet to set the lock for. + * @param lockMethod The method to use for locking the wallet. + * @param changeUnsecured If false, the method will have no effect on unsecured wallets. */ - suspend fun setLock(userWalletId: UserWalletId, lockMethod: LockMethod): Either + suspend fun setLock( + userWalletId: UserWalletId, + lockMethod: LockMethod, + changeUnsecured: Boolean = true, + ): Either + + /** + * Removes biometric lock for user wallet if it is set. + */ + suspend fun removeBiometricLock(userWalletId: UserWalletId) /** * Deletes user wallets by ids. diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index 1a64989894..dd8608db95 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -24,8 +24,10 @@ interface SettingsRepository { suspend fun setShouldOpenWelcomeScreenOnResume(value: Boolean) + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun shouldSaveAccessCodes(): Boolean + @Deprecated("Use walletsRepository.requireAccessCode instead") suspend fun setShouldSaveAccessCodes(value: Boolean) suspend fun incrementAppLaunchCounter() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 5a6c051057..a1eb1f0cd8 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -11,10 +11,20 @@ interface WalletsRepository { suspend fun shouldSaveUserWalletsSync(): Boolean + @Deprecated("Hot wallet make always save user wallets. Do not use this method") fun shouldSaveUserWallets(): Flow + @Deprecated("Hot wallet make always save user wallets. Do not use this method") suspend fun saveShouldSaveUserWallets(item: Boolean) + suspend fun useBiometricAuthentication(): Boolean + + suspend fun setUseBiometricAuthentication(value: Boolean) + + suspend fun requireAccessCode(): Boolean + + suspend fun setRequireAccessCode(value: Boolean) + suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 23b10bf3c9..7e328ae146 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -6,11 +6,12 @@ import arrow.core.raise.either import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.legacy.UserWalletsListError +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.repository.WalletsRepository /** * Use case for saving user wallet @@ -22,6 +23,7 @@ import com.tangem.domain.core.wallets.UserWalletsListRepository class SaveWalletUseCase( private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, private val useNewRepository: Boolean, ) { @@ -35,10 +37,14 @@ class SaveWalletUseCase( if (newUserWallet) { when (userWallet) { is UserWallet.Cold -> { - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } else { + Unit.right() + } } is UserWallet.Hot -> { userWalletsListRepository.setLock( diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index 4ee4b4358b..def97bf1f3 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { api(projects.features.biometry.api) + implementation(projects.features.hotWallet.api) /** Core modules */ implementation(projects.core.ui) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 0713f99e5b..614b462e24 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository @@ -20,6 +21,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -45,6 +47,8 @@ internal class AskBiometryModel @Inject constructor( private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsManager: SettingsManager, private val uiMessageSender: UiMessageSender, + private val userWalletsListRepository: UserWalletsListRepository, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -109,10 +113,18 @@ internal class AskBiometryModel @Inject constructor( walletsRepository.saveShouldSaveUserWallets(item = true) settingsRepository.setShouldSaveAccessCodes(value = true) - if (userWallet is UserWallet.Cold) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + walletsRepository.setUseBiometricAuthentication(value = true) + setBiometryLockForAllWallets() cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, + isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(), ) + } else { + if (userWallet is UserWallet.Cold) { + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) + } } if (_uiState.value.bottomSheetVariant) { @@ -123,6 +135,18 @@ internal class AskBiometryModel @Inject constructor( params.modelCallbacks.onAllowed() } + private fun setBiometryLockForAllWallets() { + modelScope.launch { + userWalletsListRepository.userWalletsSync().forEach { userWallet -> + userWalletsListRepository.setLock( + userWalletId = userWallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + } + private fun showEnrollBiometricsDialog() { uiMessageSender.send( DialogMessage( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 373af1cf05..2db69f48ee 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -8,6 +8,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM import com.tangem.hot.sdk.TangemHotSdk @@ -28,6 +29,7 @@ internal class AccessCodeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletsListRepository: UserWalletsListRepository, + private val walletsRepository: WalletsRepository, private val tangemHotSdk: TangemHotSdk, ) : Model() { @@ -85,13 +87,15 @@ internal class AccessCodeModel @Inject constructor( auth = HotAuth.Password(accessCode.toCharArray()), ) - updatedHotWalletId = tangemHotSdk.changeAuth( - unlockHotWallet = UnlockHotWallet( - walletId = updatedHotWalletId, - auth = HotAuth.Password(accessCode.toCharArray()), - ), - auth = HotAuth.Biometry, - ) + if (walletsRepository.requireAccessCode().not()) { + updatedHotWalletId = tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = updatedHotWalletId, + auth = HotAuth.Password(accessCode.toCharArray()), + ), + auth = HotAuth.Biometry, + ) + } userWalletsListRepository.saveWithoutLock( userWallet.copy( @@ -106,10 +110,12 @@ internal class AccessCodeModel @Inject constructor( UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()), ) - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) + if (walletsRepository.useBiometricAuthentication()) { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.Biometric, + ) + } params.callbacks.onAccessCodeConfirmed(params.userWalletId) }.onFailure { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 29196737e8..d495361f44 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -91,7 +91,7 @@ internal class AddExistingWalletImportModel @Inject constructor( val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() - saveUserWalletUseCase(userWallet) + saveUserWalletUseCase(userWallet.copy(backedUp = true)) params.callbacks.onWalletImported(userWallet.walletId) }.onFailure { Timber.e(it) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index b81a8806c5..0c6739540e 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.features.welcome.impl.R @@ -35,6 +36,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class WelcomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, @@ -43,6 +45,7 @@ internal class WelcomeModel @Inject constructor( private val userWalletsFetcherFactory: UserWalletsFetcher.Factory, private val userWalletsListRepository: UserWalletsListRepository, private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase, + private val walletsRepository: WalletsRepository, ) : Model() { // TODO add intent handling @@ -154,8 +157,7 @@ internal class WelcomeModel @Inject constructor( when (option) { Create -> router.push(AppRoute.CreateWalletSelection) Add -> router.push(AppRoute.AddExistingWallet) - Buy -> { - } + Buy -> Unit // TODO } } @@ -182,8 +184,8 @@ internal class WelcomeModel @Inject constructor( unlockWallet(userWallet.walletId, unlockMethod) } - private fun canUnlockWithBiometrics(): Boolean { - return getIsBiometricsEnabledUseCase.canUseBiometry() + private suspend fun canUnlockWithBiometrics(): Boolean { + return getIsBiometricsEnabledUseCase.canUseBiometry() && walletsRepository.useBiometricAuthentication() } suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index b24c0a7b55..305b00750c 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -70,16 +70,18 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) - SecondaryButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .navigationBarsPadding() - .animateEnterExit(fadeIn(), fadeOut()), - text = "Unlock all with biometric", - onClick = state.onUnlockWithBiometricClick, - ) + if (state.showUnlockWithBiometricButton) { + SecondaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp) + .navigationBarsPadding() + .animateEnterExit(fadeIn(), fadeOut()), + text = "Unlock all with biometric", + onClick = state.onUnlockWithBiometricClick, + ) + } } LaunchedEffect(state.wallets) { From 7d0121c40cd1c1c5781a23c9e0c45465849c9e52 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:33:32 +0500 Subject: [PATCH 49/87] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 11 ++++++++++- .../routing/AddExistingWalletChildFactory.kt | 2 ++ .../api/PushNotificationsParams.kt | 3 +++ .../analytics/PushNotificationAnalyticEvents.kt | 9 +++++++++ .../impl/model/PushNotificationsModel.kt | 17 +++++++++++------ .../wallet/child/wallet/WalletComponent.kt | 2 ++ 7 files changed, 38 insertions(+), 7 deletions(-) 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 dd4108f2e0..82979df7ee 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 @@ -384,6 +384,7 @@ internal class ChildFactory @Inject constructor( context = context, params = PushNotificationsParams( modelCallbacks = PushNotificationsModelCallbacksStub(), + source = route.source, ), componentFactory = pushNotificationsComponentFactory, ) 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 77a7b1f113..682378bf51 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 @@ -1,6 +1,7 @@ package com.tangem.common.routing import android.os.Bundle +import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent @@ -185,7 +186,15 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId") @Serializable - data object PushNotification : AppRoute(path = "/push_notification") + data class PushNotification( + val source: Source, + ) : AppRoute(path = "/push_notification") { + enum class Source { + Stories, + Main, + Onboarding, + } + } @Serializable data class WalletSettings( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt index 26321ced04..a07186b664 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt @@ -1,5 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -51,6 +52,7 @@ internal class AddExistingWalletChildFactory @Inject constructor( context = childContext, params = PushNotificationsParams( modelCallbacks = PushNotificationsModelCallbacksStub(), + source = AppRoute.PushNotification.Source.Onboarding, ), ) AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent( diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt index 27bc4d7131..9e347a47ba 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsParams.kt @@ -1,6 +1,9 @@ package com.tangem.features.pushnotifications.api +import com.tangem.common.routing.AppRoute + data class PushNotificationsParams( val isBottomSheet: Boolean = false, val modelCallbacks: PushNotificationsModelCallbacks, + val source: AppRoute.PushNotification.Source, ) \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt index 7977e16e18..8ffec6c23c 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/analytics/PushNotificationAnalyticEvents.kt @@ -53,6 +53,15 @@ sealed class PushNotificationAnalyticEvents( ), ) + data class NotificationsScreenOpened( + val source: AnalyticsParam.ScreensSources, + ) : PushNotificationAnalyticEvents( + event = "Push Notification Screen Opened", + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), + ) + data class NotificationsEnabled( val isEnabled: Boolean, ) : PushNotificationAnalyticEvents( diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index ebf625c1ee..dab4f52d73 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -37,6 +37,11 @@ internal class PushNotificationsModel @Inject constructor( ) : Model(), PushNotificationsClickIntents { val params: PushNotificationsParams = paramsContainer.require() + val source = when (params.source) { + AppRoute.PushNotification.Source.Stories -> AnalyticsParam.ScreensSources.Stories + AppRoute.PushNotification.Source.Main -> AnalyticsParam.ScreensSources.Main + AppRoute.PushNotification.Source.Onboarding -> AnalyticsParam.ScreensSources.Onboarding + } private val _state = MutableStateFlow( PushNotificationsUM( @@ -44,6 +49,10 @@ internal class PushNotificationsModel @Inject constructor( ), ) + init { + analyticHandler.send(PushNotificationAnalyticEvents.NotificationsScreenOpened(source)) + } + val state = _state.asStateFlow() override fun onAllowClick() { @@ -52,9 +61,7 @@ internal class PushNotificationsModel @Inject constructor( notificationsRepository.setUserAllowToSubscribeOnPushNotifications(true) } } - analyticHandler.send( - PushNotificationAnalyticEvents.ButtonAllow(AnalyticsParam.ScreensSources.Stories), - ) + analyticHandler.send(PushNotificationAnalyticEvents.ButtonAllow(source)) } override fun onLaterClick() { @@ -63,9 +70,7 @@ internal class PushNotificationsModel @Inject constructor( notificationsRepository.setUserAllowToSubscribeOnPushNotifications(false) } } - analyticHandler.send( - PushNotificationAnalyticEvents.ButtonLater(AnalyticsParam.ScreensSources.Stories), - ) + analyticHandler.send(PushNotificationAnalyticEvents.ButtonLater(source)) modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 07a629a880..8ee7b2c6c2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -74,6 +75,7 @@ internal class WalletComponent @AssistedInject constructor( params = PushNotificationsParams( isBottomSheet = true, modelCallbacks = model.askForPushNotificationsModelCallbacks, + source = AppRoute.PushNotification.Source.Main, ), ) } From 5284873a3ed89f0aaa769f77b042887c780c4a40 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 14:06:58 +0500 Subject: [PATCH 50/87] Updated on 2026-08-14 --- .../tangem/features/disclaimer/impl/model/DisclaimerModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index 2c45bfb510..a34f346733 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -53,7 +53,7 @@ internal class DisclaimerModel @Inject constructor( val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() if (shouldAskPushPermission && !isHuaweiDevice) { - router.push(AppRoute.PushNotification) + router.push(AppRoute.PushNotification(AppRoute.PushNotification.Source.Stories)) } else { neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) neverRequestPermissionUseCase(PUSH_PERMISSION) From 7cfd21d898438366f8db34f64e389d1950b1bb21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:24:55 +0300 Subject: [PATCH 51/87] Updated on 2026-08-14 --- .../features/welcome/impl/model/WelcomeModel.kt | 16 +++++++++++++--- .../tangem/features/welcome/impl/ui/Welcome.kt | 4 +++- .../features/welcome/impl/ui/state/WelcomeUM.kt | 2 ++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 0c6739540e..1901cc427a 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -67,6 +67,7 @@ internal class WelcomeModel @Inject constructor( ) private val walletsFetcherJobHolder = JobHolder() private val wallets = MutableStateFlow>(persistentListOf()) + private var routedOut = false init { modelScope.launch { @@ -87,6 +88,7 @@ internal class WelcomeModel @Inject constructor( if (canUnlockWithBiometrics()) { userWalletsListRepository.unlockAllWallets() .onRight { + routedOut = true router.replaceAll(AppRoute.Wallet) } .onLeft { @@ -100,16 +102,19 @@ internal class WelcomeModel @Inject constructor( } } - private fun tryToUnlockWithAccessCodeRightAway() = modelScope.launch { + private suspend fun tryToUnlockWithAccessCodeRightAway() { if (onlyOneHotWalletWithAccessCode()) { val userWallets = userWalletsListRepository.userWalletsSync() val userWallet = userWallets.first() + uiState.value = WelcomeUM.Empty unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode) } } private fun setSelectWalletState() { modelScope.launch { + if (routedOut || uiState.value is WelcomeUM.SelectWallet) return@launch + uiState.value = WelcomeUM.SelectWallet( wallets = walletsFetcher.userWallets.first(), showUnlockWithBiometricButton = canUnlockWithBiometrics(), @@ -178,10 +183,14 @@ internal class WelcomeModel @Inject constructor( val unlockMethod = when (userWallet) { is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan - is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode + is UserWallet.Hot -> { + uiState.value = WelcomeUM.Empty + UserWalletsListRepository.UnlockMethod.AccessCode + } } unlockWallet(userWallet.walletId, unlockMethod) + setSelectWalletState() } private suspend fun canUnlockWithBiometrics(): Boolean { @@ -191,6 +200,7 @@ internal class WelcomeModel @Inject constructor( suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) { userWalletsListRepository.unlock(userWalletId, unlockMethod) .onRight { + routedOut = true userWalletsListRepository.select(userWalletId) router.replaceAll(AppRoute.Wallet) } @@ -199,7 +209,7 @@ internal class WelcomeModel @Inject constructor( } } - suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: () -> Unit = { }) { + suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: suspend () -> Unit = { }) { when (this) { UnlockWalletError.AlreadyUnlocked -> { // this should not happen, as we check for locked state before this diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt index 3427544768..04b1e81910 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/Welcome.kt @@ -35,6 +35,7 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) { state = st, modifier = modifier, ) + WelcomeUM.Empty -> {} } } } @@ -79,7 +80,8 @@ private fun Preview() { onClick = { currentState = when (currentState) { is WelcomeUM.Plain -> state - is WelcomeUM.SelectWallet -> WelcomeUM.Plain + is WelcomeUM.SelectWallet -> WelcomeUM.Empty + WelcomeUM.Empty -> WelcomeUM.Plain } }, ) { diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt index 390446ba73..c86bd0befa 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/state/WelcomeUM.kt @@ -9,6 +9,8 @@ import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class WelcomeUM { + data object Empty : WelcomeUM() + data object Plain : WelcomeUM() data class SelectWallet( From 1c192d52b20c68b94ffd5b03b42b276dd5639e4a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:25:47 +0300 Subject: [PATCH 52/87] Updated on 2026-08-14 --- .../data/walletmanager/WalletManagerFactory.kt | 4 ++-- .../derivations/MissedDerivationsFinder.kt | 9 ++------- .../domain/wallets/config/ColdCurvesConfig.kt | 18 ++++++++++++++++++ .../domain/wallets/config/CurvesConfig.kt | 18 ++++++++++++++++++ .../domain/wallets/config/HotCurvesConfig.kt | 15 +++++++++++++++ .../wallets/extension/UserWalletExtensions.kt | 7 ++----- 6 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index 2ab20f0a42..edd6da8af5 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,10 +7,10 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider import timber.log.Timber @@ -42,7 +42,7 @@ internal class WalletManagerFactory( blockchain: Blockchain, derivationPath: DerivationPath?, ): WalletManager? { - val curve = Wallet2CardConfig.primaryCurve(blockchain) + val curve = hotWallet.curvesConfig.primaryCurve(blockchain) val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } ?: return null return try { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index dffa80fd65..ce12ab7516 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -7,12 +7,11 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.card.configs.CardConfig -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.curvesConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.operations.derivation.ExtendedPublicKeysMap import kotlin.collections.forEach @@ -51,13 +50,9 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } private fun List.mapToNewDerivations(): List { - val config = when (userWallet) { - is UserWallet.Cold -> CardConfig.createConfig(userWallet.scanResponse.card) - is UserWallet.Hot -> Wallet2CardConfig // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet - } return mapNotNull { network -> val blockchain = network.toBlockchain() - val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null + val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null val walletPublicKey = when (userWallet) { is UserWallet.Cold -> { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt new file mode 100644 index 0000000000..103f85021a --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/ColdCurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO + +class ColdCurvesConfig(cardDTO: CardDTO) : CurvesConfig { + + val cardConfig = CardConfig.createConfig(cardDTO) + + override val mandatoryCurves: List + get() = cardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return cardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt new file mode 100644 index 0000000000..dcf753e3e3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/CurvesConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.models.wallet.UserWallet + +interface CurvesConfig { + + val mandatoryCurves: List + + fun primaryCurve(blockchain: Blockchain): EllipticCurve? +} + +val UserWallet.curvesConfig: CurvesConfig + get() = when (this) { + is UserWallet.Cold -> ColdCurvesConfig(this.scanResponse.card) + is UserWallet.Hot -> HotCurvesConfig + } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt new file mode 100644 index 0000000000..eec380f63d --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/config/HotCurvesConfig.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.wallets.config + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.configs.Wallet2CardConfig + +data object HotCurvesConfig : CurvesConfig { + + override val mandatoryCurves: List + get() = Wallet2CardConfig.mandatoryCurves + + override fun primaryCurve(blockchain: Blockchain): EllipticCurve? { + return Wallet2CardConfig.primaryCurve(blockchain) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt index 25d25bd977..74055cd741 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/extension/UserWalletExtensions.kt @@ -4,17 +4,14 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.card.common.util.hasDerivation -import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.models.wallet.UserWallet -import kotlin.collections.first -import kotlin.collections.orEmpty +import com.tangem.domain.wallets.config.curvesConfig fun UserWallet.hasDerivation(blockchain: Blockchain, derivationPath: String): Boolean { return when (this) { is UserWallet.Cold -> scanResponse.hasDerivation(blockchain, derivationPath) is UserWallet.Hot -> { - // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet - val primaryCurve = Wallet2CardConfig.primaryCurve(blockchain) + val primaryCurve = curvesConfig.primaryCurve(blockchain) val list = if (blockchain == Blockchain.Cardano) { listOf( CardanoUtils.extendedDerivationPath(DerivationPath(derivationPath)), From c8a163fd0a0b56f6021805a14dccf2c19f9655c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 13:32:41 +0300 Subject: [PATCH 53/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 590764ebe8..57e50cf4ac 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.27.0-1148" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-513" +tangemCardSdk = "releases-5.27.0-519" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From c6aa63a44f95eec0fc16f9a5f1cb0ab992d19144 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 15:55:14 +0500 Subject: [PATCH 54/87] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 13 ++- .../com/tangem/common/routing/AppRoute.kt | 5 + .../hotwallet/CreateWalletBackupComponent.kt | 14 +++ .../CreateWalletBackupModel.kt | 97 +++++++++++++++++++ .../CreateWalletBackupStepperStateManager.kt | 56 +++++++++++ .../DefaultCreateWalletBackupComponent.kt | 92 ++++++++++++++++++ .../di/CreateWalletBackupModule.kt | 42 ++++++++ .../routing/CreateWalletBackupChildFactory.kt | 47 +++++++++ .../routing/CreateWalletBackupRoute.kt | 19 ++++ .../ui/CreateWalletBackupContent.kt | 42 ++++++++ .../walletbackup/model/WalletBackupModel.kt | 3 +- .../walletsettings/entity/WalletSettingsUM.kt | 1 + .../model/WalletSettingsModel.kt | 37 ++++++- 13 files changed, 465 insertions(+), 3 deletions(-) create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 71f7c77d8b..ed41201e3f 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 @@ -17,9 +17,10 @@ import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.AddExistingWalletComponent import com.tangem.features.hotwallet.CreateMobileWalletComponent import com.tangem.features.hotwallet.WalletActivationComponent -import com.tangem.features.hotwallet.WalletBackupComponent +import com.tangem.features.hotwallet.CreateWalletBackupComponent import com.tangem.features.hotwallet.UpdateAccessCodeComponent import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource @@ -101,6 +102,7 @@ internal class ChildFactory @Inject constructor( private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, private val walletActivationComponentFactory: WalletActivationComponent.Factory, + private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory, private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, @@ -498,6 +500,15 @@ internal class ChildFactory @Inject constructor( componentFactory = walletActivationComponentFactory, ) } + is AppRoute.CreateWalletBackup -> { + createComponentChild( + context = context, + params = CreateWalletBackupComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = createWalletBackupComponentFactory, + ) + } is AppRoute.UpdateAccessCode -> { createComponentChild( context = context, 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 e0d6d80408..55cc7fca0d 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 @@ -308,6 +308,11 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, ) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}") + @Serializable + data class CreateWalletBackup( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}") + @Serializable data class UpdateAccessCode( val userWalletId: UserWalletId, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt new file mode 100644 index 0000000000..84d8828d8f --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/CreateWalletBackupComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface CreateWalletBackupComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt new file mode 100644 index 0000000000..8cc687e476 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupModel.kt @@ -0,0 +1,97 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push +import com.tangem.core.decompose.di.ModelScoped +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.models.wallet.UserWalletId +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ModelScoped +internal class CreateWalletBackupModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, +) : Model() { + + val params = paramsContainer.require() + + val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback() + val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks() + val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks() + val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks() + val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks() + + val stackNavigation = StackNavigation() + val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart + val currentRoute: MutableStateFlow = MutableStateFlow(startRoute) + + fun onBack() { + when (currentRoute.value) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop() + is CreateWalletBackupRoute.RecoveryPhrase -> stackNavigation.pop() + is CreateWalletBackupRoute.ConfirmBackup -> stackNavigation.pop() + is CreateWalletBackupRoute.BackupCompleted -> router.pop() + } + } + + fun onManualBackupStarted() { + stackNavigation.push(CreateWalletBackupRoute.RecoveryPhrase) + } + + fun onManualBackupPhraseShown() { + stackNavigation.push(CreateWalletBackupRoute.ConfirmBackup) + } + + fun onManualBackupChecked() { + stackNavigation.push(CreateWalletBackupRoute.BackupCompleted) + } + + fun onManualBackupCompleted() { + router.pop() + } + + inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback { + override fun onBackClick() { + onBack() + } + + override fun onSkipClick() = Unit + } + + inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupStarted() + } + } + + inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks { + override fun onContinueClick() { + onManualBackupPhraseShown() + } + } + + inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks { + override fun onCompleteClick() { + onManualBackupChecked() + } + } + + inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks { + override fun onContinueClick(userWalletId: UserWalletId) { + onManualBackupCompleted() + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt new file mode 100644 index 0000000000..5b8c3a2e68 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/CreateWalletBackupStepperStateManager.kt @@ -0,0 +1,56 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.impl.R +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import javax.inject.Inject + +internal class CreateWalletBackupStepperStateManager @Inject constructor() { + + fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? { + return when (route) { + is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_START, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_PHRASE, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_CONFIRM, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_backup), + showBackButton = true, + showSkipButton = false, + showFeedbackButton = true, + ) + is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM( + currentStep = STEP_COMPLETED, + steps = STEPS_COUNT, + title = resourceReference(R.string.common_done), + showBackButton = false, + showSkipButton = false, + showFeedbackButton = false, + ) + } + } + + companion object { + private const val STEPS_COUNT = 4 + + private const val STEP_START = 1 + private const val STEP_PHRASE = 2 + private const val STEP_CONFIRM = 3 + private const val STEP_COMPLETED = 4 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt new file mode 100644 index 0000000000..b13c9193f9 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/DefaultCreateWalletBackupComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.hotwallet.createwalletbackup + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.ObserveLifecycleMode +import com.arkivanov.decompose.value.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory +import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent +import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultCreateWalletBackupComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: CreateWalletBackupComponent.Params, + private val stepperStateManager: CreateWalletBackupStepperStateManager, + createWalletBackupChildFactory: CreateWalletBackupChildFactory, + stepperComponentFactory: DefaultHotWalletStepperComponent.Factory, +) : CreateWalletBackupComponent, AppComponentContext by appComponentContext { + + private val model: CreateWalletBackupModel = getOrCreateModel(params) + + private val innerStack = childStack( + key = "createWalletBackupInnerStack", + source = model.stackNavigation, + serializer = null, + initialConfiguration = model.startRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createWalletBackupChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + model = model, + ) + }, + ) + + private val stepperComponent = stepperComponentFactory.create( + context = this, + params = HotWalletStepperComponent.Params( + initState = HotWalletStepperComponent.StepperUM.initialState(), + callback = model.hotWalletStepperComponentModelCallback, + ), + ) + + init { + innerStack.subscribe( + lifecycle = lifecycle, + mode = ObserveLifecycleMode.CREATE_DESTROY, + ) { stack -> + componentScope.launch { + model.currentRoute.emit(stack.active.configuration) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val stackState by innerStack.subscribeAsState() + val currentRoute = stackState.active.configuration + + BackHandler(onBack = model::onBack) + + val stepperState = stepperStateManager.getStepperState(currentRoute) + stepperState?.let { stepperComponent.updateState(it) } + + CreateWalletBackupContent( + stackState = stackState, + stepperComponent = stepperComponent.takeIf { stepperState != null }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : CreateWalletBackupComponent.Factory { + override fun create( + context: AppComponentContext, + params: CreateWalletBackupComponent.Params, + ): DefaultCreateWalletBackupComponent + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt new file mode 100644 index 0000000000..5ea687703d --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/di/CreateWalletBackupModule.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.hotwallet.CreateWalletBackupComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager +import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CreateWalletBackupModuleBinds { + + @Binds + @Singleton + fun bindCreateWalletBackupComponentFactory( + impl: DefaultCreateWalletBackupComponent.Factory, + ): CreateWalletBackupComponent.Factory + + @Binds + @IntoMap + @ClassKey(CreateWalletBackupModel::class) + fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal object CreateWalletBackupModule { + + @Provides + @Singleton + fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager { + return CreateWalletBackupStepperStateManager() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt new file mode 100644 index 0000000000..44d740af6c --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupChildFactory.kt @@ -0,0 +1,47 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel +import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent +import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent +import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent +import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent +import javax.inject.Inject + +internal class CreateWalletBackupChildFactory @Inject constructor() { + + fun createChild( + route: CreateWalletBackupRoute, + childContext: AppComponentContext, + model: CreateWalletBackupModel, + ): ComposableContentComponent = when (route) { + CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent( + context = childContext, + params = ManualBackupStartComponent.Params( + callbacks = model.manualBackupStartModelCallbacks, + ), + ) + CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent( + context = childContext, + params = ManualBackupPhraseComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupPhraseModelCallbacks, + ), + ) + CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent( + context = childContext, + params = ManualBackupCheckComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCheckModelCallbacks, + ), + ) + CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent( + context = childContext, + params = ManualBackupCompletedComponent.Params( + userWalletId = model.params.userWalletId, + callbacks = model.manualBackupCompletedModelCallbacks, + ), + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt new file mode 100644 index 0000000000..f063a1f796 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/routing/CreateWalletBackupRoute.kt @@ -0,0 +1,19 @@ +package com.tangem.features.hotwallet.createwalletbackup.routing + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface CreateWalletBackupRoute { + + @Serializable + data object RecoveryPhraseStart : CreateWalletBackupRoute + + @Serializable + data object RecoveryPhrase : CreateWalletBackupRoute + + @Serializable + data object ConfirmBackup : CreateWalletBackupRoute + + @Serializable + data object BackupCompleted : CreateWalletBackupRoute +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt new file mode 100644 index 0000000000..e6f1e9e732 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createwalletbackup/ui/CreateWalletBackupContent.kt @@ -0,0 +1,42 @@ +package com.tangem.features.hotwallet.createwalletbackup.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute +import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent + +@Composable +internal fun CreateWalletBackupContent( + stackState: ChildStack, + stepperComponent: HotWalletStepperComponent?, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.primary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + ) { + stepperComponent?.Content(Modifier) + + Children( + stack = stackState, + animation = stackAnimation(slide()), + modifier = Modifier.fillMaxSize(), + ) { + it.instance.Content(Modifier.fillMaxSize()) + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 37c2dd7948..b398ed047a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.common.routing.AppRoute import com.tangem.features.hotwallet.WalletBackupComponent import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -66,8 +67,8 @@ internal class WalletBackupModel @Inject constructor( secondaryButton { text = resourceReference(R.string.hw_backup_need_action) onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) closeBs() - // TODO [REDACTED_TASK_KEY] } } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt index 3cadad2268..39bd16ae56 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -9,4 +9,5 @@ internal data class WalletSettingsUM( val items: PersistentList, val requestPushNotificationsPermission: Boolean = false, val onPushNotificationPermissionGranted: (Boolean) -> Unit, + val isWalletBackedUp: Boolean = true, ) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 22dd035507..f9d0b633b2 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -15,10 +15,16 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 +import com.tangem.core.ui.components.bottomsheets.message.icon +import com.tangem.core.ui.components.bottomsheets.message.infoBlock +import com.tangem.core.ui.components.bottomsheets.message.onClick +import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO @@ -86,9 +92,29 @@ internal class WalletSettingsModel @Inject constructor( items = persistentListOf(), requestPushNotificationsPermission = false, onPushNotificationPermissionGranted = ::onPushNotificationPermissionGranted, + isWalletBackedUp = true, ), ) + private val makeBackupAtFirstAlertBS + get() = bottomSheetMessage { + infoBlock { + icon(R.drawable.ic_passcode_lock_32) { + type = MessageBottomSheetUMV2.Icon.Type.Accent + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.hw_backup_need_title) + body = resourceReference(R.string.hw_backup_need_description) + } + secondaryButton { + text = resourceReference(R.string.hw_backup_need_action) + onClick { + router.push(AppRoute.CreateWalletBackup(params.userWalletId)) + closeBs() + } + } + } + init { combine( getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(), @@ -97,6 +123,10 @@ internal class WalletSettingsModel @Inject constructor( ) { maybeWallet, nftEnabled, notificationsEnabled -> val wallet = maybeWallet.getOrNull() ?: return@combine val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase() + val isWalletBackedUp = when (wallet) { + is UserWallet.Hot -> wallet.backedUp + is UserWallet.Cold -> true + } val isNeedShowNotifications = notificationsToggles.isNotificationsEnabled && !getIsHuaweiDeviceWithoutGoogleServicesUseCase() state.update { value -> @@ -110,6 +140,7 @@ internal class WalletSettingsModel @Inject constructor( isNotificationsFeatureEnabled = isNeedShowNotifications, isNotificationsPermissionGranted = isNotificationsPermissionGranted(), ), + isWalletBackedUp = isWalletBackedUp, ) } } @@ -330,6 +361,10 @@ internal class WalletSettingsModel @Inject constructor( } private fun onAccessCodeClick() { - router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + if (!state.value.isWalletBackedUp) { + messageSender.send(makeBackupAtFirstAlertBS) + } else { + router.push(AppRoute.UpdateAccessCode(params.userWalletId)) + } } } \ No newline at end of file From 65ace6c5f2ff48ca098d5f7918a5b32b8a6ed975 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 14:15:30 +0700 Subject: [PATCH 55/87] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 6 +++ .../ethereum/WcEthAddNetworkUseCase.kt | 44 ++++++++++++++++ .../network/ethereum/WcEthNetwork.kt | 35 ++++++------- .../ethereum/WcEthSwitchNetworkUseCase.kt | 51 +++++++++++++++++++ .../request/DefaultWcRequestService.kt | 1 + .../utils/WcNetworksConverter.kt | 5 ++ .../domain/walletconnect/model/WcEthMethod.kt | 7 +-- .../walletconnect/model/WcMethodName.kt | 1 + .../walletconnect/model/WcRequestError.kt | 5 ++ .../usecase/method/WcAddNetworkUseCase.kt | 7 +++ .../usecase/method/WcSwitchNetworkUseCase.kt | 17 +++++++ .../connections/components/AlertsComponent.kt | 18 +++++++ .../routing/DefaultWcRoutingComponent.kt | 28 +++++++++- .../connections/routing/WcInnerRoute.kt | 12 +++++ .../connections/routing/WcRoutingModel.kt | 2 + .../connections/ui/AlertsModalBottomSheet.kt | 42 +++++++++++++-- .../di/WalletConnectModelModule.kt | 6 +++ .../chain/WcSwitchNetworkComponent.kt | 21 ++++++++ .../converter/WcHandleMethodErrorConverter.kt | 4 ++ .../transaction/model/WcAddNetworkModel.kt | 31 ++++++----- .../transaction/model/WcSwitchNetworkModel.kt | 45 ++++++++++++++++ 21 files changed, 349 insertions(+), 39 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcSwitchNetworkComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index c81754117c..ec20bf2c8f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1505,12 +1505,16 @@ Enable Trustline A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. Trustline Required + The required network %s is not added to your portfolio. Add it first, then proceed with the connection. + Add network to portfolio Malicious domain Unknown domain Connect anyway Timeout error. Please, try again later. Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. + To continue, please reconnect your dApp session with the required network %s. + Network not connected Please return to your browser and reconnect via WalletConnect. Wallet Connect session was disconnected Sign anyway @@ -1521,6 +1525,8 @@ Unsupported dApp Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error + This network %s is not supported by Tangem Wallet and cannot be connected. + Unsupported network Tangem does not currently support a required network by %s. Unsupported networks This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index 0bc201fd7a..5c988f8a8e 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -1,9 +1,17 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.extensions.hexToInt +import com.tangem.data.walletconnect.model.CAIP2 +import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork.NamespaceConverter.Companion.ETH_NAMESPACE_KEY import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.walletconnect.model.HandleMethodError import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession @@ -16,6 +24,7 @@ import dagger.assisted.AssistedInject internal class WcEthAddNetworkUseCase @AssistedInject constructor( private val respondService: WcRespondService, + addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory, @Assisted val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.AddEthereumChain, ) : WcAddNetworkUseCase { @@ -31,6 +40,14 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( else -> WcNetworkDerivationState.Single } + private val addSwitchCommonDelegate = addSwitchCommonDelegateFactory.create(context) + + override suspend fun invoke(): Either { + return addSwitchCommonDelegate + .commonChecks(method.rawChain.chainId) + .map { addedNetwork -> WcAddNetworkUseCase.AddNetwork(addedNetwork) } + } + override suspend fun approve(): Either { return respondService.respond(rawSdkRequest, "") } @@ -43,4 +60,31 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( interface Factory { fun create(context: WcMethodUseCaseContext, method: WcEthMethod.AddEthereumChain): WcEthAddNetworkUseCase } +} + +internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor( + private val networksConverter: WcNetworksConverter, + @Assisted val context: WcMethodUseCaseContext, +) { + + private val wallet: UserWallet get() = context.session.wallet + + suspend fun commonChecks(hexChainId: String): Either { + val caip2 = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${hexChainId.hexToInt()}") + ?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left() + val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet) + if (generalNetwork == null) { + return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() + } + val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(caip2.raw, wallet) + if (addedNetwork == null) { + return HandleMethodError.NotAddedNetwork(generalNetwork.name).left() + } + return addedNetwork.right() + } + + @AssistedFactory + interface Factory { + fun create(context: WcMethodUseCaseContext): WcEthAddSwitchCommonDelegate + } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index fe375b57d6..a14460a566 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -6,17 +6,14 @@ import arrow.core.left import arrow.core.right import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.extensions.hexToInt import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey -import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork.NamespaceConverter.Companion.ETH_NAMESPACE_KEY import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.data.walletconnect.utils.WcNetworksConverter -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -46,7 +43,7 @@ internal class WcEthNetwork( ?: return HandleMethodError.UnknownSession.left() val wallet = session.wallet val chainId = request.chainId.orEmpty() - val method: WcEthMethod = name.toMethod(request, wallet) + val method: WcEthMethod = name.toMethod(request) .getOrElse { return error(it.message.orEmpty()) } ?: return error("Failed to parse $name") suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) @@ -56,7 +53,9 @@ internal class WcEthNetwork( is WcEthMethod.SendTransaction -> method.transaction.from is WcEthMethod.SignTransaction -> method.transaction.from is WcEthMethod.SignTypedData -> method.account - is WcEthMethod.AddEthereumChain -> + is WcEthMethod.AddEthereumChain, + is WcEthMethod.SwitchEthereumChain, + -> anyExistNetwork() ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } .orEmpty() @@ -67,7 +66,9 @@ internal class WcEthNetwork( is WcEthMethod.SendTransaction, is WcEthMethod.SignTransaction, -> networksConverter.findWalletNetworkForRequest(request, session, accountAddress) - is WcEthMethod.AddEthereumChain -> anyExistNetwork() + is WcEthMethod.AddEthereumChain, + is WcEthMethod.SwitchEthereumChain, + -> anyExistNetwork() } ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") val context = WcMethodUseCaseContext( @@ -83,13 +84,11 @@ internal class WcEthNetwork( is WcEthMethod.SignTransaction -> factories.signTransaction.create(context, method) is WcEthMethod.SignTypedData -> factories.signTypedData.create(context, method) is WcEthMethod.AddEthereumChain -> factories.addNetwork.create(context, method) + is WcEthMethod.SwitchEthereumChain -> factories.switchNetwork.create(context, method) }.right() } - private suspend fun WcEthMethodName.toMethod( - request: WcSdkSessionRequest, - wallet: UserWallet, - ): Either { + private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest): Either { val rawParams = request.request.params return when (this) { WcEthMethodName.EthSign, @@ -111,16 +110,17 @@ internal class WcEthNetwork( } } ?: return null.right() - WcEthMethodName.AddEthereumChain -> moshi.fromJson>(rawParams) + WcEthMethodName.AddEthereumChain, + WcEthMethodName.SwitchEthereumChain, + -> moshi.fromJson>(rawParams) .getOrElse { return it.left() } ?.firstOrNull() ?.let { - val caip2 = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${it.chainId.hexToInt()}") - ?: return null.right() - val newNetwork = networksConverter - .mainOrAnyWalletNetworkForRequest(caip2.raw, wallet) - ?: return null.right() - WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork).right() + if (this == WcEthMethodName.AddEthereumChain) { + WcEthMethod.AddEthereumChain(rawChain = it).right() + } else { + WcEthMethod.SwitchEthereumChain(rawChain = it).right() + } } ?: null.right() } @@ -170,5 +170,6 @@ internal class WcEthNetwork( val sendTransaction: WcEthSendTransactionUseCase.Factory, val signTransaction: WcEthSignTransactionUseCase.Factory, val addNetwork: WcEthAddNetworkUseCase.Factory, + val switchNetwork: WcEthSwitchNetworkUseCase.Factory, ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt new file mode 100644 index 0000000000..0835594dfe --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt @@ -0,0 +1,51 @@ +package com.tangem.data.walletconnect.network.ethereum + +import arrow.core.Either +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.HandleMethodError +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState +import com.tangem.domain.walletconnect.usecase.method.WcSwitchNetworkUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class WcEthSwitchNetworkUseCase @AssistedInject constructor( + private val respondService: WcRespondService, + @Assisted val context: WcMethodUseCaseContext, + @Assisted override val method: WcEthMethod.SwitchEthereumChain, + addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory, +) : WcSwitchNetworkUseCase { + + override val session: WcSession + get() = context.session + override val rawSdkRequest: WcSdkSessionRequest + get() = context.rawSdkRequest + override val network: Network + get() = context.network + override val derivationState: WcNetworkDerivationState = when { + context.networkDerivationsCount > 1 -> WcNetworkDerivationState.Multiple(walletAddress = context.accountAddress) + else -> WcNetworkDerivationState.Single + } + + private val addSwitchCommonDelegate = addSwitchCommonDelegateFactory.create(context) + + override suspend fun invoke(): Either { + return addSwitchCommonDelegate + .commonChecks(method.rawChain.chainId) + .map { addedNetwork -> WcSwitchNetworkUseCase.SwitchNetwork(addedNetwork) } + } + + override fun reject() { + respondService.rejectRequestNonBlock(rawSdkRequest) + } + + @AssistedFactory + interface Factory { + fun create(context: WcMethodUseCaseContext, method: WcEthMethod.SwitchEthereumChain): WcEthSwitchNetworkUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index 4c50b189fd..db8788c05a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -33,6 +33,7 @@ internal class DefaultWcRequestService( Timber.tag(WC_TAG).i("handle request name $name") if (name is WcMethodName.Unsupported) { respondService.rejectRequestNonBlock(sr) + if (name.raw.startsWith("wallet_")) return } _wcRequest.trySend(name to sr) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 6a80763cc9..4b49445c7f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -25,6 +25,11 @@ internal class WcNetworksConverter @Inject constructor( private val tokensFeatureToggles: TokensFeatureToggles, ) { + fun createNetwork(chainId: String, wallet: UserWallet): Network? { + return namespaceConverters + .firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } + } + suspend fun findWalletNetworkForRequest( request: WcSdkSessionRequest, session: WcSession, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt index afb348e77e..34b43e11bb 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthMethod.kt @@ -1,7 +1,5 @@ package com.tangem.domain.walletconnect.model -import com.tangem.domain.models.network.Network - sealed interface WcEthMethod : WcMethod { data class MessageSign( @@ -28,6 +26,9 @@ sealed interface WcEthMethod : WcMethod { data class AddEthereumChain( val rawChain: WcEthAddChain, - val network: Network, + ) : WcEthMethod + + data class SwitchEthereumChain( + val rawChain: WcEthAddChain, ) : WcEthMethod } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt index 6ae4b431ac..982c1cacf1 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt @@ -14,6 +14,7 @@ enum class WcEthMethodName(override val raw: String) : WcMethodName { SignTransaction("eth_signTransaction"), SendTransaction("eth_sendTransaction"), AddEthereumChain("wallet_addEthereumChain"), + SwitchEthereumChain("wallet_switchEthereumChain"), } enum class WcSolanaMethodName(override val raw: String) : WcMethodName { diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt index 3c1cfe610a..e0ad853f77 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt @@ -69,4 +69,9 @@ sealed class HandleMethodError( data object UnknownSession : HandleMethodError(message = "WalletConnect session was disconnected") data class UnknownError(override val message: String) : HandleMethodError(message) + data class TangemUnsupportedNetwork(val unsupportedNetwork: String) : + HandleMethodError("TangemUnsupportedNetwork $unsupportedNetwork") + + data class NotAddedNetwork(val networkName: String) : HandleMethodError("NotAddedNetwork $networkName") + data class RequiredNetwork(val networkName: String) : HandleMethodError("RequiredNetwork $networkName") } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt index 787bea6e93..35223ba040 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt @@ -1,12 +1,19 @@ package com.tangem.domain.walletconnect.usecase.method import arrow.core.Either +import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.HandleMethodError import com.tangem.domain.walletconnect.model.WcRequestError interface WcAddNetworkUseCase : WcMethodUseCase, WcMethodContext { + suspend operator fun invoke(): Either suspend fun approve(): Either fun reject() + + data class AddNetwork( + val network: Network, + ) } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt new file mode 100644 index 0000000000..5c2066049a --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletconnect.usecase.method + +import arrow.core.Either +import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.HandleMethodError + +interface WcSwitchNetworkUseCase : + WcMethodUseCase, + WcMethodContext { + + suspend operator fun invoke(): Either + fun reject() + + data class SwitchNetwork( + val network: Network, + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt index 6fd6fde73b..fd682083d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/AlertsComponent.kt @@ -38,5 +38,23 @@ internal class AlertsComponent( @Serializable data class WcDisconnected(override val onDismiss: () -> Unit) : AlertType() + + @Serializable + data class TangemUnsupportedNetwork( + val network: String, + override val onDismiss: () -> Unit, + ) : AlertType() + + @Serializable + data class RequiredAddNetwork( + val network: String, + override val onDismiss: () -> Unit, + ) : AlertType() + + @Serializable + data class RequiredReconnectWithNetwork( + val network: String, + override val onDismiss: () -> Unit, + ) : AlertType() } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 42b336932f..30c6d10c14 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -18,8 +18,10 @@ import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.FeeSelectorComponent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.connections.components.AlertsComponent +import com.tangem.features.walletconnect.connections.components.AlertsComponent.AlertType.* import com.tangem.features.walletconnect.connections.components.WcPairComponent import com.tangem.features.walletconnect.transaction.components.chain.WcAddNetworkContainerComponent +import com.tangem.features.walletconnect.transaction.components.chain.WcSwitchNetworkComponent import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.send.WcSendTransactionContainerComponent import com.tangem.features.walletconnect.transaction.components.sign.WcSignTransactionContainerComponent @@ -71,6 +73,10 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), ) + is WcInnerRoute.SwitchNetwork -> WcSwitchNetworkComponent( + appComponentContext = childContext, + params = WcTransactionModelParams(config.rawRequest), + ) is WcInnerRoute.Send -> WcSendTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), @@ -88,13 +94,31 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( is WcInnerRoute.UnsupportedMethodAlert -> AlertsComponent( childContext, AlertsComponent.Params( - alertType = AlertsComponent.AlertType.UnsupportedMethod { model.innerRouter.pop() }, + alertType = UnsupportedMethod { model.innerRouter.pop() }, ), ) is WcInnerRoute.WcDappDisconnected -> AlertsComponent( childContext, AlertsComponent.Params( - alertType = AlertsComponent.AlertType.WcDisconnected { model.innerRouter.pop() }, + alertType = WcDisconnected { model.innerRouter.pop() }, + ), + ) + is WcInnerRoute.TangemUnsupportedNetwork -> AlertsComponent( + childContext, + AlertsComponent.Params( + alertType = TangemUnsupportedNetwork(config.networkName) { model.innerRouter.pop() }, + ), + ) + is WcInnerRoute.RequiredAddNetwork -> AlertsComponent( + childContext, + AlertsComponent.Params( + alertType = RequiredAddNetwork(config.networkName) { model.innerRouter.pop() }, + ), + ) + is WcInnerRoute.RequiredReconnectWithNetwork -> AlertsComponent( + childContext, + AlertsComponent.Params( + alertType = RequiredReconnectWithNetwork(config.networkName) { model.innerRouter.pop() }, ), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt index 4cabfc0547..bc9f386e88 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcInnerRoute.kt @@ -22,6 +22,9 @@ internal sealed interface WcInnerRoute : Route { @Serializable data class AddNetwork(override val rawRequest: WcSdkSessionRequest) : Method + @Serializable + data class SwitchNetwork(override val rawRequest: WcSdkSessionRequest) : Method + @Serializable data class Pair(val request: WcPairRequest) : WcInnerRoute @@ -30,4 +33,13 @@ internal sealed interface WcInnerRoute : Route { @Serializable data object WcDappDisconnected : WcInnerRoute + + @Serializable + data class TangemUnsupportedNetwork(val networkName: String) : WcInnerRoute + + @Serializable + data class RequiredAddNetwork(val networkName: String) : WcInnerRoute + + @Serializable + data class RequiredReconnectWithNetwork(val networkName: String) : WcInnerRoute } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt index 3c320faab7..5524f954e3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/WcRoutingModel.kt @@ -51,6 +51,8 @@ internal class WcRoutingModel @Inject constructor( -> WcInnerRoute.SignMessage(rawRequest) WcEthMethodName.AddEthereumChain, -> WcInnerRoute.AddNetwork(rawRequest) + WcEthMethodName.SwitchEthereumChain, + -> WcInnerRoute.SwitchNetwork(rawRequest) WcEthMethodName.SignTransaction, WcEthMethodName.SendTransaction, WcSolanaMethodName.SignTransaction, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt index 7c93a184f5..c7ff306829 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt @@ -87,7 +87,11 @@ private fun ButtonsContainer(alert: AlertsComponent.AlertType, modifier: Modifie ) { val buttonsModifier = Modifier.fillMaxWidth() when (alert) { - is AlertsComponent.AlertType.UnsupportedMethod -> SecondaryButton( + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> SecondaryButton( modifier = buttonsModifier, onClick = alert.onDismiss, text = stringResourceSafe(R.string.balance_hidden_got_it_button), @@ -105,16 +109,28 @@ private fun ButtonsContainer(alert: AlertsComponent.AlertType, modifier: Modifie private fun AlertIcon(alert: AlertsComponent.AlertType, modifier: Modifier = Modifier) { val color = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> TangemTheme.colors.icon.informative - is AlertsComponent.AlertType.UnsupportedMethod -> TangemTheme.colors.icon.attention + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> TangemTheme.colors.icon.attention } @DrawableRes val drawableId = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> R.drawable.ic_wallet_connect_24 - is AlertsComponent.AlertType.UnsupportedMethod -> R.drawable.img_attention_20 + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> R.drawable.img_attention_20 } val iconTint = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> color - is AlertsComponent.AlertType.UnsupportedMethod -> Color.Unspecified + is AlertsComponent.AlertType.UnsupportedMethod, + is AlertsComponent.AlertType.RequiredAddNetwork, + is AlertsComponent.AlertType.RequiredReconnectWithNetwork, + is AlertsComponent.AlertType.TangemUnsupportedNetwork, + -> Color.Unspecified } Box( modifier = modifier @@ -138,6 +154,9 @@ private fun AlertContentTitle(alert: AlertsComponent.AlertType, modifier: Modifi @StringRes val titleRes: Int = when (alert) { is AlertsComponent.AlertType.WcDisconnected -> R.string.wc_alert_session_disconnected_title is AlertsComponent.AlertType.UnsupportedMethod -> R.string.wc_alert_unsupported_method_title + is AlertsComponent.AlertType.RequiredAddNetwork -> R.string.wc_alert_add_network_to_portfolio_title + is AlertsComponent.AlertType.RequiredReconnectWithNetwork -> R.string.wc_alert_network_not_connected_title + is AlertsComponent.AlertType.TangemUnsupportedNetwork -> R.string.wc_alert_unsupported_network_title } Text( modifier = modifier, @@ -157,6 +176,18 @@ private fun AlertContentDescription(alert: AlertsComponent.AlertType, modifier: is AlertsComponent.AlertType.UnsupportedMethod -> stringResourceSafe( R.string.wc_alert_unsupported_method_description, ) + is AlertsComponent.AlertType.RequiredAddNetwork -> stringResourceSafe( + R.string.wc_alert_unsupported_method_description, + alert.network, + ) + is AlertsComponent.AlertType.RequiredReconnectWithNetwork -> stringResourceSafe( + R.string.wc_alert_network_not_connected_description, + alert.network, + ) + is AlertsComponent.AlertType.TangemUnsupportedNetwork -> stringResourceSafe( + R.string.wc_alert_unsupported_network_description, + alert.network, + ) } Text( modifier = modifier, @@ -188,5 +219,8 @@ private class AlertTypesProvider : CollectionPreviewParameterProvider WcInnerRoute.UnsupportedMethodAlert HandleMethodError.UnknownSession -> WcInnerRoute.WcDappDisconnected + + is HandleMethodError.NotAddedNetwork -> WcInnerRoute.RequiredAddNetwork(value.networkName) + is HandleMethodError.RequiredNetwork -> WcInnerRoute.RequiredReconnectWithNetwork(value.networkName) + is HandleMethodError.TangemUnsupportedNetwork -> WcInnerRoute.TangemUnsupportedNetwork(value.unsupportedNetwork) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 107e983abc..f85c7a6003 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -57,22 +57,27 @@ internal class WcAddNetworkModel @Inject constructor( .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } .getOrNull() ?: return@launch sendSignatureReceivedAnalytics(useCase) - _uiState.emit( - wcAddEthereumChainUMConverter.convert( - WcAddEthereumChainUMConverter.Input( - useCase = useCase, - actions = WcTransactionActionsUM( - onShowVerifiedAlert = ::showVerifiedAlert, - onDismiss = { cancel(useCase) }, - onSign = { sign(useCase) }, - onCopy = { copyData(useCase.rawSdkRequest.request.params) }, - ), - ), - ), - ) + val either = useCase.invoke() + either + .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } + .map { showUI() } } } + private fun showUI() { + _uiState.value = wcAddEthereumChainUMConverter.convert( + WcAddEthereumChainUMConverter.Input( + useCase = useCase, + actions = WcTransactionActionsUM( + onShowVerifiedAlert = ::showVerifiedAlert, + onDismiss = { cancel(useCase) }, + onSign = { sign(useCase) }, + onCopy = { copyData(useCase.rawSdkRequest.request.params) }, + ), + ), + ) + } + override fun dismiss() { _uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop() } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt new file mode 100644 index 0000000000..65b63ae8b6 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt @@ -0,0 +1,45 @@ +package com.tangem.features.walletconnect.transaction.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +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.WcRequestUseCaseFactory +import com.tangem.domain.walletconnect.model.HandleMethodError +import com.tangem.domain.walletconnect.usecase.method.WcSwitchNetworkUseCase +import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams +import com.tangem.features.walletconnect.transaction.converter.WcHandleMethodErrorConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class WcSwitchNetworkModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val useCaseFactory: WcRequestUseCaseFactory, +) : Model() { + + private val params = paramsContainer.require() + + init { + modelScope.launch { + val useCase = useCaseFactory.createUseCase(params.rawRequest) + .onLeft { showErrorDialog(it) } + .getOrNull() ?: return@launch + val either = useCase.invoke() + useCase.reject() + either + .onLeft { showErrorDialog(it) } + .map { showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) } + } + } + + private fun showErrorDialog(error: HandleMethodError) { + router.push(WcHandleMethodErrorConverter.convert(error)) + } +} \ No newline at end of file From 99a5efd7e2deb0b017a8173005a208d1ad425aa6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 14:59:00 +0700 Subject: [PATCH 56/87] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 ++ .../pair/DefaultWcPairUseCase.kt | 22 +++++++++++++------ .../walletconnect/pair/WcPairSdkDelegate.kt | 2 +- .../domain/walletconnect/model/WcPairError.kt | 1 + .../connections/components/WcPairComponent.kt | 1 + .../connections/model/WcPairModel.kt | 17 +++++--------- .../connections/routes/WcAppInfoRoutes.kt | 1 + .../connections/utils/WcAlertsFactory.kt | 17 ++++++++++++++ 8 files changed, 43 insertions(+), 20 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ec20bf2c8f..f9238788d1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1515,6 +1515,8 @@ This domain cannot be verified. Check the request carefully approving. To continue, please reconnect your dApp session with the required network %s. Network not connected + Check your network connection + Request timeout Please return to your browser and reconnect via WalletConnect. Wallet Connect session was disconnected Sign anyway diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 0e78ed19ce..ad67584bed 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -20,6 +20,7 @@ import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime @@ -119,14 +120,21 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") } emit(WcPairState.Approving.Result(sessionForApprove, either)) - }.onCompletion { - if (it != null) { - Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") - emit(WcPairState.Error(WcPairError.Unknown(it.message.orEmpty()))) - } else { - Timber.tag(WC_TAG).i("Completed successfully $pairRequest") - } } + .catch { + val pairError: WcPairError = when (it) { + is TimeoutCancellationException -> WcPairError.TimeoutException(it.message.orEmpty()) + else -> WcPairError.Unknown(it.message.orEmpty()) + } + emit(WcPairState.Error(pairError)) + } + .onCompletion { + if (it != null) { + Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") + } else { + Timber.tag(WC_TAG).i("Completed successfully $pairRequest") + } + } } override fun approve(sessionForApprove: WcSessionApprove) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index e9665c0673..35902fb41f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -150,7 +150,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left() companion object { - private const val CALLBACK_TIMEOUT = 60 + private const val CALLBACK_TIMEOUT = 15 // com.reown.android.pairing.engine.domain.PairingEngine.pair private val pairingExpiredMessages = listOf( "Pairing URI expired", diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt index f04de6251a..2ac1b68531 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -16,4 +16,5 @@ sealed class WcPairError( data class ApprovalFailed(override val message: String) : WcPairError("107 002 003") data object RejectionFailed : WcPairError("107 002 004") data class Unknown(override val message: String) : WcPairError(message) + data class TimeoutException(override val message: String) : WcPairError(message) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index b41e493f67..afa23ea711 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -121,6 +121,7 @@ internal class WcPairComponent( is Alert.Type.UnsupportedNetwork -> WcAlertsFactory.createUnsupportedChainAlert(alertType.appName, model::errorAlertOnDismiss) is Alert.Type.UriAlreadyUsed -> WcAlertsFactory.createUriAlreadyUsedAlert(model::errorAlertOnDismiss) + is Alert.Type.TimeoutException -> WcAlertsFactory.createTimeoutExceptionAlert(model::errorAlertOnDismiss) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 7c566be7c6..fe64931b39 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -216,18 +216,11 @@ internal class WcPairModel @Inject constructor( private fun processError(error: WcPairError) { val alert = when (error) { - is WcPairError.InvalidDomainURL -> { - WcAppInfoRoutes.Alert.Type.InvalidDomain - } - is WcPairError.UnsupportedDApp -> { - WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName) - } - is WcPairError.UnsupportedBlockchains -> { - WcAppInfoRoutes.Alert.Type.UnsupportedNetwork(error.appName) - } - is WcPairError.UriAlreadyUsed -> { - WcAppInfoRoutes.Alert.Type.UriAlreadyUsed - } + is WcPairError.InvalidDomainURL -> WcAppInfoRoutes.Alert.Type.InvalidDomain + is WcPairError.UnsupportedDApp -> WcAppInfoRoutes.Alert.Type.UnsupportedDApp(error.appName) + is WcPairError.UnsupportedBlockchains -> WcAppInfoRoutes.Alert.Type.UnsupportedNetwork(error.appName) + is WcPairError.UriAlreadyUsed -> WcAppInfoRoutes.Alert.Type.UriAlreadyUsed + is WcPairError.TimeoutException -> WcAppInfoRoutes.Alert.Type.TimeoutException else -> { messageSender.send(ToastMessage(message = stringReference(error.message))) router.pop() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index 226ececbbc..784e0457ed 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -36,6 +36,7 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { data class UnsupportedDApp(val appName: String) : Type() data class UnsupportedNetwork(val appName: String) : Type() data object UriAlreadyUsed : Type() + data object TimeoutException : Type() } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt index a17284a34d..5ac9d638e6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt @@ -127,6 +127,23 @@ internal object WcAlertsFactory { } } + fun createTimeoutExceptionAlert(onDismiss: () -> Unit): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.ic_wallet_connect_24) { + type = Type.Informative + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.wc_alert_request_timeout_title) + body = resourceReference(R.string.wc_alert_request_timeout_description) + } + primaryButton { + text = resourceReference(R.string.common_got_it) + onClick { onDismiss() } + } + } + } + fun createUnsupportedChainAlert(appName: String, onDismiss: () -> Unit): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { From 00171d2276daf315c241bb65d3570ce36ddc5ae9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 18:01:55 +0700 Subject: [PATCH 57/87] Updated on 2026-08-14 --- .../ethereum/WcEthAddNetworkUseCase.kt | 62 ++++++++++++++++++- .../ethereum/WcEthSwitchNetworkUseCase.kt | 7 ++- .../utils/WcNetworksConverter.kt | 5 ++ .../utils/WcSdkSessionConverter.kt | 8 +++ .../WcSignUseCaseDelegateTest.kt | 1 + .../model/sdkcopy/WcSdkSession.kt | 10 ++- .../usecase/method/WcAddNetworkUseCase.kt | 1 + .../usecase/method/WcSwitchNetworkUseCase.kt | 1 + .../connections/ui/AlertsModalBottomSheet.kt | 2 +- .../transaction/model/WcAddNetworkModel.kt | 4 +- .../transaction/model/WcSwitchNetworkModel.kt | 8 ++- 11 files changed, 102 insertions(+), 7 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index 5c988f8a8e..bf9f6674f9 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -3,7 +3,11 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.Either import arrow.core.left import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.Wallet.Model +import com.reown.walletkit.client.WalletKit import com.tangem.blockchain.extensions.hexToInt +import com.tangem.data.walletconnect.model.CAIP10 import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork.NamespaceConverter.Companion.ETH_NAMESPACE_KEY import com.tangem.data.walletconnect.respond.WcRespondService @@ -21,9 +25,12 @@ import com.tangem.domain.walletconnect.usecase.method.WcNetworkDerivationState import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume internal class WcEthAddNetworkUseCase @AssistedInject constructor( private val respondService: WcRespondService, + private val networksConverter: WcNetworksConverter, addSwitchCommonDelegateFactory: WcEthAddSwitchCommonDelegate.Factory, @Assisted val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.AddEthereumChain, @@ -45,13 +52,58 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( override suspend fun invoke(): Either { return addSwitchCommonDelegate .commonChecks(method.rawChain.chainId) - .map { addedNetwork -> WcAddNetworkUseCase.AddNetwork(addedNetwork) } + .map { addedNetwork -> + WcAddNetworkUseCase.AddNetwork( + network = addedNetwork, + isExistInWcSession = addSwitchCommonDelegate.existInWcSession(addedNetwork), + ) + } } override suspend fun approve(): Either { + fun illegalState() = WcRequestError.UnknownError(IllegalStateException("IllegalStateException")).left() + val requestedNetworkCAIP2 = CAIP2.fromRaw(rawSdkRequest.chainId.orEmpty()) ?: return illegalState() + val networkToAddCAIP2 = addSwitchCommonDelegate.hexChainIdToCAIP2(method.rawChain.chainId) + ?: return illegalState() + val namespaces = session.sdkModel.namespaces[requestedNetworkCAIP2.namespace] + ?: return illegalState() + // find and add all derivation + val networkToAddCAIP10 = networksConverter + .allAddressForChain(networkToAddCAIP2.raw, wallet) + .map { address -> CAIP10(networkToAddCAIP2, address).raw } + val newNamespaces = namespaces.copy( + chains = namespaces.chains.plus(networkToAddCAIP2.raw), + accounts = namespaces.accounts.plus(networkToAddCAIP10), + ) + val sdkNewNamespaces = session.sdkModel.namespaces + .plus(requestedNetworkCAIP2.namespace to newNamespaces) + .mapValues { (_, session) -> + Model.Namespace.Session( + chains = session.chains, + accounts = session.accounts, + methods = session.methods, + events = session.events, + ) + } + + val sessionUpdate = Wallet.Params.SessionUpdate( + sessionTopic = context.session.sdkModel.topic, + namespaces = sdkNewNamespaces, + ) + sdkUpdateSession(sessionUpdate) // ignore result for now return respondService.respond(rawSdkRequest, "") } + private suspend fun sdkUpdateSession(sessionUpdate: Wallet.Params.SessionUpdate): Either { + return suspendCancellableCoroutine { continuation -> + WalletKit.updateSession( + params = sessionUpdate, + onSuccess = { if (continuation.isActive) continuation.resume(Unit.right()) }, + onError = { if (continuation.isActive) continuation.resume(it.throwable.left()) }, + ) + } + } + override fun reject() { respondService.rejectRequestNonBlock(rawSdkRequest) } @@ -69,8 +121,14 @@ internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor( private val wallet: UserWallet get() = context.session.wallet + fun hexChainIdToCAIP2(hexChainId: String): CAIP2? = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${hexChainId.hexToInt()}") + + fun existInWcSession(network: Network): Boolean { + return context.session.networks.any { it.rawId == network.rawId } + } + suspend fun commonChecks(hexChainId: String): Either { - val caip2 = CAIP2.fromRaw("$ETH_NAMESPACE_KEY:${hexChainId.hexToInt()}") + val caip2 = hexChainIdToCAIP2(hexChainId) ?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left() val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet) if (generalNetwork == null) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt index 0835594dfe..5b94908fa8 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSwitchNetworkUseCase.kt @@ -37,7 +37,12 @@ internal class WcEthSwitchNetworkUseCase @AssistedInject constructor( override suspend fun invoke(): Either { return addSwitchCommonDelegate .commonChecks(method.rawChain.chainId) - .map { addedNetwork -> WcSwitchNetworkUseCase.SwitchNetwork(addedNetwork) } + .map { addedNetwork -> + WcSwitchNetworkUseCase.SwitchNetwork( + network = addedNetwork, + isExistInWcSession = addSwitchCommonDelegate.existInWcSession(addedNetwork), + ) + } } override fun reject() { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 4b49445c7f..a7d1d285fb 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -53,6 +53,11 @@ internal class WcNetworksConverter @Inject constructor( return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull() } + suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List { + return filterWalletNetworkForRequest(rawChainId, wallet) + .mapNotNull { walletManagersFacade.getDefaultAddress(wallet.walletId, it)?.lowercase() } + } + /** * return all exist derivation networks */ diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt index 0f2e570acd..f588c4f171 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkSessionConverter.kt @@ -10,6 +10,14 @@ internal object WcSdkSessionConverter : Converter + WcSdkSession.Session( + chains = session.chains ?: listOf(), + accounts = session.accounts, + methods = session.methods, + events = session.events, + ) + }, ) } } \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt index e68555c7c1..15049820a7 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -62,6 +62,7 @@ internal class WcSignUseCaseDelegateTest { connectingTime = 0L, sdkModel = WcSdkSession( topic = "", + namespaces = mapOf(), appMetaData = WcAppMetaData( name = "", description = "", diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt index 9b2d6c45b7..63ea82098f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt @@ -6,4 +6,12 @@ package com.tangem.domain.walletconnect.model.sdkcopy data class WcSdkSession( val topic: String, val appMetaData: WcAppMetaData, -) \ No newline at end of file + val namespaces: Map, +) { + data class Session( + val chains: List, + val accounts: List, + val methods: List, + val events: List, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt index 35223ba040..7573141b6d 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt @@ -15,5 +15,6 @@ interface WcAddNetworkUseCase : data class AddNetwork( val network: Network, + val isExistInWcSession: Boolean, ) } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt index 5c2066049a..b7980f5500 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcSwitchNetworkUseCase.kt @@ -13,5 +13,6 @@ interface WcSwitchNetworkUseCase : data class SwitchNetwork( val network: Network, + val isExistInWcSession: Boolean, ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt index c7ff306829..30370ab112 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/AlertsModalBottomSheet.kt @@ -177,7 +177,7 @@ private fun AlertContentDescription(alert: AlertsComponent.AlertType, modifier: R.string.wc_alert_unsupported_method_description, ) is AlertsComponent.AlertType.RequiredAddNetwork -> stringResourceSafe( - R.string.wc_alert_unsupported_method_description, + R.string.wc_alert_add_network_to_portfolio_description, alert.network, ) is AlertsComponent.AlertType.RequiredReconnectWithNetwork -> stringResourceSafe( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index f85c7a6003..ab284df37a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -60,7 +60,9 @@ internal class WcAddNetworkModel @Inject constructor( val either = useCase.invoke() either .onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) } - .map { showUI() } + .map { + if (it.isExistInWcSession) cancel(useCase) else showUI() + } } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt index 65b63ae8b6..d8c49a7508 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSwitchNetworkModel.kt @@ -35,7 +35,13 @@ internal class WcSwitchNetworkModel @Inject constructor( useCase.reject() either .onLeft { showErrorDialog(it) } - .map { showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) } + .map { + if (it.isExistInWcSession) { + router.pop() + } else { + showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) + } + } } } From 881e5adb9d9e521b755aef15fc5907eeaf555f39 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 14:38:32 +0300 Subject: [PATCH 58/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 10 +++++++++- core/res/src/main/res/values-es/strings.xml | 10 +++++++++- core/res/src/main/res/values-fr/strings.xml | 10 +++++++++- core/res/src/main/res/values-ja/strings.xml | 10 +++++++++- core/res/src/main/res/values-ru/strings.xml | 10 +++++++++- core/res/src/main/res/values-uk-rUA/strings.xml | 11 ++++++++++- core/res/src/main/res/values/strings.xml | 2 +- 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index c9acb51a42..c62cee51c8 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1396,12 +1396,18 @@ Trustline aktivieren Um dieses Token zu erhalten, muss eine Trustline aktiviert sein. Das Netzwerk benötigt eine Reserve von %1$s %2$s. Trustline erforderlich + Das erforderliche Netzwerk %s ist nicht in Ihrem Portfolio hinzugefügt. Fügen Sie es zuerst hinzu und fahren Sie dann mit der Verbindung fort. + Netzwerk zum Portfolio hinzufügen Bösartige/ verdächtige Domäne Unbekannte Domäne Trotzdem verbinden Zeitüberschreitungsfehler. Bitte versuche es später erneut. WalletConnect konnte nicht hergestellt werden Diese Domäne kann nicht verifiziert werden. Überprüfe die Anfrage sorgfältig und bestätige diese dann. + Um fortzufahren, verbinden Sie bitte Ihre dApp-Sitzung erneut mit dem erforderlichen Netzwerk %s. + Netzwerk nicht verbunden + Überprüfen Sie Ihre Netzwerkverbindung + Anforderungs-Zeitüberschreitung Bitte kehre zu Deinem Browser zurück und stellen die Verbindung über WalletConnect erneut her. WalletConnect-Sitzung wurde getrennt Trotzdem unterschreiben @@ -1411,9 +1417,11 @@ Tangem Wallet unterstützt derzeit nicht %s Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. + Dieses Netzwerk %s wird von Tangem Wallet nicht unterstützt und kann nicht verbunden werden. + Nicht unterstütztes Netzwerk Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. Nicht unterstützte Netzwerke - Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. %s + Diese Domain hat die Überprüfungen bestanden und gilt als sicher, vertrauenswürdig und frei von bekannten Bedrohungen oder verdächtigen Aktivitäten. Verifizierte Domain Falsche Karte oder falscher Ring in der App ausgewählt Wir haben eine Art Problem diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 3810328a13..c510d098aa 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1336,12 +1336,18 @@ Habilitar línea de confianza Una línea de confianza debe estar habilitada para recibir este token. La red requiere un %1$s %2$s reserva. Se requiere línea de confianza + La red requerida %s no está añadida a su portafolio. Añádala primero y luego continúe con la conexión. + Agregar red al portafolio Dominio malicioso Dominio desconocido Conectarse de todas formas Error de tiempo de espera. Por favor, inténtalo de nuevo más tarde. Error al establecer WalletConnect Este dominio no puede ser verificado. Compruebe cuidadosamente la solicitud de aprobación. + Para continuar, vuelva a conectar su sesión de dApp con la red requerida %s. + Red no conectada + Verifique su conexión de red + Tiempo de espera de la solicitud agotado Vuelva a su navegador y vuelva a conectarse a través de WalletConnect. La sesión de Wallet Connect se desconectó Firmar de todos modos @@ -1352,9 +1358,11 @@ dApp no compatible Código de error: 8 005. Si el problema persiste, no dudes en contactar con nuestro soporte. Hemos encontrado un error desconocido + Esta red %s no es compatible con Tangem Wallet y no puede conectarse. + Red no compatible Actualmente, Tangem no es compatible con una red requerida por %s. Redes no compatibles - Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. %s + Este dominio ha superado las verificaciones y se considera seguro, confiable y libre de amenazas conocidas o actividades sospechosas. Dominio verificado Se seleccionó una tarjeta o un anillo incorrectos en la app Tenemos algún tipo de problema diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a24b04d401..5996fd51d5 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1317,14 +1317,22 @@ Activer Trustline Une Trustline doit être activée pour recevoir ce jeton. Le réseau requiert une réserve de %1$s %2$s Trustline requise + Le réseau requis %s n’est pas ajouté à votre portefeuille. Ajoutez-le d’abord, puis poursuivez la connexion. + Ajouter le réseau au portefeuille Domaine malveillant + Pour continuer, veuillez reconnecter votre session dApp avec le réseau requis %s. + Réseau non connecté + Vérifiez votre connexion réseau + Délai d’attente de la requête dépassé Signer quand même Si le problème persiste, n’hésitez pas à contacter notre support. Le portefeuille Tangem ne prend actuellement pas en charge %ss dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue - Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes.%s + Le réseau %s n’est pas pris en charge par Tangem Wallet et ne peut pas être connecté. + Réseau non pris en charge + Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes. Autoriser à dépenser Adresse Chargement diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index a66d2ba966..7e06ea9819 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1437,12 +1437,18 @@ トラストラインを有効にする このトークンを受け取るには、トラストラインを有効にする必要があります。ネットワークには%1$s %2$s予備金が必要です。 トラストラインが必要 + 必要なネットワーク %s はポートフォリオに追加されていません。まず追加してから接続を続行してください。 + ネットワークをポートフォリオに追加 悪意のあるドメイン 不明なドメイン とにかく接続する タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。 WalletConnectを確立できませんでした このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 + 続行するには、必要なネットワーク %s でdAppセッションを再接続してください。 + ネットワークが接続されていません + ネットワーク接続を確認してください + リクエストがタイムアウトしました ブラウザに戻り、WalletConnect経由で再接続してください。 Wallet Connectセッションが接続解除されました とにかくサインする @@ -1453,9 +1459,11 @@ サポートされていないdApp エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました + このネットワーク %s はTangem Walletでサポートされておらず、接続できません。 + サポートされていないネットワーク Tangemは現在%sで必要なネットワークをサポートしていません。 未対応のネットワーク - このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。%s + このドメインは検証チェックに合格しており、安全で信頼でき、既知の脅威や不審な活動がないと判断されています。 検証済みドメイン アプリで間違ったカードまたはリングが選択されました 問題が起きています diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6bef7c8991..8de88e16ba 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1306,12 +1306,18 @@ Открыть Trustline Чтобы получить этот токен, необходимо включить Trustline. Сеть требует резерв %1$s %2$s. Требуется трастлайн + Требуемая сеть %s не добавлена в ваш портфель. Добавьте её, а затем выполните подключение. + Добавьте сеть в ваш портфель Вредоносный домен Неизвестный домен Всё равно подключиться Ошибка тайм-аута. Пожалуйста, попробуйте позже. Не удалось подключиться через Wallet Connect Этот домен не может быть верифицирован. Внимательно проверьте запрос перед одобрением. + Чтобы продолжить, переподключите сессию dApp с требуемой сетью %s. + Сеть не подключена + Проверьте подключение к сети + Время ожидания запроса истекло Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect. Сессия Wallet Connect была завершена Подписать всё равно @@ -1321,9 +1327,11 @@ Кошелек Tangem в настоящий момент не поддерживает %s Неподдерживаемый dApp Мы обнаружили неизвестную ошибку + Эта сеть %s не поддерживается Tangem Wallet и не может быть подключена. + Неподдерживаемая сеть Tangem в настоящее время не поддерживает необходимую сеть для %s Неподдерживаемые сети - Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. %s + Этот домен прошёл проверку и считается безопасным, надёжным и свободным от известных угроз или подозрительной активности. Верифицированный домен Выбрана не верная карта или кольцо Похоже, возникла проблема diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index e8b9b18fbc..ef5cb62c26 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1293,19 +1293,27 @@ Відкрити Trustline Щоб отримати цей токен, потрібно увімкнути Trustline. Мережа вимагає резерв %1$s %2$s. Відкрийте Trustline + Потрібна мережа %s не додана до вашого портфеля. Спочатку додайте її, а потім виконайте підключення. + Додати мережу до портфеля Невідомий домен Все одно підключитися Помилка тайм-ауту. Будь ласка, спробуйте пізніше. Не вдалося зʼєднатися через Wallet Connect Цей домен не може бути підтверджений. Уважно перевірте запит перед схваленням. + Щоб продовжити, перепідключіть сесію dApp із потрібною мережею %s. + Мережа не підключена + Перевірте підключення до мережі + Час очікування запиту вичерпано Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. Сеанс Wallet Connect було завершено Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою Tangem Wallet наразі не підтримує %s + Ця мережа %s не підтримується Tangem Wallet і не може бути підключена. + Непідтримувана мережа Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі - Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s + Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. Верифікований домен Обрана не вірна картка або кільце Схоже, виникла проблема @@ -1337,6 +1345,7 @@ Немає мереж Будь ласка, згенеруйте новий URI та спробуйте ще раз Термін для з’єднання минув + Прогнозовані зміни Поповніть баланс, щоб покрити комісію мережі Недостатньо %1$s Додайте %s мережі до вашого портфелю для цього гаманця diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f9238788d1..5074ca1886 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1531,7 +1531,7 @@ Unsupported network Tangem does not currently support a required network by %s. Unsupported networks - This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s + This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. Verified domain Wrong card or ring selected in the App We\'ve got some kind of problem From 1f1f7adef44f1904db8249ba25557a646c83ca56 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Aug 2025 14:32:52 +0400 Subject: [PATCH 59/87] Updated on 2026-08-14 --- .../DefaultAccountsCRUDRepository.kt | 39 ++++- domain/account/build.gradle.kts | 2 + .../repository/AccountsCRUDRepository.kt | 23 +++ .../usecase/GetArchivedAccountsUseCase.kt | 86 ++++++++++ .../usecase/GetArchivedAccountsUseCaseTest.kt | 158 ++++++++++++++++++ 5 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt create mode 100644 domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index a46b5d096a..a9fba30f57 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -13,6 +13,8 @@ import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /** [REDACTED_AUTHOR] @@ -37,16 +39,23 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun getArchivedAccount(accountId: AccountId): Option = option { - ArchivedAccount( - accountId = accountId, - name = AccountName("Archived Account").getOrNull()!!, - icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - derivationIndex = DerivationIndex(value = 1000).getOrNull()!!, - tokensCount = 2, - networksCount = 1, + createMockArchivedAccount(userWalletId = accountId.userWalletId) + } + + override suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> = option { + listOf( + createMockArchivedAccount(userWalletId), ) } + override fun getArchivedAccounts(userWalletId: UserWalletId): Flow> { + return flow { + getArchivedAccountsSync(userWalletId).getOrNull().orEmpty() + } + } + + override suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) = Unit + override suspend fun saveAccounts(accountList: AccountList) { runtimeStore.update(emptyList()) { it.addOrReplace(accountList) { it.userWallet.walletId == accountList.userWallet.walletId } @@ -62,4 +71,20 @@ internal class DefaultAccountsCRUDRepository( override fun getUserWallet(userWalletId: UserWalletId): UserWallet { return userWalletsStore.getSyncStrict(userWalletId) } + + private fun createMockArchivedAccount(userWalletId: UserWalletId): ArchivedAccount { + val derivationIndex = DerivationIndex(value = 1000).getOrNull()!! + + return ArchivedAccount( + accountId = AccountId.forCryptoPortfolio( + userWalletId = userWalletId, + derivationIndex = derivationIndex, + ), + name = AccountName("Archived Account").getOrNull()!!, + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + derivationIndex = derivationIndex, + tokensCount = 2, + networksCount = 1, + ) + } } \ No newline at end of file diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index cf1bc96831..75db105c86 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -10,10 +10,12 @@ tasks.withType().configureEach { dependencies { + api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) implementation(deps.kotlin.serialization) testImplementation(deps.test.coroutine) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index a05f267633..ac6921e167 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Repository interface for performing CRUD operations on accounts @@ -38,6 +39,28 @@ interface AccountsCRUDRepository { */ suspend fun getArchivedAccount(accountId: AccountId): Option + /** + * Retrieves a list of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + * @return an [Option] containing a list of [ArchivedAccount] if found, or `Option.None` if not + */ + suspend fun getArchivedAccountsSync(userWalletId: UserWalletId): Option> + + /** + * Provides a flow of archived accounts associated with a specific user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + fun getArchivedAccounts(userWalletId: UserWalletId): Flow> + + /** + * Fetches archived accounts for a specific user wallet and updates the repository + * + * @param userWalletId the unique identifier of the user wallet + */ + suspend fun fetchArchivedAccounts(userWalletId: UserWalletId) + /** * Saves a list of accounts to the repository * diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt new file mode 100644 index 0000000000..cbcfb13168 --- /dev/null +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/GetArchivedAccountsUseCase.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.account.usecase + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.retryWhen +import kotlinx.coroutines.launch + +typealias ArchivedAccountList = List + +/** + * Use case for retrieving archived accounts for a specific user wallet + * + * @property crudRepository the repository for performing CRUD operations on accounts + * +[REDACTED_AUTHOR] + */ +class GetArchivedAccountsUseCase( + private val crudRepository: AccountsCRUDRepository, +) { + + /** + * Executes the use case to retrieve archived accounts for the given user wallet + * + * @param userWalletId the unique identifier of the user wallet + */ + operator fun invoke(userWalletId: UserWalletId): LceFlow = channelFlow { + val archivedAccounts = getArchivedAccounts(userWalletId = userWalletId) + + archivedAccounts + .onRight { send(it.lceContent()) } + .onLeft { + send(lceLoading()) + + launch { + fetchArchivedAccounts(userWalletId).getOrElse { + send(it.lceError()) + } + } + } + + subscribeOnArchivedAccounts(userWalletId) + } + .distinctUntilChanged() + + private suspend fun getArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { + crudRepository.getArchivedAccountsSync(userWalletId = userWalletId).getOrElse { + error("Archived accounts not found for user wallet: $userWalletId") + } + } + } + + private suspend fun fetchArchivedAccounts(userWalletId: UserWalletId): Either { + return Either.catch { crudRepository.fetchArchivedAccounts(userWalletId) } + } + + private suspend fun ProducerScope>.subscribeOnArchivedAccounts( + userWalletId: UserWalletId, + ) { + crudRepository.getArchivedAccounts(userWalletId) + .distinctUntilChanged() + .retryWhen { cause, _ -> + send(cause.lceError()) + + delay(timeMillis = 2000) + + true + } + .collectLatest { archivedAccounts -> + send(archivedAccounts.lceContent()) + } + } +} \ No newline at end of file diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt new file mode 100644 index 0000000000..eb0019f93c --- /dev/null +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetArchivedAccountsUseCaseTest.kt @@ -0,0 +1,158 @@ +package com.tangem.domain.account.usecase + +import arrow.core.None +import arrow.core.toOption +import com.google.common.truth.Truth +import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetArchivedAccountsUseCaseTest { + + private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) + private val useCase = GetArchivedAccountsUseCase(crudRepository) + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun resetMocks() { + clearMocks(crudRepository) + } + + @Test + fun `invoke should emit archived accounts when repository returns data`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns archivedAccounts.toOption() + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf(archivedAccounts.lceContent()) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + + coVerify(exactly = 0) { crudRepository.fetchArchivedAccounts(any()) } + } + + @Test + fun `invoke should emit loading and fetch when accounts not found`() = runTest { + // Arrange + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if getArchivedAccountsSync throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Test error") + val archivedAccounts = listOf( + mockk(), + mockk(), + ) + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } throws exception + every { crudRepository.getArchivedAccounts(userWalletId) } returns flowOf(archivedAccounts) + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + archivedAccounts.lceContent(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @Test + fun `invoke should emit error if fetchArchivedAccounts throws exception`() = runTest { + // Arrange + val exception = IllegalStateException("Fetch error") + + coEvery { crudRepository.getArchivedAccountsSync(userWalletId) } returns None + every { crudRepository.getArchivedAccounts(userWalletId) } returns emptyFlow() + coEvery { crudRepository.fetchArchivedAccounts(userWalletId) } throws exception + + // Act + val actual = getEmittedValues(useCase(userWalletId)) + + // Assert + val expected = listOf( + lceLoading(), + exception.lceError(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerify(exactly = 1) { + crudRepository.getArchivedAccountsSync(userWalletId) + crudRepository.fetchArchivedAccounts(userWalletId) + crudRepository.getArchivedAccounts(userWalletId) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + fun TestScope.getEmittedValues(flow: Flow): List { + val values = mutableListOf() + + backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { + flow.toList(values) + } + + return values + } +} \ No newline at end of file From a443332da81a8c9cb9057b52ba2d3c78cedc1a10 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 15:12:25 +0300 Subject: [PATCH 60/87] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index bc4cd43085..794a8187e6 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112 +Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a From 40993944eefe9012d0ae80a6d8c3f6b16f472e0e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Aug 2025 14:37:42 +0700 Subject: [PATCH 61/87] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 11 + .../com/tangem/common/routing/AppRoute.kt | 5 + core/res/src/main/res/values-de/strings.xml | 3 +- core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-uk-rUA/strings.xml | 3 +- core/res/src/main/res/values/strings.xml | 9 + .../account/ArchivedAccountListComponent.kt | 11 + .../archived/ArchivedAccountListModel.kt | 68 ++++++ .../DefaultArchivedAccountListComponent.kt | 40 ++++ .../archived/di/AccountArchivedModule.kt | 27 +++ .../archived/entity/AccountArchivedUM.kt | 27 +++ .../archived/ui/ArchivedAccountListContent.kt | 202 ++++++++++++++++++ .../details/ui/AccountDetailsContent.kt | 2 +- 14 files changed, 407 insertions(+), 4 deletions(-) create mode 100644 features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt create mode 100644 features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index ed41201e3f..d4b9a8ee88 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.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent import com.tangem.feature.usedesk.api.UsedeskComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.createwalletselection.CreateWalletSelectionComponent @@ -94,6 +95,7 @@ internal class ChildFactory @Inject constructor( private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory, private val accountCreateEditComponentFactory: AccountCreateEditComponent.Factory, private val accountDetailsComponentFactory: AccountDetailsComponent.Factory, + private val archivedAccountListComponentFactory: ArchivedAccountListComponent.Factory, private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val usedeskComponentFactory: UsedeskComponent.Factory, @@ -565,6 +567,15 @@ internal class ChildFactory @Inject constructor( componentFactory = accountDetailsComponentFactory, ) } + is AppRoute.ArchivedAccountList -> { + createComponentChild( + context = context, + params = ArchivedAccountListComponent.Params( + userWalletId = route.userWalletId, + ), + componentFactory = archivedAccountListComponentFactory, + ) + } } } } \ 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 55cc7fca0d..719d1dd05d 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 @@ -346,4 +346,9 @@ sealed class AppRoute(val path: String) : Route { data class AccountDetails( val account: Account, ) : AppRoute(path = "/account_details/${account.accountId.value}") + + @Serializable + data class ArchivedAccountList( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/archived_account/${userWalletId.stringValue}") } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 38278a60f0..c9acb51a42 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1408,6 +1408,7 @@ Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. + Tangem Wallet unterstützt derzeit nicht %s Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support. Wir haben einen unbekannten Fehler festgestellt. Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht. @@ -1460,7 +1461,7 @@ Transaktionsanfrage Transaktionsanfrage Unbegrenzte Menge - Wallet verbinden + WalletConnect Verwerfen Du hast eine unterbrochene Sicherung. Möchtest du diese fortsetzen? Ja, fortsetzen diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 900aa6723d..a24b04d401 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1355,6 +1355,7 @@ Demande de transaction Demande de transaction Montant illimité + WalletConnect Ignorer Vous avez une sauvegarde interrompue. Voulez-vous la reprendre ? Oui, reprendre diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index f9fdb1b3f2..6bef7c8991 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1359,7 +1359,7 @@ Нет сетей Пожалуйста, сгенерируйте новый URI и попробуйте подключиться снова Предложение подключения истекло - Предварительные изменения + Прогнозируемые изменения Не удалось выполнить симуляцию транзакции. Пожалуйста, действуйте с осторожностью. Оценка не поддерживается для %s Предложено %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 952d5a7dd6..e8b9b18fbc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1302,6 +1302,7 @@ Сеанс Wallet Connect було завершено Код помилки: %s. Якщо проблема зберігається, зверніться до нашої служби підтримки. Ми зіткнулися з невідомою помилкою + Tangem Wallet наразі не підтримує %s Tangem наразі не підтримує необхідну мережу для %s. Непідтримувані мережі Цей домен пройшов перевірку та вважається безпечним, надійним і вільним від відомих загроз чи підозрілої активності. %s @@ -1354,7 +1355,7 @@ До Запит транзакції Запит транзакції - Підключення гаманця + WalletConnect Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a76469f95b..f9238788d1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -495,6 +495,7 @@ Stay up to date with the latest features and news Seed phrase backup Create Mobile Wallet + This recovery phrase has already been imported Mobile Wallet This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation @@ -1504,12 +1505,18 @@ Enable Trustline A Trustline must be enabled to receive this token. The network requires a %1$s %2$s reserve. Trustline Required + The required network %s is not added to your portfolio. Add it first, then proceed with the connection. + Add network to portfolio Malicious domain Unknown domain Connect anyway Timeout error. Please, try again later. Failed to establish WalletConnect This domain cannot be verified. Check the request carefully approving. + To continue, please reconnect your dApp session with the required network %s. + Network not connected + Check your network connection + Request timeout Please return to your browser and reconnect via WalletConnect. Wallet Connect session was disconnected Sign anyway @@ -1520,6 +1527,8 @@ Unsupported dApp Error code: 8 005. If the problem persists — feel free to contact our support. We\'ve encountered unknown error + This network %s is not supported by Tangem Wallet and cannot be connected. + Unsupported network Tangem does not currently support a required network by %s. Unsupported networks This domain has passed verification checks and is considered safe, reputable, and free from known threats or suspicious activity. %s diff --git a/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt new file mode 100644 index 0000000000..91acb0ea4d --- /dev/null +++ b/features/account/api/src/main/java/com/tangem/features/account/ArchivedAccountListComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.account + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId + +interface ArchivedAccountListComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt new file mode 100644 index 0000000000..3c2aed7f6b --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -0,0 +1,68 @@ +package com.tangem.features.account.archived + +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +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.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("UnusedPrivateMember") // todo account +internal class ArchivedAccountListModel @Inject constructor( + paramsContainer: ParamsContainer, + private val messageSender: UiMessageSender, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase, +) : Model() { + + private val params = paramsContainer.require() + + val uiState: StateFlow get() = _uiState + private val _uiState: MutableStateFlow = MutableStateFlow(getInitialState()) + + private fun confirmRecoverDialog(accountId: AccountId) { + val account: Account? = null // todo account find + account ?: return + val secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + val firstAction = EventMessageAction( + title = resourceReference(R.string.account_archived_recover), + onClick = { recoverCryptoPortfolio(account.accountId) }, + ) + messageSender.send( + DialogMessage( + title = stringReference(account.name.value), + message = TextReference.EMPTY, + firstActionBuilder = { firstAction }, + secondActionBuilder = { secondAction }, + ), + ) + } + + private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { + recoverCryptoPortfolioUseCase(accountId) + } + + private fun getInitialState(): AccountArchivedUM { + return AccountArchivedUM.Loading( + onCloseClick = { router.pop() }, + ) + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt new file mode 100644 index 0000000000..6179fd12b2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/DefaultArchivedAccountListComponent.kt @@ -0,0 +1,40 @@ +package com.tangem.features.account.archived + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ui.ArchivedAccountListContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultArchivedAccountListComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: ArchivedAccountListComponent.Params, +) : AppComponentContext by appComponentContext, ArchivedAccountListComponent { + + private val model: ArchivedAccountListModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + ArchivedAccountListContent( + modifier = modifier, + state = state, + ) + BackHandler(onBack = state.onCloseClick) + } + + @AssistedFactory + interface Factory : ArchivedAccountListComponent.Factory { + override fun create( + context: AppComponentContext, + params: ArchivedAccountListComponent.Params, + ): DefaultArchivedAccountListComponent + } +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt new file mode 100644 index 0000000000..21c674cef2 --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/di/AccountArchivedModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.di + +import com.tangem.core.decompose.model.Model +import com.tangem.features.account.ArchivedAccountListComponent +import com.tangem.features.account.archived.ArchivedAccountListModel +import com.tangem.features.account.archived.DefaultArchivedAccountListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(SingletonComponent::class) +internal interface AccountArchivedModule { + + @Binds + fun bindArchivedAccountListComponentFactory( + impl: DefaultArchivedAccountListComponent.Factory, + ): ArchivedAccountListComponent.Factory + + @Binds + @IntoMap + @ClassKey(ArchivedAccountListModel::class) + fun bindArchivedAccountListModel(model: ArchivedAccountListModel): Model +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt new file mode 100644 index 0000000000..ee42b871ff --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt @@ -0,0 +1,27 @@ +package com.tangem.features.account.archived.entity + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.account.common.CryptoPortfolioIconUM +import kotlinx.collections.immutable.ImmutableList + +internal sealed interface AccountArchivedUM { + val onCloseClick: () -> Unit + + data class Loading(override val onCloseClick: () -> Unit) : AccountArchivedUM + data class Error( + override val onCloseClick: () -> Unit, + val onRetryClick: () -> Unit, + ) : AccountArchivedUM + data class Content( + override val onCloseClick: () -> Unit, + val accounts: ImmutableList, + ) : AccountArchivedUM +} + +internal data class ArchivedAccountUM( + val accountId: String, + val accountName: String, + val accountIcon: CryptoPortfolioIconUM, + val tokensInfo: TextReference, + val onClick: (accountId: String) -> Unit, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt new file mode 100644 index 0000000000..65b5e1794c --- /dev/null +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -0,0 +1,202 @@ +package com.tangem.features.account.archived.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.features.account.archived.entity.AccountArchivedUM +import com.tangem.features.account.archived.entity.ArchivedAccountUM +import com.tangem.features.account.common.toUM +import com.tangem.features.account.details.ui.AccountIcon +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun ArchivedAccountListContent(state: AccountArchivedUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithBackButton( + text = stringResourceSafe(R.string.account_archived_title), + onBackClick = state.onCloseClick, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) + + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f), + + ) { + when (state) { + is AccountArchivedUM.Content -> ArchiveAccountContent(state) + is AccountArchivedUM.Error -> ArchiveAccountError(state) + is AccountArchivedUM.Loading -> ArchiveAccountLoading() + } + } + } +} + +@Composable +private fun ArchiveAccountLoading(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.primary1, + modifier = Modifier, + ) + } +} + +@Composable +private fun ArchiveAccountError(state: AccountArchivedUM.Error, modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = stringResourceSafe(R.string.common_unable_to_load), + ) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = state.onRetryClick, + ), + ) + } + } +} + +@Composable +private fun ArchiveAccountContent(state: AccountArchivedUM.Content, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier) { + itemsIndexed( + items = state.accounts, + key = { index, item -> item.accountId }, + ) { index, account -> + ArchivedAccountRow( + item = account, + modifier = Modifier.roundedShapeItemDecoration( + backgroundColor = TangemTheme.colors.background.primary, + radius = TangemTheme.dimens.radius20, + currentIndex = index, + addDefaultPadding = true, + lastIndex = state.accounts.lastIndex, + ), + ) + } + } +} + +@Composable +private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = { item.onClick(item.accountId) }) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + modifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)), + accountName = item.accountName, + accountIcon = item.accountIcon, + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = item.accountName, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = item.tokensInfo.resolveReference(), + ) + } + + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.account_archived_recover), + onClick = { item.onClick(item.accountId) }, + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountArchivedUM) { + TangemThemePreview { + ArchivedAccountListContent(state = params) + } +} + +@Suppress("MagicNumber") +private class PreviewStateProvider : CollectionPreviewParameterProvider( + buildList { + fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + + val firstList = List(10) { + ArchivedAccountUM( + accountId = it.toString(), + accountName = "Account name", + accountIcon = portfolioIcon(), + tokensInfo = stringReference("10 tokens in 2 networks"), + onClick = {}, + + ) + }.toImmutableList() + val first = AccountArchivedUM.Content( + onCloseClick = {}, + accounts = firstList, + ) + add(first) + add(AccountArchivedUM.Loading {}) + add(AccountArchivedUM.Error({}, {})) + }, +) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt index 6234a1001b..08b3562176 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -183,7 +183,7 @@ private fun AccountRow(state: AccountDetailsUM) { // todo account make reusable @Composable -private fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) { +internal fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) { Box( contentAlignment = Alignment.Center, modifier = modifier.background(accountIcon.color.getUiColor()), From 28210080c5a06b11d8f9230e4c8c1b5179d98d5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 11:37:44 +0400 Subject: [PATCH 62/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 14441c2e44..fb79857d96 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1140" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-511" +tangemCardSdk = "develop-518" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 94196d74c42340e644a9518006484bc8e0615af4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 19:43:54 +0500 Subject: [PATCH 63/87] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++++ .../com/tangem/features/send/v2/api/SendFeatureToggles.kt | 1 + .../com/tangem/features/send/v2/DefaultSendFeatureToggles.kt | 2 ++ 3 files changed, 7 insertions(+) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6ecb2f5ac6..4cd82bc21e 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -50,5 +50,9 @@ { "name": "HOT_WALLET_ENABLED", "version": "undefined" + }, + { + "name": "NFT_SEND_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index fb665394be..294674ae19 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -3,5 +3,6 @@ package com.tangem.features.send.v2.api interface SendFeatureToggles { val isSendRedesignEnabled: Boolean + val isNFTSendRedesignEnabled: Boolean val isSendWithSwapEnabled: Boolean } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index f414e52610..d73b45c69b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -8,6 +8,8 @@ internal class DefaultSendFeatureToggles( ) : SendFeatureToggles { override val isSendRedesignEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") + override val isNFTSendRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("NFT_SEND_REDESIGN_ENABLED") override val isSendWithSwapEnabled: Boolean get() = featureToggles.isFeatureEnabled("SEND_VIA_SWAP_ENABLED") } \ No newline at end of file From f7bd99f737b690971c81ddadc4739bedff692d6c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Aug 2025 19:52:11 +0500 Subject: [PATCH 64/87] Updated on 2026-08-14 --- .../nft/component/NFTDetailsBlockComponent.kt | 5 +- .../block/DefaultNFTDetailsBlockComponent.kt | 2 + .../nft/details/block/ui/NFTDetailsBlock.kt | 31 +++-- .../success/ui/SendConfirmSuccessContent.kt | 2 +- .../v2/sendnft/DefaultNFTSendComponent.kt | 34 +++++- .../confirm/NFTSendConfirmComponent.kt | 39 ++++++- .../confirm/model/NFTSendConfirmModel.kt | 98 ++++++++++------ .../confirm/ui/NFTSendConfirmContent.kt | 59 ++++++---- .../send/v2/sendnft/di/NFTSendModelModule.kt | 6 + .../send/v2/sendnft/model/NFTSendModel.kt | 17 ++- .../success/NFTSendSuccessComponent.kt | 96 ++++++++++++++++ .../success/model/NFTSendSuccessModel.kt | 107 ++++++++++++++++++ .../success/ui/NFTSendSuccessContent.kt | 103 +++++++++++++++++ .../send/v2/sendnft/ui/state/NFTSendUM.kt | 5 +- 14 files changed, 527 insertions(+), 77 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt index ed1df0a1f2..801c8f6420 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTDetailsBlockComponent.kt @@ -2,8 +2,9 @@ package com.tangem.features.nft.component import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.nft.models.NFTAsset +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.nft.models.NFTAsset interface NFTDetailsBlockComponent : ComposableContentComponent { @@ -11,6 +12,8 @@ interface NFTDetailsBlockComponent : ComposableContentComponent { val userWalletId: UserWalletId, val nftAsset: NFTAsset, val nftCollectionName: String, + val title: TextReference, + val isSuccessScreen: Boolean, ) interface Factory : ComponentFactory diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt index 234cb0adfb..4856ca5634 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/DefaultNFTDetailsBlockComponent.kt @@ -22,6 +22,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor( assetName = stringReference(params.nftAsset.name.orEmpty()), collectionName = stringReference(params.nftCollectionName), assetImage = params.nftAsset.media?.imageUrl, + title = params.title, + isSuccessScreen = params.isSuccessScreen, networkIconRes = getActiveIconRes(params.nftAsset.network.rawId), ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt index 6336d2dbb2..8d7d1148c9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/block/ui/NFTDetailsBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -19,12 +20,15 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.common.ui.NFTLogo import com.tangem.features.nft.impl.R +@Suppress("LongParameterList") @Composable internal fun NFTDetailsBlock( + title: TextReference, assetName: TextReference, collectionName: TextReference, assetImage: String?, networkIconRes: Int, + isSuccessScreen: Boolean, ) { Column( modifier = Modifier @@ -35,20 +39,21 @@ internal fun NFTDetailsBlock( verticalArrangement = Arrangement.spacedBy(6.dp), ) { Text( - text = "NFT Asset", + text = title.resolveReference(), style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.secondary, + color = TangemTheme.colors.text.tertiary, ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - NFTLogo( - assetImage, - networkIconRes, - background = TangemTheme.colors.background.action, - ) - + if (isSuccessScreen) { + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } Column( verticalArrangement = Arrangement.spacedBy(2.dp), ) { @@ -63,6 +68,14 @@ internal fun NFTDetailsBlock( color = TangemTheme.colors.text.tertiary, ) } + if (!isSuccessScreen) { + SpacerWMax() + NFTLogo( + assetImage, + networkIconRes, + background = TangemTheme.colors.background.action, + ) + } } } } @@ -78,6 +91,8 @@ private fun NFTDetailsBlock_Preview() { collectionName = stringReference("NFT Collection"), assetImage = null, networkIconRes = R.drawable.img_polygon_22, + title = stringReference("From My Wallet"), + isSuccessScreen = false, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt index 636828e73f..1d679a3e59 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/success/ui/SendConfirmSuccessContent.kt @@ -56,7 +56,7 @@ internal fun SendConfirmSuccessContent(sendUM: SendUM, destinationBlockComponent .padding(horizontal = TangemTheme.dimens.spacing16) .scrollable( state = rememberScrollState(), - orientation = Orientation.Horizontal, + orientation = Orientation.Vertical, ), verticalArrangement = Arrangement.spacedBy(12.dp), ) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt index c4ecdee6e2..b23440cfeb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/DefaultNFTSendComponent.kt @@ -18,7 +18,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -29,6 +28,7 @@ import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinationComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams @@ -42,7 +42,8 @@ import java.math.BigDecimal internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, - private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + private val nftSendConfirmComponentFactory: NFTSendConfirmComponent.Factory, + private val nftSendSuccessComponentFactory: NFTSendSuccessComponent.Factory, private val analyticsEventHandler: AnalyticsEventHandler, ) : NFTSendComponent, AppComponentContext by appComponentContext { @@ -121,6 +122,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( is CommonSendRoute.Destination -> getDestinationComponent(factoryContext) is CommonSendRoute.Fee -> getFeeComponent(factoryContext) CommonSendRoute.Confirm -> getConfirmComponent(factoryContext) + CommonSendRoute.ConfirmSuccess -> getSuccessComponent(factoryContext) else -> getStubComponent() } @@ -164,9 +166,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( } } - private fun getConfirmComponent(factoryContext: AppComponentContext) = NFTSendConfirmComponent( + private fun getConfirmComponent(factoryContext: AppComponentContext) = nftSendConfirmComponentFactory.create( appComponentContext = factoryContext, - nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, params = NFTSendConfirmComponent.Params( state = model.uiState.value, analyticsCategoryName = analyticsCategoryName, @@ -180,9 +181,34 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( currentRoute = model.currentRouteFlow.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, onLoadFee = model::loadFee, + onSendTransaction = { innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) }, ), ) + private fun getSuccessComponent(factoryContext: AppComponentContext): ComposableContentComponent { + val txUrl = (model.uiState.value.confirmUM as? ConfirmUM.Success)?.txUrl + + if (txUrl == null) { + model.showAlertError() + return getStubComponent() + } + + return nftSendSuccessComponentFactory.create( + appComponentContext = factoryContext, + params = NFTSendSuccessComponent.Params( + nftSendUMFlow = model.uiState, + analyticsCategoryName = analyticsCategoryName, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + callback = model, + currentRoute = model.currentRouteFlow.filterIsInstance(), + txUrl = txUrl, + ), + ) + } + private fun getStubComponent() = ComposableContentComponent { } private fun onChildBack() { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index cc0fe9a936..d2fd5670b1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -10,17 +10,23 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.params.FeeSelectorParams.FeeStateConfiguration import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.confirm.ui.NFTSendConfirmContent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM @@ -28,13 +34,17 @@ import com.tangem.features.send.v2.subcomponents.destination.DefaultSendDestinat import com.tangem.features.send.v2.subcomponents.fee.SendFeeBlockComponent import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponentParams import com.tangem.features.send.v2.subcomponents.notifications.DefaultSendNotificationsComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* import java.math.BigDecimal -internal class NFTSendConfirmComponent( - appComponentContext: AppComponentContext, - params: Params, +internal class NFTSendConfirmComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: NFTSendConfirmModel = getOrCreateModel(params = params) @@ -74,12 +84,28 @@ internal class NFTSendConfirmComponent( onClick = model::showEditFee, ) + private val feeSelectorBlockComponent = feeSelectorComponentFactory.create( + context = child("NFTSendConfirmFeeSelectorBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = params.onLoadFee, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = params.analyticsCategoryName, + ), + onResult = model::onFeeResult, + ) + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( context = child("NFTDetailsBlock"), params = NFTDetailsBlockComponent.Params( userWalletId = params.userWallet.walletId, nftAsset = params.nftAsset, nftCollectionName = params.nftCollectionName, + isSuccessScreen = false, + title = resourceReference(R.string.send_from_wallet_name, wrappedList(params.userWallet.name)), ), ) @@ -126,6 +152,7 @@ internal class NFTSendConfirmComponent( nftSendUM = state, destinationBlockComponent = destinationBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, notificationsComponent = notificationsComponent, notificationsUM = notificationState, @@ -145,9 +172,15 @@ internal class NFTSendConfirmComponent( val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val onLoadFee: suspend () -> Either, + val onSendTransaction: () -> Unit, ) interface ModelCallback { fun onResult(nftSendUM: NFTSendUM) } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendConfirmComponent + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 522470a90e..d834d13885 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -33,6 +33,7 @@ import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger @@ -61,6 +62,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @Suppress("LongParameterList", "LargeClass") @ModelScoped @@ -89,7 +91,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val sendFeeReloadTrigger: SendFeeReloadTrigger, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, -) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback { +) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback { private val params: NFTSendConfirmComponent.Params = paramsContainer.require() @@ -141,6 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor( updateConfirmNotifications() } + override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + fun onDestinationResult(destinationUM: DestinationUM) { _uiState.update { it.copy(destinationUM = destinationUM) } updateConfirmNotifications() @@ -400,13 +408,23 @@ internal class NFTSendConfirmModel @Inject constructor( ).onEach { (state, _) -> val confirmUM = state.confirmUM val confirmUMContent = confirmUM as? ConfirmUM.Content - val isReadyToSend = confirmUMContent != null && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( title = resourceReference(R.string.nft_send), - subtitle = confirmUMContent?.walletName, - backIconRes = R.drawable.ic_close_24, + subtitle = if (uiState.value.isRedesignEnabled) { + null + } else { + confirmUMContent?.walletName + }, + backIconRes = if (state.isRedesignEnabled) { + when (confirmUM) { + is ConfirmUM.Success -> R.drawable.ic_close_24 + else -> R.drawable.ic_back_24 + } + } else { + R.drawable.ic_close_24 + }, backIconClick = { analyticsEventHandler.send( CommonSendAnalyticEvents.CloseButtonClicked( @@ -416,26 +434,13 @@ internal class NFTSendConfirmModel @Inject constructor( isValid = confirmUM.isPrimaryButtonEnabled, ), ) - appRouter.pop() + if (state.isRedesignEnabled) { + router.pop() + } else { + appRouter.pop() + } }, - primaryButton = NavigationButton( - textReference = when (confirmUM) { - is ConfirmUM.Success -> resourceReference(R.string.common_close) - is ConfirmUM.Content -> if (confirmUM.isSending) { - resourceReference(R.string.send_sending) - } else { - resourceReference(R.string.common_send) - } - else -> resourceReference(R.string.common_send) - }, - iconRes = R.drawable.ic_tangem_24, - isIconVisible = isReadyToSend, - isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = isReadyToSend, - onClick = { - onNextClick(confirmUM) - }, - ), + primaryButton = primaryButtonUM(), prevButton = null, secondaryPairButtonsUM = ( NavigationButton( @@ -454,21 +459,40 @@ internal class NFTSendConfirmModel @Inject constructor( }.launchIn(modelScope) } - private fun onNextClick(confirmUM: ConfirmUM) { - when (confirmUM) { - is ConfirmUM.Success -> { - modelScope.launch { - nftSendSuccessTrigger.triggerSuccessNFTSend() + private fun primaryButtonUM(): NavigationButton { + val confirmUM = uiState.value.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending + return NavigationButton( + textReference = when (confirmUM) { + is ConfirmUM.Success -> resourceReference(R.string.common_close) + is ConfirmUM.Content -> if (confirmUM.isSending) { + resourceReference(R.string.send_sending) + } else { + resourceReference(R.string.common_send) } - appRouter.pop() - } - is ConfirmUM.Content -> if (confirmUM.isSending) { - return - } else { - onSendClick() - } - else -> return - } + else -> resourceReference(R.string.common_send) + }, + iconRes = R.drawable.ic_tangem_24, + isIconVisible = isReadyToSend, + isEnabled = confirmUM.isPrimaryButtonEnabled, + isHapticClick = isReadyToSend, + onClick = { + when (confirmUM) { + is ConfirmUM.Success -> { + modelScope.launch { + nftSendSuccessTrigger.triggerSuccessNFTSend() + } + appRouter.pop() + } + is ConfirmUM.Content -> if (confirmUM.isSending) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ) } private companion object { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt index c2a2de5774..9ff84edfb4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/ui/NFTSendConfirmContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.sendnft.confirm.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope 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.unit.dp import com.tangem.common.ui.footers.SendingText import com.tangem.common.ui.notifications.NotificationUM @@ -21,6 +23,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp import com.tangem.features.send.v2.impl.R @@ -40,6 +43,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { @@ -54,6 +58,7 @@ internal fun NFTSendConfirmContent( destinationBlockComponent = destinationBlockComponent, nftDetailsBlockComponent = nftDetailsBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { tapHelp(isDisplay = confirmUM.showTapHelp) @@ -79,31 +84,45 @@ private fun LazyListScope.blocks( destinationBlockComponent: DefaultSendDestinationBlockComponent, nftDetailsBlockComponent: NFTDetailsBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, ) { item(key = BLOCKS_KEY) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - AnimatedVisibility( - visible = nftSendUM.confirmUM is ConfirmUM.Success, - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), - ) { - val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } - TransactionDoneTitle( - title = resourceReference(R.string.sent_transaction_sent_title), - subtitle = resourceReference( - R.string.send_date_format, - wrappedList( - wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), - wrappedConfirmUM.transactionDate.toTimeFormat(), - ), - ), - modifier = Modifier.padding(vertical = 12.dp), + if (nftSendUM.isRedesignEnabled) { + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), ) + } else { + TransactionDoneTitleAnimated(nftSendUM = nftSendUM) + destinationBlockComponent.Content(modifier = Modifier) + nftDetailsBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) } - destinationBlockComponent.Content(modifier = Modifier) - - nftDetailsBlockComponent.Content(modifier = Modifier) - - feeBlockComponent.Content(modifier = Modifier) } } +} + +@Composable +private fun TransactionDoneTitleAnimated(nftSendUM: NFTSendUM) { + AnimatedVisibility( + visible = nftSendUM.confirmUM is ConfirmUM.Success, + modifier = Modifier.padding(vertical = 12.dp), + ) { + val wrappedConfirmUM = remember(this) { nftSendUM.confirmUM as ConfirmUM.Success } + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + wrappedConfirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + wrappedConfirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt index 3252c7bc71..5980fa7d22 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/di/NFTSendModelModule.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.send.v2.sendnft.confirm.model.NFTSendConfirmModel import com.tangem.features.send.v2.sendnft.model.NFTSendModel +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -23,4 +24,9 @@ internal interface NFTSendModelModule { @IntoMap @ClassKey(NFTSendConfirmModel::class) fun provideNFTSendConfirmModel(model: NFTSendConfirmModel): Model + + @Binds + @IntoMap + @ClassKey(NFTSendSuccessModel::class) + fun provideNFTSendSuccessModel(model: NFTSendSuccessModel): Model } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 0bebb6f107..90a8753b4a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -29,6 +29,8 @@ import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.CommonSendRoute @@ -36,6 +38,7 @@ import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.sendnft.confirm.NFTSendConfirmComponent +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeComponent import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -70,7 +73,8 @@ internal class NFTSendModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val alertFactory: SendConfirmAlertFactory, -) : Model(), SendNFTComponentCallback { + private val sendFeatureToggles: SendFeatureToggles, +) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -124,7 +128,7 @@ internal class NFTSendModel @Inject constructor( } else { when (currentRouteFlow.value) { is Destination -> router.push(Confirm) - Confirm -> router.push(ConfirmSuccess) + Confirm -> router.replaceAll(ConfirmSuccess) else -> onBackClick() } } @@ -193,6 +197,13 @@ internal class NFTSendModel @Inject constructor( } } + fun showAlertError() { + alertFactory.getGenericErrorState( + onFailedTxEmailClick = ::onFailedTxEmailClick, + popBack = router::pop, + ) + } + private fun onFailedTxEmailClick(errorMessage: String? = null) { saveBlockchainErrorUseCase( error = BlockchainErrorInfo( @@ -249,7 +260,9 @@ internal class NFTSendModel @Inject constructor( private fun initialState(): NFTSendUM = NFTSendUM( destinationUM = DestinationUM.Empty(), feeUM = FeeUM.Empty(), + feeSelectorUM = FeeSelectorUM.Loading, confirmUM = ConfirmUM.Empty, navigationUM = NavigationUM.Empty, + isRedesignEnabled = sendFeatureToggles.isNFTSendRedesignEnabled, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt new file mode 100644 index 0000000000..76f7329658 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/NFTSendSuccessComponent.kt @@ -0,0 +1,96 @@ +package com.tangem.features.send.v2.sendnft.success + +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.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.success.model.NFTSendSuccessModel +import com.tangem.features.send.v2.sendnft.success.ui.NFTSendSuccessContent +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +internal class NFTSendSuccessComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Params, + nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + sendDestinationBlockComponentFactory: SendDestinationBlockComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: NFTSendSuccessModel = getOrCreateModel(params = params) + + private val nftDetailsBlockComponent = nftDetailsBlockComponentFactory.create( + context = child("NFTDetailsSuccessBlock"), + params = NFTDetailsBlockComponent.Params( + userWalletId = params.userWallet.walletId, + nftAsset = params.nftAsset, + nftCollectionName = params.nftCollectionName, + isSuccessScreen = true, + title = resourceReference(R.string.nft_asset), + ), + ) + + private val sendDestinationBlockComponent = sendDestinationBlockComponentFactory.create( + context = child("NFTDestinationSuccessBlock"), + params = DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrency = params.cryptoCurrencyStatus.currency, + blockClickEnableFlow = MutableStateFlow(false), + predefinedValues = PredefinedValues.Empty, + ), + onResult = {}, + onClick = {}, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + NFTSendSuccessContent( + nftSendUM = state, + destinationBlockComponent = sendDestinationBlockComponent, + nftDetailsBlockComponent = nftDetailsBlockComponent, + modifier = modifier, + ) + } + + data class Params( + val nftSendUMFlow: StateFlow, + val analyticsCategoryName: String, + val currentRoute: Flow, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val userWallet: UserWallet, + val nftAsset: NFTAsset, + val nftCollectionName: String, + val txUrl: String, + val callback: ModelCallback, + ) + + interface ModelCallback { + fun onResult(nftSendUM: NFTSendUM) + } + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): NFTSendSuccessComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt new file mode 100644 index 0000000000..9352ec6898 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/model/NFTSendSuccessModel.kt @@ -0,0 +1,107 @@ +package com.tangem.features.send.v2.sendnft.success.model + +import androidx.compose.runtime.Stable +import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +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.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.SendScreenSource +import com.tangem.features.send.v2.common.CommonSendRoute +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.sendnft.success.NFTSendSuccessComponent +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +@Stable +@ModelScoped +internal class NFTSendSuccessModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val appRouter: AppRouter, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, +) : Model() { + private val params: NFTSendSuccessComponent.Params = paramsContainer.require() + + val uiState = params.nftSendUMFlow + + init { + configConfirmSuccessNavigation() + } + + private fun configConfirmSuccessNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).filter { it.second is CommonSendRoute.ConfirmSuccess }.onEach { (state, _) -> + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = stringReference(""), + subtitle = null, + backIconRes = R.drawable.ic_close_24, + backIconClick = { + analyticsEventHandler.send( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = params.analyticsCategoryName, + source = SendScreenSource.Confirm, + isFromSummary = true, + isValid = true, + ), + ) + appRouter.pop() + }, + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_close), + iconRes = null, + isEnabled = true, + isHapticClick = false, + onClick = { + appRouter.pop() + }, + ), + prevButton = null, + secondaryPairButtonsUM = NavigationButton( + textReference = resourceReference(R.string.common_explore), + iconRes = R.drawable.ic_web_24, + onClick = ::onExploreClick, + ) to NavigationButton( + textReference = resourceReference(R.string.common_share), + iconRes = R.drawable.ic_share_24, + onClick = ::onShareClick, + ), + ), + ), + ) + }.launchIn(modelScope) + } + + private fun onExploreClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(params.analyticsCategoryName)) + urlOpener.openUrl(params.txUrl) + } + + private fun onShareClick() { + analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(params.analyticsCategoryName)) + shareManager.shareText(params.txUrl) + } + + interface ModelCallback { + fun onResult(sendUM: SendUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt new file mode 100644 index 0000000000..10fa100bc9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/success/ui/NFTSendSuccessContent.kt @@ -0,0 +1,103 @@ +package com.tangem.features.send.v2.sendnft.success.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButtonsBlockV2 +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toPx +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.nft.component.NFTDetailsBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.common.ui.FeeBlock +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import kotlinx.coroutines.delay + +@Composable +internal fun NFTSendSuccessContent( + nftSendUM: NFTSendUM, + destinationBlockComponent: SendDestinationBlockComponent, + nftDetailsBlockComponent: NFTDetailsBlockComponent, + modifier: Modifier = Modifier, +) { + var visible by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(ANIMATION_DELAY) + visible = true + } + + val height = ANIMATION_OFFSET.toPx().toInt() + + AnimatedVisibility( + visible = visible, + enter = slideInVertically( + initialOffsetY = { height }, + ).plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate success content", + modifier = modifier, + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary), + ) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .scrollable( + state = rememberScrollState(), + orientation = Orientation.Vertical, + ), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (nftSendUM.confirmUM is ConfirmUM.Success) { + TransactionDoneTitle( + title = resourceReference(R.string.sent_transaction_sent_title), + subtitle = resourceReference( + R.string.send_date_format, + wrappedList( + nftSendUM.confirmUM.transactionDate.toTimeFormat(DateTimeFormatters.dateFormatter), + nftSendUM.confirmUM.transactionDate.toTimeFormat(), + ), + ), + modifier = Modifier.padding(vertical = 12.dp), + ) + } + nftDetailsBlockComponent.Content(modifier = Modifier) + destinationBlockComponent.Content(modifier = Modifier) + FeeBlock(feeSelectorUM = nftSendUM.feeSelectorUM) + Spacer(Modifier.height(60.dp)) + } + BottomFade(Modifier.align(Alignment.BottomCenter), TangemTheme.colors.background.tertiary) + NavigationButtonsBlockV2( + navigationUM = nftSendUM.navigationUM, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } + } +} + +private const val ANIMATION_DELAY = 600L +private val ANIMATION_OFFSET = (-40).dp \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt index fb48eed197..45287e3c6e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/ui/state/NFTSendUM.kt @@ -1,13 +1,16 @@ package com.tangem.features.send.v2.sendnft.ui.state import com.tangem.common.ui.navigationButtons.NavigationUM -import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM internal data class NFTSendUM( val destinationUM: DestinationUM, val feeUM: FeeUM, + val feeSelectorUM: FeeSelectorUM, val confirmUM: ConfirmUM, val navigationUM: NavigationUM, + val isRedesignEnabled: Boolean, ) \ No newline at end of file From 0a38083fb7d91c602b401a2b66a2072d9fd653f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 17:23:44 +0300 Subject: [PATCH 65/87] Updated on 2026-08-14 --- .../send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index b9e53dda49..7d5cc74f54 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -8,6 +8,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationTextFieldUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.Companion.NFT_SEND_CATEGORY import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import javax.inject.Inject @@ -34,7 +35,7 @@ internal class NFTSendAnalyticHelper @Inject constructor( Basic.TransactionSent( sentFrom = AnalyticsParam.TxSentFrom.NFT( blockchain = cryptoCurrency.network.name, - token = cryptoCurrency.symbol, + token = NFT_SEND_CATEGORY, // should send "NFT" in token param feeType = feeType, ), memoType = getSendTransactionMemoType(destinationUM?.memoTextField), From ac0302d45249d5664611b7be6c3fed22447b1966 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 21:40:52 +0500 Subject: [PATCH 66/87] Updated on 2026-08-14 --- core/res/src/main/res/values-fr/strings.xml | 38 ++++++++++++++++ core/res/src/main/res/values-ja/strings.xml | 5 ++- .../model/WcSendTransactionModel.kt | 43 ++++++++++++------- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 5996fd51d5..1b64b43e63 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -195,6 +195,7 @@ Rejeter Recharger Renommer + Obligatoire Enregistrez Sauvegarder les modifications Rechercher @@ -1320,27 +1321,55 @@ Le réseau requis %s n’est pas ajouté à votre portefeuille. Ajoutez-le d’abord, puis poursuivez la connexion. Ajouter le réseau au portefeuille Domaine malveillant + Domaine inconnu + Se connecter quand même + Erreur de délai d\'attente. Veuillez réessayer plus tard. + Échec de la connexion à WalletConnect + Ce domaine ne peut pas être vérifié. Vérifiez attentivement la demande avant de l\'approuver. Pour continuer, veuillez reconnecter votre session dApp avec le réseau requis %s. Réseau non connecté Vérifiez votre connexion réseau Délai d’attente de la requête dépassé + Veuillez retourner à votre navigateur et vous reconnecter via WalletConnect. + La session Wallet Connect a été déconnectée. Signer quand même + Code d\'erreur : %s. Si le problème persiste, n\'hésitez pas à contacter notre service d\'assistance. Si le problème persiste, n’hésitez pas à contacter notre support. + Nous avons rencontré une erreur inconnue. Le portefeuille Tangem ne prend actuellement pas en charge %ss dApp non prise en charge Code d\'erreur : 8 005. Si le problème persiste, n\'hésitez pas à contacter notre support. Nous avons rencontré une erreur inconnue Le réseau %s n’est pas pris en charge par Tangem Wallet et ne peut pas être connecté. Réseau non pris en charge + Tangem ne prend actuellement pas en charge le réseau requis par %s. + Réseaux non pris en charge Ce domaine a passé les contrôles de vérification et est considéré comme sûr, fiable et exempt de menaces connues ou d’activités suspectes. + Domaine vérifié + Carte ou bague incorrecte sélectionnée dans l\'application + Nous avons un problème. + Toutes les dApps sont déconnectées Autoriser à dépenser Adresse + Connecter Chargement + Réseau + Réseaux Illimité + Wallet + Application connectée Réseaux connectés + Connecté à %1$s + Consultez le solde et l\'activité de votre portefeuille + Signer des transactions sans vous en informer + Demander l\'accord pour les transactions + Ne pourra pas + Souhaite + Demande de connexion Connexions Contenu Copier les données + dApp déconnectée Déconnecter tout Toutes les sessions dApp seront déconnectées. Votre portefeuille ne sera plus lié à aucune dApp. Déconnecter toutes les dApps @@ -1352,13 +1381,22 @@ La proposition de connexion a expiré Modifications estimées du portefeuille La transaction n\'a pas pu être simulée. Veuillez procéder avec prudence. + Rechargez votre solde pour couvrir les frais de réseau. + %1$s Insuffisant Transaction malveillante + Ajoutez le réseau %s à votre portefeuille pour ce wallet + Le wallet ne nécessite aucun réseaux Nouvelle connexion Connectez votre portefeuille à différentes dApps Aucune séance Des risques potentiels ou un comportement malveillant ont été détectés. Se connecter ou signer des transactions peut entraîner une perte de fonds. + Risque de sécurité connu + Ouvrez l\'application Web3 et sélectionnez l\'option WalletConnect. Demande de Type de signature + Au moins un réseau est requis pour la connexion à une dApp. + Spécifier les réseaux sélectionnés + Signé avec succès À Demande de transaction Demande de transaction diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 7e06ea9819..7f437b9d1e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -71,8 +71,10 @@ 設定に移動して、Tangemアプリで生体認証を有効にします。 生体認証を有効にする %1$sが無効になると、アプリのロックを解除してウォレットを操作するために、パスコードを入力する必要があります。 + 後でウォレットのアクセスコードを入力してもらいます。それを安全に保存し、今後の利用に備えるためです。 これにより、保存されているウォレットアクセスコードがすべて削除されます。ウォレットでの今後の操作には、アクセスコードの送信が必要になります。 保存したデバイスを削除すると、保存されているすべてのウォレットとそのアクセスコードがアプリから削除されます。 + これにより、保存されているウォレットアクセスコードがすべて削除されます。今後ウォレットを操作するには、アクセスコードの送信が必要になります。 アクセスコードを要求する このオプションを選択すると、機密性の高い操作における生体認証が無効になります。取引の署名時などには、毎回アクセスコードの入力が必要になります。 アクセスコードを保存 @@ -486,6 +488,7 @@ 最新の機能とニュースをお届けします シードフレーズのバックアップ モバイルウォレットを作成する + このリカバリーフレーズはすでにインポートされています。 モバイルウォレット この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。 アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。 @@ -1491,7 +1494,7 @@ 使用可能量の設定 dAppが接続解除されました すべての接続を解除する - すべてのdAppセッションが切断されます。ウォレットはどのdAppにも接続されなくなります。 + すべてのdAppセッションの接続が解除されます。ウォレットはどのdAppにもリンクされなくなります。 すべてのdAppを接続解除する 新しいURIで、再度ペアリングを試してください 無効なdAppドメイン diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index c0e035a70e..335160da29 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -27,6 +27,7 @@ import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.error.SendTransactionError.UserCancelledError import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcAnalyticEvents.SignatureRequestReceived.EmulationStatus @@ -346,25 +347,35 @@ internal class WcSendTransactionModel @Inject constructor( } private fun signingIsDone(signState: WcSignState<*>, useCase: WcSignUseCase<*>): Boolean { - (signState.domainStep as? WcSignStep.Result)?.result?.let { - return handleSigningError(it, useCase) + return when (val step = signState.domainStep) { + is WcSignStep.Result -> processResultStep(result = step.result, useCase = useCase) + WcSignStep.PreSign, + WcSignStep.Signing, + -> false } - return false } - private fun handleSigningError(result: Either, useCase: WcSignUseCase<*>): Boolean { - return if (result.isLeft()) { - val error = WcTransactionRoutes.Alert.Type.UnknownError( - errorMessage = result.leftOrNull()?.message(), - onDismiss = { cancel(useCase) }, - onRetry = { signFromAlert() }, - ) - stackNavigation.pushNew(WcTransactionRoutes.Alert(error)) - false - } else { - showSuccessSignMessage() - router.pop() - true + private fun processResultStep(result: Either, useCase: WcSignUseCase<*>): Boolean { + return when (result) { + is Either.Left -> { + val error = result.value + if (error is WcRequestError.WrappedSendError && error.sendTransactionError is UserCancelledError) { + return false + } + + val alertError = WcTransactionRoutes.Alert.Type.UnknownError( + errorMessage = result.value.message(), + onDismiss = { cancel(useCase) }, + onRetry = { signFromAlert() }, + ) + stackNavigation.pushNew(WcTransactionRoutes.Alert(alertError)) + false + } + is Either.Right -> { + showSuccessSignMessage() + router.pop() + true + } } } From eb0738872909b9406c66d5ae22aea6781b6c2615 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Aug 2025 11:40:46 +0400 Subject: [PATCH 67/87] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 60 +++++++------------ .../tangemTech/models/UserTokensResponse.kt | 1 + .../account/GetWalletAccountsResponse.kt | 23 +++++++ .../GetWalletArchivedAccountsResponse.kt | 9 +++ .../account/SaveWalletAccountsResponse.kt | 9 +++ .../models/account/WalletAccountDTO.kt | 17 ++++++ 6 files changed, 79 insertions(+), 40 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 14ec9f906f..51fd1ce19a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -1,9 +1,11 @@ package com.tangem.datasource.api.tangemTech import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.promotion.models.PromotionInfoResponse import com.tangem.datasource.api.promotion.models.StoryContentResponse import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.local.config.providers.models.ProviderModel import retrofit2.http.* @@ -30,9 +32,6 @@ interface TangemTechApi { @Query("limit") limit: Int? = null, ): ApiResponse - @GET("v1/rates") - suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse - @GET("v1/currencies") suspend fun getCurrencyList( @Header("Cache-Control") cacheControl: String = "max-age=600", @@ -68,46 +67,11 @@ interface TangemTechApi { @Query("fields") fields: String, ): ApiResponse - @GET("v1/promotion") - suspend fun getPromotionInfo( - @Query("programName") name: String, - @Header("Cache-Control") cacheControl: String = "max-age=600", - ): ApiResponse - - @GET("v1/settings/{wallet_id}") - suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse - - @PUT("v1/settings/{wallet_id}") - suspend fun saveUserTokensSettings( - @Path("wallet_id") walletId: String, - @Body userTokensSettings: UserTokensSettingsResponse, - ): ApiResponse - @POST("v1/user-network-account") suspend fun createUserNetworkAccount( @Body body: CreateUserNetworkAccountBody, ): ApiResponse - @POST("v1/account") - suspend fun createUserTokensAccount( - @Body body: CreateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}") - suspend fun updateUserTokensAccount( - @Path("account_id") accountId: Int, - @Body body: UpdateUserTokensAccountBody, - ): ApiResponse - - @PUT("v1/account/{account_id}/archive") - suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @PUT("v1/account/{account_id}/unarchive") - suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - - @GET("v1/features") - suspend fun getFeatures(): ApiResponse - @ReadTimeout(duration = 5, unit = TimeUnit.SECONDS) @GET("v1/networks/providers") suspend fun getBlockchainProviders(): Map> @@ -160,7 +124,7 @@ interface TangemTechApi { suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse // endregion - // region wallets + // region user-wallets @PATCH("v1/user-wallets/wallets/{wallet_id}") suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse @@ -176,4 +140,20 @@ interface TangemTechApi { @GET("v1/user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion + + // region account + @GET("/v1/wallets/{walletId}/accounts") + suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse + + @PUT("/v1/wallets/{walletId}/accounts") + suspend fun saveWalletAccounts( + @Path("walletId") walletId: String, + @Header("If-Match") ifMatch: String, + ): ApiResponse + + @GET("/v1/wallets/{walletId}/accounts/archived") + suspend fun getWalletArchivedAccounts( + @Path("walletId") walletId: String, + ): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 2476bf0a31..8e1dec299f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -16,6 +16,7 @@ data class UserTokensResponse( @JsonClass(generateAdapter = true) data class Token( @Json(name = "id") val id: String? = null, + @Json(name = "accountId") val accountId: String? = null, @Json(name = "networkId") val networkId: String, @Json(name = "derivationPath") val derivationPath: String? = null, @Json(name = "name") val name: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt new file mode 100644 index 0000000000..6c3afd812f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletAccountsResponse.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType + +@JsonClass(generateAdapter = true) +data class GetWalletAccountsResponse( + @Json(name = "wallet") val wallet: Wallet, + @Json(name = "accounts") val accounts: List, + @Json(name = "unassignedTokens") val unassignedTokens: List, +) { + + @JsonClass(generateAdapter = true) + data class Wallet( + @Json(name = "version") val version: Int, + @Json(name = "group") val group: GroupType, + @Json(name = "sort") val sort: SortType, + @Json(name = "totalAccounts") val totalAccounts: Int, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt new file mode 100644 index 0000000000..3f1db4c76b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/GetWalletArchivedAccountsResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetWalletArchivedAccountsResponse( + @Json(name = "archivedAccounts") val accounts: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt new file mode 100644 index 0000000000..3f36276519 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SaveWalletAccountsResponse( + @Json(name = "accounts") val accounts: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt new file mode 100644 index 0000000000..343b6cdf2a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/WalletAccountDTO.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.tangemTech.models.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse + +@JsonClass(generateAdapter = true) +data class WalletAccountDTO( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String, + @Json(name = "derivation") val derivationIndex: Int, + @Json(name = "icon") val icon: String, + @Json(name = "iconColor") val iconColor: String, + @Json(name = "tokens") val tokens: List? = null, + @Json(name = "totalTokens") val totalTokens: Int? = null, + @Json(name = "totalNetworks") val totalNetworks: Int? = null, +) \ No newline at end of file From 3f6bcd3a0e80edafa5a59dd73894a083e5ef2c24 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 11:43:43 +0400 Subject: [PATCH 68/87] Updated on 2026-08-14 --- app/build.gradle.kts | 2 +- .../sdk/impl/DefaultTangemSdkManager.kt | 11 ++-- .../domain/sdk/impl/MockTangemSdkManager.kt | 4 +- .../visa/VisaCustomerWalletApproveTask.kt | 33 +++++++----- .../tangem/datasource/api/pay/TangemPayApi.kt | 22 ++++---- .../GenerateNonceByCustomerWalletRequest.kt | 10 ++++ .../GetTokenByCustomerWalletRequest.kt | 12 +++++ data/visa/build.gradle.kts | 1 + .../tangem/data/pay/DefaultKycRepository.kt | 43 ++++++++++------ .../visa/DefaultVisaActivationRepository.kt | 20 ++++---- .../data/visa/DefaultVisaAuthRepository.kt | 33 ++++++++++++ .../model/VisaDataToSignByCustomerWallet.kt | 2 +- .../VisaSignedChallengeByCustomerWallet.kt | 6 +++ .../domain/pay/repository/KycRepository.kt | 5 +- .../visa/repository/VisaAuthRepository.kt | 10 ++++ .../com/tangem/features/kyc/KycComponent.kt | 7 ++- features/kyc/impl/build.gradle.kts | 3 +- .../features/kyc/DefaultKycComponent.kt | 50 +++++++------------ .../tangem/features/kyc/DefaultKycModel.kt | 32 ++++++++++++ .../tangem/features/kyc/di/FeatureModule.kt | 14 ++++++ features/wallet/impl/build.gradle.kts | 1 + .../com/tangem/sdk/api/TangemSdkManager.kt | 4 +- settings.gradle.kts | 2 +- 23 files changed, 226 insertions(+), 101 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt create mode 100644 features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31cb895042..b22a62d437 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -227,8 +227,8 @@ dependencies { implementation(projects.features.usedesk.impl) implementation(projects.features.hotWallet.api) implementation(projects.features.hotWallet.impl) + implementation(projects.features.kyc.api) //TODO disable for release because of the permissions - // implementation(projects.features.kyc.api) // implementation(projects.features.kyc.impl) implementation(projects.features.welcome.api) implementation(projects.features.welcome.impl) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index aaf4f87888..09a9039974 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -24,9 +24,7 @@ import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.* import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse @@ -501,7 +499,12 @@ internal class DefaultTangemSdkManager( ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = VisaCustomerWalletApproveTask( - visaDataForApprove = visaDataForApprove, + VisaCustomerWalletApproveTask.Input( + cardId = visaDataForApprove.customerWalletCardId, + targetAddress = visaDataForApprove.targetAddress, + hashToSign = visaDataForApprove.dataToSign.hashToSign, + sign = visaDataForApprove.dataToSign::sign, + ), ), cardId = visaDataForApprove.customerWalletCardId, initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)), diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index edca4bea1a..1568e7dfc8 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -18,9 +18,7 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index a44a04e051..4fa773c18c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card @@ -10,7 +11,6 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError @@ -22,15 +22,13 @@ import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError -import com.tangem.domain.visa.model.VisaDataForApprove import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet -import com.tangem.domain.visa.model.sign import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand class VisaCustomerWalletApproveTask( - private val visaDataForApprove: VisaDataForApprove, + private val visaDataForApprove: Input, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { @@ -44,7 +42,7 @@ class VisaCustomerWalletApproveTask( return } - if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { + if (visaDataForApprove.cardId != null && card.cardId != visaDataForApprove.cardId) { callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) return } @@ -153,6 +151,12 @@ class VisaCustomerWalletApproveTask( ) } + // TODO: [REDACTED_TASK_KEY] - Get this public function from Blockchain SDK + private fun hashPersonalMessage(message: ByteArray): ByteArray { + val prefix = "\u0019Ethereum Signed Message:\n${message.size}".toByteArray() + return (prefix + message).toKeccak() + } + private fun signApproveData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, @@ -160,10 +164,11 @@ class VisaCustomerWalletApproveTask( session: CardSession, callback: CompletionCallback, ) { - val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes() + val content = "Tangem Pay wants to sign in with your account. Nonce: ${visaDataForApprove.hashToSign}" + val hash = hashPersonalMessage(content.toByteArray(Charsets.UTF_8)) val signTask = SignHashCommand( - hash = hashToSign, + hash = hash, walletPublicKey = targetWalletPublicKey, derivationPath = derivationPath, ) @@ -173,7 +178,7 @@ class VisaCustomerWalletApproveTask( is CompletionResult.Success -> { val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( signature = result.data.signature, - hash = hashToSign, + hash = hash, publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() ?: targetWalletPublicKey.toDecompressedPublicKey(), ).asRSVLegacyEVM().toHexString().lowercase() @@ -181,10 +186,7 @@ class VisaCustomerWalletApproveTask( scanCard( session = session, callback = callback, - signedData = visaDataForApprove.dataToSign.sign( - signature = rsvSignature, - customerWalletAddress = visaDataForApprove.targetAddress, - ), + signedData = visaDataForApprove.sign(rsvSignature, visaDataForApprove.targetAddress), ) } is CompletionResult.Failure -> { @@ -211,4 +213,11 @@ class VisaCustomerWalletApproveTask( } } } + + data class Input( + val cardId: String? = null, + val targetAddress: String, + val hashToSign: String, + val sign: (signature: String, customerWalletAddress: String) -> VisaSignedDataByCustomerWallet, + ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 2749826753..8678e1959c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -1,19 +1,7 @@ package com.tangem.datasource.api.pay import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest -import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest -import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest -import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest -import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest -import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest -import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest -import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest -import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest -import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest +import com.tangem.datasource.api.pay.models.request.* import com.tangem.datasource.api.pay.models.response.* import retrofit2.http.Body import retrofit2.http.GET @@ -33,9 +21,17 @@ interface TangemPayApi { @Body request: GenerateNoneByCardWalletRequest, ): ApiResponse + @POST("v1/auth/challenge") + suspend fun generateNonceByCustomerWallet( + @Body request: GenerateNonceByCustomerWalletRequest, + ): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse + @POST("v1/auth/token") + suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse + @POST("v1/auth/token") suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt new file mode 100644 index 0000000000..edd8260545 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GenerateNonceByCustomerWalletRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GenerateNonceByCustomerWalletRequest( + @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "customer_wallet_address") val customerWalletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt new file mode 100644 index 0000000000..9a5e47e327 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/GetTokenByCustomerWalletRequest.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetTokenByCustomerWalletRequest( + @Json(name = "auth_type") val authType: String = "customer_wallet", + @Json(name = "session_id") val sessionId: String, + @Json(name = "signature") val signature: String, + @Json(name = "message_format") val messageFormat: String, +) \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ff1d5fb9ef..38d1d2957a 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { /** Libs - Tangem */ implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) + implementation(projects.libs.tangemSdkApi) /** DI */ implementation(deps.hilt.core) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt index e42238404f..cc1a69895e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultKycRepository.kt @@ -2,39 +2,52 @@ package com.tangem.data.pay import arrow.core.Either import com.squareup.moshi.Moshi +import com.tangem.common.map import com.tangem.core.error.UniversalError import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter import com.tangem.datasource.di.NetworkMoshi -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.KycStartInfo import com.tangem.domain.pay.repository.KycRepository import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted +import com.tangem.domain.visa.model.VisaDataForApprove +import com.tangem.domain.visa.model.VisaDataToSignByCustomerWallet +import com.tangem.domain.visa.repository.VisaAuthRepository +import com.tangem.sdk.api.TangemSdkManager import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.withContext -@Suppress("UnusedPrivateMember") class DefaultKycRepository @AssistedInject constructor( - @Assisted userWalletId: UserWalletId, @NetworkMoshi moshi: Moshi, private val tangemPayApi: TangemPayApi, - private val dispatcherProvider: CoroutineDispatcherProvider, + private val visaAuthRepository: VisaAuthRepository, + private val tangemSdkManager: TangemSdkManager, ) : KycRepository { private val visaErrorAdapter = VisaErrorResponseJsonAdapter(moshi) - override suspend fun getKycStartInfo(): Either = withContext(dispatcherProvider.io) { - val authTokenForSpecificWallet = "get from userWalletId" - - request { - tangemPayApi.getKycAccess( - authHeader = authTokenForSpecificWallet, - ).getOrThrow().result + override suspend fun getKycStartInfo(address: String, cardId: String): Either { + var authHeader = "" + visaAuthRepository.getCustomerWalletAuthChallenge(address).getOrNull()?.let { result -> + tangemSdkManager.visaCustomerWalletApprove( + VisaDataForApprove( + customerWalletCardId = cardId, + targetAddress = address, + dataToSign = VisaDataToSignByCustomerWallet(hashToSign = result.challenge), + ), + ).map { signResult -> + visaAuthRepository.getTokenWithCustomerWallet( + sessionId = result.session.sessionId, + signature = signResult.signature, + nonce = signResult.dataToSign.hashToSign, + ).getOrNull()?.let { authHeader = it } + } + } + return request { + authHeader.ifEmpty { error("Cannot get auth header for KYC") } + tangemPayApi.getKycAccess(authHeader = authHeader).getOrThrow().result }.map { KycStartInfo( token = it.token, @@ -65,6 +78,6 @@ class DefaultKycRepository @AssistedInject constructor( @AssistedFactory interface Factory : KycRepository.Factory { - override fun create(userWalletId: UserWalletId): DefaultKycRepository + override fun create(): DefaultKycRepository } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 30457ea047..b38b9a82a8 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -131,16 +131,18 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - visaApi.activateByCustomerWallet( - authHeader = authTokens.getAuthHeader(), - body = ActivationByCustomerWalletRequest( - orderId = signedData.dataToSign.request.orderId, - customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( - deployAcceptanceSignature = signedData.signature, - customerWalletAddress = signedData.customerWalletAddress, + signedData.dataToSign.request?.orderId?.let { orderId -> + visaApi.activateByCustomerWallet( + authHeader = authTokens.getAuthHeader(), + body = ActivationByCustomerWalletRequest( + orderId = orderId, + customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( + deployAcceptanceSignature = signedData.signature, + customerWalletAddress = signedData.customerWalletAddress, + ), ), - ), - ).getOrThrow() + ).getOrThrow() + } ?: error("Order Id cannot be null") } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index c9c07624e5..8b3024f1fe 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -65,6 +65,39 @@ internal class DefaultVisaAuthRepository @Inject constructor( } } + override suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.generateNonceByCustomerWallet( + GenerateNonceByCustomerWalletRequest(customerWalletAddress = customerWalletAddress), + ).getOrThrow() + }.map { response -> + VisaAuthChallenge.Wallet( + challenge = response.result.nonce, + session = VisaAuthSession(response.result.sessionId), + ) + } + } + + override suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either = withContext(dispatchers.io) { + request { + visaAuthApi.getTokenByCustomerWallet( + GetTokenByCustomerWalletRequest( + sessionId = sessionId, + signature = signature, + messageFormat = "Tangem Pay wants to sign in with your account. Nonce: $nonce", + ), + ).getOrThrow() + }.map { response -> + "Bearer ${response.result.accessToken}" + } + } + override suspend fun getAccessTokens( signedChallenge: VisaAuthSignedChallenge, ): Either = withContext(dispatchers.io) { diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt index 17de705186..d6c97a1919 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCustomerWallet.kt @@ -4,8 +4,8 @@ import kotlinx.serialization.Serializable @Serializable data class VisaDataToSignByCustomerWallet( - val request: VisaCustomerWalletDataToSignRequest, val hashToSign: String, + val request: VisaCustomerWalletDataToSignRequest? = null, ) fun VisaDataToSignByCustomerWallet.sign(signature: String, customerWalletAddress: String) = diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt new file mode 100644 index 0000000000..7c46288fc1 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedChallengeByCustomerWallet.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaSignedChallengeByCustomerWallet( + val challenge: String, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt index e072914e92..7d46ff2d52 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/KycRepository.kt @@ -3,13 +3,12 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.pay.KycStartInfo -import com.tangem.domain.models.wallet.UserWalletId interface KycRepository { - suspend fun getKycStartInfo(): Either + suspend fun getKycStartInfo(address: String, cardId: String): Either interface Factory { - fun create(userWalletId: UserWalletId): KycRepository + fun create(): KycRepository } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index f7f19ca0e9..098ca44c00 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -18,6 +18,16 @@ interface VisaAuthRepository { cardWalletAddress: String, ): Either + suspend fun getCustomerWalletAuthChallenge( + customerWalletAddress: String, + ): Either + + suspend fun getTokenWithCustomerWallet( + sessionId: String, + signature: String, + nonce: String, + ): Either + suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): Either diff --git a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt index 6a18888000..de05f5dc62 100644 --- a/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt +++ b/features/kyc/api/src/main/kotlin/com/tangem/features/kyc/KycComponent.kt @@ -4,9 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext interface KycComponent { - fun launch() + fun launch(params: Params) interface Factory { fun create(appComponentContext: AppComponentContext): KycComponent } + + data class Params( + val targetAddress: String, + val cardId: String, + ) } \ No newline at end of file diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts index 3e07cc6bd5..1bf8fc66f2 100644 --- a/features/kyc/impl/build.gradle.kts +++ b/features/kyc/impl/build.gradle.kts @@ -13,8 +13,7 @@ android { dependencies { /** Api */ - //TODO disable for release because of the permissions - // implementation(projects.features.kyc.api) + implementation(projects.features.kyc.api) /** Domain */ implementation(projects.domain.visa) diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt index afabc4e184..92d39cee70 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycComponent.kt @@ -1,57 +1,41 @@ package com.tangem.features.kyc import com.sumsub.sns.core.SNSMobileSDK -import com.sumsub.sns.core.data.listener.SNSCompleteHandler import com.sumsub.sns.core.data.listener.TokenExpirationHandler -import com.sumsub.sns.core.data.model.SNSCompletionResult -import com.sumsub.sns.core.data.model.SNSInitConfig -import com.sumsub.sns.core.data.model.SNSSDKState import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.pay.repository.KycRepository -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.kyc.theme.TangemSNSTheme +import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.kyc.theme.TangemSNSIconHandler +import com.tangem.features.kyc.theme.TangemSNSTheme import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import java.util.Locale class DefaultKycComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - private val kycRepositoryFactory: KycRepository.Factory, ) : KycComponent, AppComponentContext by appComponentContext { - private val kycRepository = kycRepositoryFactory.create(UserWalletId("0FFFFF")) + private val model: DefaultKycModel = getOrCreateModel() - override fun launch() { + override fun launch(params: KycComponent.Params) { componentScope.launch { - val startInfo = kycRepository.getKycStartInfo().getOrNull() ?: return@launch - - val tokenExpirationHandler = object : TokenExpirationHandler { - override fun onTokenExpired(): String? { - val newToken = runBlocking { kycRepository.getKycStartInfo().getOrNull()?.token } - return newToken + model.uiState.collect { + it?.let { startInfo -> + val tokenExpirationHandler = object : TokenExpirationHandler { + override fun onTokenExpired() = "" + } + val snsSdk = SNSMobileSDK.Builder(activity) + .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) + .withTheme(TangemSNSTheme.theme(activity)) + .withIconHandler(TangemSNSIconHandler()) + .withLocale(Locale("en")) + .build() + snsSdk.launch() } } - - val snsSdk = SNSMobileSDK.Builder(activity) - .withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler) - .withConf(SNSInitConfig(strings = mapOf())) - .withTheme(TangemSNSTheme.theme(activity)) - .withIconHandler(TangemSNSIconHandler()) - .withLocale(Locale("en")) - .withCompleteHandler( - object : SNSCompleteHandler { - override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) { - } - }, - ) - .build() - - snsSdk.launch() } + model.getKycToken(params) } @AssistedFactory diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt new file mode 100644 index 0000000000..aff81ad334 --- /dev/null +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -0,0 +1,32 @@ +package com.tangem.features.kyc + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.domain.pay.KycStartInfo +import com.tangem.domain.pay.repository.KycRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +class DefaultKycModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + kycRepositoryFactory: KycRepository.Factory, +) : Model() { + + private val kycRepository = kycRepositoryFactory.create() + + private val _uiState: MutableStateFlow = MutableStateFlow(null) + val uiState = _uiState.asStateFlow() + + fun getKycToken(params: KycComponent.Params) { + modelScope.launch { + kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull() + ?.let { _uiState.emit(it) } + } + } +} \ No newline at end of file diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt index c72f282775..a2fefafc7b 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/di/FeatureModule.kt @@ -1,11 +1,16 @@ package com.tangem.features.kyc.di +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model import com.tangem.features.kyc.DefaultKycComponent +import com.tangem.features.kyc.DefaultKycModel import com.tangem.features.kyc.KycComponent import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap @Module @InstallIn(SingletonComponent::class) @@ -13,4 +18,13 @@ internal interface FeatureModule { @Binds fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(DefaultKycModel::class) + fun provideModel(model: DefaultKycModel): Model } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e6d1f16109..5d506674c2 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -113,6 +113,7 @@ dependencies { implementation(projects.features.biometry.api) implementation(projects.features.nft.api) implementation(projects.features.sendV2.api) + implementation(projects.features.kyc.api) /** Common modules */ implementation(projects.common) diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index f15198c725..12aeb18278 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -15,10 +15,8 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.visa.model.VisaActivationInput -import com.tangem.domain.visa.model.VisaDataForApprove -import com.tangem.domain.visa.model.VisaSignedDataByCustomerWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.visa.model.* import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse diff --git a/settings.gradle.kts b/settings.gradle.kts index 2e4a4b6f20..b77ede786a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -260,8 +260,8 @@ include(":features:walletconnect:impl") include(":features:hot-wallet:api") include(":features:hot-wallet:impl") +include(":features:kyc:api") //TODO disable for release because of the permissions -// include(":features:kyc:api") // include(":features:kyc:impl") include(":features:create-wallet-selection:api") From be07bf138cfffb3e6c52354e7da16d5f07679c5a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 14:20:25 +0700 Subject: [PATCH 69/87] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 1 + core/res/src/main/res/values-es/strings.xml | 1 + core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values-uk-rUA/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + ...AddEthereumChainModalBottomSheetContent.kt | 7 +- .../ui/common/WcTransactionRequestButtons.kt | 96 +++++++++++++------ 9 files changed, 79 insertions(+), 31 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index c62cee51c8..ddcad5ed92 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -116,6 +116,7 @@ Nicht genug ADA Akzeptieren Zugang verweigert + Hinzufügen Zum Portfolio hinzufügen Token hinzufügen Vertragsadresse diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c510d098aa..fa2411cdf5 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -113,6 +113,7 @@ ADA insuficiente Aceptar Acceso denegado + Agregar Añadir al portafolio Agregar token Dirección diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 1b64b43e63..e907d8df01 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -95,6 +95,7 @@ ADA insuffisant Accepter Accès refusé + Ajouter Ajouter au portfolio Ajouter un jeton Adresse diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 7f437b9d1e..5ffb365ef3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -149,6 +149,7 @@ ADAが不足しています。 受け入れる アクセスが拒否されました + 追加 ポートフォリオに追加 トークンを追加 アドレス diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 8de88e16ba..befe0fd2d7 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -92,6 +92,7 @@ Недостаточно ADA Принять Доступ запрещен + Добавить Добавить в портфель Добавить токен Адрес diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index ef5cb62c26..5c7d9f4ef4 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -92,6 +92,7 @@ Недостатньо ADA Прийняти Доступ заборонено + Додати Додати у портфель Додати токен Адреса diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5074ca1886..8bda8073a5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -152,6 +152,7 @@ Not enough ADA Accept Access denied + Add Add to portfolio Add token Address diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt index 3f1a55cfe8..73d7ef80e2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/chain/WcAddEthereumChainModalBottomSheetContent.kt @@ -30,8 +30,8 @@ import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.chain.WcAddEthereumChainItemUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM +import com.tangem.features.walletconnect.transaction.ui.common.WcSimpleConfirmButtons import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem -import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import com.tangem.features.walletconnect.transaction.ui.common.WcWalletItem @@ -91,13 +91,12 @@ internal fun WcAddEthereumChainModalBottomSheetContent( } }, footer = { - WcTransactionRequestButtons( + WcSimpleConfirmButtons( modifier = Modifier.padding(16.dp), onDismiss = state.onDismiss, onClickActiveButton = state.onSign, - activeButtonText = resourceReference(R.string.common_sign), + activeButtonText = resourceReference(R.string.common_add), isLoading = state.isLoading, - validationResult = null, ) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index ffd23bc142..61f1cf95e7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.ui.common import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -24,6 +25,73 @@ internal fun WcTransactionRequestButtons( onClickActiveButton: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, +) { + WcCommonButtons( + onDismiss = onDismiss, + modifier = modifier, + primaryButton = { + // Before change, make sure you are align with WcSendTransactionModel::onSign + when (validationResult) { + ValidationResult.UNSAFE, + ValidationResult.WARNING, + -> PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = stringResourceSafe(R.string.common_continue), + onClick = onClickActiveButton, + showProgress = isLoading, + enabled = enabled, + ) + ValidationResult.SAFE, + ValidationResult.FAILED_TO_VALIDATE, + null, + -> PrimaryButtonIconEnd( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = activeButtonText.resolveReference(), + onClick = onClickActiveButton, + iconResId = R.drawable.ic_tangem_24, + showProgress = isLoading, + enabled = enabled, + ) + } + }, + ) +} + +@Composable +internal fun WcSimpleConfirmButtons( + activeButtonText: TextReference, + isLoading: Boolean, + onDismiss: () -> Unit, + onClickActiveButton: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + WcCommonButtons( + onDismiss = onDismiss, + modifier = modifier, + primaryButton = { + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + text = activeButtonText.resolveReference(), + onClick = onClickActiveButton, + showProgress = isLoading, + enabled = enabled, + ) + }, + ) +} + +@Composable +internal fun WcCommonButtons( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + primaryButton: @Composable RowScope.() -> Unit, ) { Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { SecondaryButton( @@ -33,32 +101,6 @@ internal fun WcTransactionRequestButtons( text = stringResourceSafe(R.string.common_cancel), onClick = onDismiss, ) - // Before change, make sure you are align with WcSendTransactionModel::onSign - when (validationResult) { - ValidationResult.UNSAFE, - ValidationResult.WARNING, - -> PrimaryButton( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - text = stringResourceSafe(R.string.common_continue), - onClick = onClickActiveButton, - showProgress = isLoading, - enabled = enabled, - ) - ValidationResult.SAFE, - ValidationResult.FAILED_TO_VALIDATE, - null, - -> PrimaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - text = activeButtonText.resolveReference(), - onClick = onClickActiveButton, - iconResId = R.drawable.ic_tangem_24, - showProgress = isLoading, - enabled = enabled, - ) - } + primaryButton() } } \ No newline at end of file From dc3d109bf8160fbfc76b040695cebd95b8cf3e37 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 09:19:21 +0400 Subject: [PATCH 70/87] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 45 ++-------------- .../domain/tokens/GetTokenListUseCase.kt | 4 +- .../tokens/GetWalletTotalBalanceUseCase.kt | 4 +- .../BaseCurrencyStatusOperations.kt | 4 ++ .../CachedCurrenciesStatusesOperations.kt | 53 ++++++++++++------- 5 files changed, 45 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 089dc2bf5d..4a0a3c5922 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 @@ -18,7 +18,6 @@ import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -97,11 +96,11 @@ internal object TokensDomainModule { @Singleton fun provideGetTokenListUseCase( currenciesRepository: CurrenciesRepository, - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetTokenListUseCase { return GetTokenListUseCase( currenciesRepository = currenciesRepository, - currenciesStatusesOperations = baseCurrenciesStatusesOperations, + currenciesStatusesOperations = currenciesStatusesOperations, ) } @@ -369,9 +368,9 @@ internal object TokensDomainModule { @Provides @Singleton fun provideGetWalletTotalBalanceUseCase( - baseCurrenciesStatusesOperations: BaseCurrenciesStatusesOperations, + currenciesStatusesOperations: BaseCurrencyStatusOperations, ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase(baseCurrenciesStatusesOperations) + return GetWalletTotalBalanceUseCase(currenciesStatusesOperations) } @Provides @@ -399,42 +398,6 @@ internal object TokensDomainModule { return GetCurrencyCheckUseCase(currencyChecksRepository, dispatchers) } - @Provides - @Singleton - fun provideBaseCurrenciesStatusesOperations( - tokensFeatureToggles: TokensFeatureToggles, - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - singleYieldBalanceSupplier: SingleYieldBalanceSupplier, - multiYieldBalanceSupplier: MultiYieldBalanceSupplier, - multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - stakingIdFactory: StakingIdFactory, - ): BaseCurrenciesStatusesOperations { - return CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, - multiYieldBalanceFetcher = multiYieldBalanceFetcher, - tokensFeatureToggles = tokensFeatureToggles, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - ) - } - @Provides @Singleton fun provideBaseCurrencyStatusOperations( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index c1d7cd8cc8..042f4d257f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.transformLatest class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { @OptIn(ExperimentalCoroutinesApi::class) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 1d02262cc7..0fc94390be 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.TokenListFiatBalanceOperations import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -20,7 +20,7 @@ import timber.log.Timber import java.util.concurrent.ConcurrentHashMap class GetWalletTotalBalanceUseCase( - private val currenciesStatusesOperations: BaseCurrenciesStatusesOperations, + private val currenciesStatusesOperations: BaseCurrencyStatusOperations, ) { private val walletBalanceCache = ConcurrentHashMap() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index e39261d0a7..d8d52708eb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -3,6 +3,7 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -28,6 +29,7 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles +import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -56,6 +58,8 @@ abstract class BaseCurrencyStatusOperations( protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator() + abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> + protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> protected abstract suspend fun fetchComponents( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index fcce30d9fd..c82e831875 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -60,19 +60,18 @@ class CachedCurrenciesStatusesOperations( multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val stakingIdFactory: StakingIdFactory, private val tokensFeatureToggles: TokensFeatureToggles, -) : BaseCurrenciesStatusesOperations, - BaseCurrencyStatusOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleYieldBalanceSupplier = singleYieldBalanceSupplier, - multiYieldBalanceSupplier = multiYieldBalanceSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - tokensFeatureToggles = tokensFeatureToggles, - ) { +) : BaseCurrencyStatusOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + singleNetworkStatusSupplier = singleNetworkStatusSupplier, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceSupplier = multiYieldBalanceSupplier, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + stakingIdFactory = stakingIdFactory, + tokensFeatureToggles = tokensFeatureToggles, +) { override fun getCurrenciesStatuses( userWalletId: UserWalletId, @@ -83,6 +82,7 @@ class CachedCurrenciesStatusesOperations( ) } + @Suppress("LongMethod") @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, @@ -163,10 +163,23 @@ class CachedCurrenciesStatusesOperations( .invokeOnCompletion { setFetchFinished(userWalletId) } } + val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) + combine( flow = getQuotes(currenciesIds), - flow2 = getNetworkStatusesUpdates(userWalletId, networks), - flow3 = getYieldsBalancesUpdates(userWalletId, currencies), + flow2 = networksStatusesUpdates, + flow3 = networksStatusesUpdates.flatMapLatest { + val currenciesAddresses = it.getOrElse(default = { emptySet() }) + .mapNotNull { + val currency = currencies.firstOrNull { currency -> currency.network == it.network } + ?: return@mapNotNull null + + currency.id to extractAddress(it) + } + .toMap() + + getYieldsBalancesUpdates(userWalletId, currenciesAddresses) + }, flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -379,24 +392,24 @@ class CachedCurrenciesStatusesOperations( // temporary code because token list is built using networks list private fun getYieldsBalancesUpdates( userWalletId: UserWalletId, - cryptoCurrencies: List, + cryptoCurrencies: Map, ): EitherFlow> { return channelFlow { val state = MutableStateFlow(emptyList()) val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) + stakingIdFactory.create(currencyId = it.key, defaultAddress = it.value) .getOrNull() } - stakingIds.onEach { + stakingIds.onEach { stakingId -> launch { singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = it), + params = SingleYieldBalanceProducer.Params(userWalletId = userWalletId, stakingId = stakingId), ) .onEach { balance -> state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { balance.stakingId == it } + loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId } } } .launchIn(scope = this) From 1563b48e8a76c4fb20700a16694fd217268505f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 14:11:12 +0300 Subject: [PATCH 71/87] Updated on 2026-08-14 --- .../di/UserWalletsListManagerModule.kt | 3 + .../DefaultUserWalletsListRepository.kt | 39 ++++- .../local/preferences/PreferencesKeys.kt | 12 ++ .../data/wallets/di/WalletsDataModule.kt | 8 + ...ltHotWalletAccessCodeAttemptsRepository.kt | 138 ++++++++++++++++++ .../data/wallets/hot/HotWalletAccessor.kt | 35 ++++- .../HotWalletAccessCodeAttemptsRepository.kt | 71 +++++++++ .../wallets/hot/HotWalletPasswordRequester.kt | 34 ++++- .../DefaultHotAccessCodeRequestComponent.kt | 6 +- .../HotAccessCodeRequestModel.kt | 110 +++++++++++++- .../entity/HotAccessCodeRequestUM.kt | 2 + .../proxy/HotWalletPasswordRequesterProxy.kt | 17 +-- .../HotAccessCodeRequestFullScreenContent.kt | 36 ++++- .../welcome/impl/model/WelcomeModel.kt | 8 +- 14 files changed, 485 insertions(+), 34 deletions(-) create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index d8173d8bc7..c521709443 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -14,6 +14,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences @@ -120,6 +121,7 @@ internal object UserWalletsListManagerModule { dispatchers: CoroutineDispatcherProvider, passwordRequester: HotWalletPasswordRequester, appPreferencesStore: AppPreferencesStore, + hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, ): UserWalletsListRepository { val moshi = buildMoshi() val secureStorage = buildSecureStorage(applicationContext = applicationContext) @@ -165,6 +167,7 @@ internal object UserWalletsListManagerModule { tangemSdkManagerProvider = Provider { tangemSdkManager }, appPreferencesStore = appPreferencesStore, savePersistentInformation = ProviderSuspend { true }, // Always save persistent information for now + hotWalletAccessCodeAttemptsRepository = hotWalletAccessCodeAttemptsRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index d900b3f687..93810c48da 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -25,6 +25,8 @@ import com.tangem.domain.core.wallets.error.SetLockError import com.tangem.domain.core.wallets.error.UnlockWalletError import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.UserWalletsListRepository.LockMethod +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.utils.encryptionKey @@ -37,7 +39,7 @@ import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class DefaultUserWalletsListRepository( private val publicInformationRepository: UserWalletsPublicInformationRepository, private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository, @@ -47,6 +49,7 @@ internal class DefaultUserWalletsListRepository( private val tangemSdkManagerProvider: Provider, private val savePersistentInformation: ProviderSuspend, private val appPreferencesStore: AppPreferencesStore, + private val hotWalletAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, ) : UserWalletsListRepository { override val userWallets = MutableStateFlow?>(null) @@ -225,6 +228,7 @@ internal class DefaultUserWalletsListRepository( } val encryptionKey = requestPasswordRecursive( + hotWalletId = userWallet.hotWalletId, block = { password -> runCatching { userWalletEncryptionKeysRepository.getEncryptedWithPassword(userWalletId, password) @@ -241,6 +245,8 @@ internal class DefaultUserWalletsListRepository( return@either } + removePasswordAttempts(userWallet) + sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } .doOnFailure { error -> @@ -282,15 +288,26 @@ internal class DefaultUserWalletsListRepository( val unsecuredKeys = userWalletEncryptionKeysRepository.getAllUnsecured() val allKeys = (biometricKeys + unsecuredKeys).distinct() - val unlockedWallets = allKeys.map { it.walletId } + val unlockedWalletsIds = allKeys.map { it.walletId } + + val unlockedWallets = unlockedWalletsIds.mapNotNull { id -> + userWalletsSync().firstOrNull { it.walletId == id } + } + + // Remove all password attempts for unlocked hot wallets + unlockedWallets.forEach { + removePasswordAttempts(it) + } // if we cant unlock all wallets - if (userWalletIds.all { it in unlockedWallets }.not()) { + if (userWalletIds.all { it in unlockedWalletsIds }.not()) { raise(UnlockWalletError.UnableToUnlock) } sensitiveInformationRepository.getAll(allKeys) - .doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } } + .doOnSuccess { sensitiveInfo -> + userWallets.update { it?.updateWith(sensitiveInfo) } + } .doOnFailure { raise(UnlockWalletError.UnableToUnlock) } } @@ -319,12 +336,16 @@ internal class DefaultUserWalletsListRepository( } private suspend fun requestPasswordRecursive( + hotWalletId: HotWalletId, block: suspend (CharArray) -> UserWalletEncryptionKey?, biometryFallback: suspend () -> Either, ): Either { - val result = passwordRequester.requestPassword( + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts hasBiometry = hasBiometry(), ) + val result = passwordRequester.requestPassword(attemptRequest) return when (result) { HotWalletPasswordRequester.Result.Dismiss -> { @@ -335,7 +356,7 @@ internal class DefaultUserWalletsListRepository( val decrypted = block(result.password.value) if (decrypted == null) { passwordRequester.wrongPassword() - requestPasswordRecursive(block, biometryFallback) + requestPasswordRecursive(hotWalletId, block, biometryFallback) } else { passwordRequester.successfulAuthentication() passwordRequester.dismiss() @@ -353,6 +374,12 @@ internal class DefaultUserWalletsListRepository( } } + private suspend fun removePasswordAttempts(userWallet: UserWallet) { + if (userWallet is UserWallet.Hot) { + hotWalletAccessCodeAttemptsRepository.resetAttempts(userWallet.hotWalletId) + } + } + private suspend fun hasBiometry(): Boolean { val useBiometricAuthentication = appPreferencesStore.getSyncOrDefault( key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, 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 6c0e164b4e..06a5f51102 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 @@ -169,6 +169,18 @@ object PreferencesKeys { fun getShouldShowInitialPermissionScreen(permission: String) = booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") // endregion + + // region Hot Wallet unlock attempts + + fun getHotWalletUnlockAttemptsKey(attemptId: String) = + intPreferencesKey(name = "hotWalletUnlockAttempts_$attemptId") + + fun getHotWalletUnlockBootKey(attemptId: String) = intPreferencesKey(name = "hotWalletUnlockBootCount_$attemptId") + + fun getHotWalletUnlockDeadlineKey(attemptId: String) = + longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId") + + // endregion } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 5564622241..bcaff10b6f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -5,6 +5,7 @@ import com.tangem.data.wallets.DefaultWalletsRepository import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.data.wallets.derivations.DefaultDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository +import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.datastore.RuntimeStateStore @@ -13,6 +14,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -68,4 +70,10 @@ internal interface WalletsDataBindsModule { @Binds @Singleton fun bindColdMapDerivationsRepository(impl: DefaultColdMapDerivationsRepository): ColdMapDerivationsRepository + + @Binds + @Singleton + fun bindHotWalletAccessCodeAttemptsRepository( + impl: DefaultHotWalletAccessCodeAttemptsRepository, + ): HotWalletAccessCodeAttemptsRepository } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..8a77f2f70a --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,138 @@ +package com.tangem.data.wallets.hot + +import android.content.Context +import android.os.SystemClock +import android.provider.Settings +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.COOLDOWN_SECONDS +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_ATTEMPTS_BEFORE_DELETION +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS +import com.tangem.hot.sdk.model.HotWalletId +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@Suppress("MagicNumber") +class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val appPreferencesStore: AppPreferencesStore, +) : HotWalletAccessCodeAttemptsRepository { + + override suspend fun incrementAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + val attemptsKey = PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey()) + + appPreferencesStore.editData { preferences -> + val currentAttempts = preferences[attemptsKey] ?: 0 + val newAttempts = currentAttempts + 1 + + preferences[attemptsKey] = newAttempts + val currentBootCount = currentBootCount() + preferences[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] = currentBootCount + + if (newAttempts >= MAX_FAST_FORWARD_ATTEMPTS) { + val currentDeadline = SystemClock.elapsedRealtime() + COOLDOWN_SECONDS * 1000 + preferences[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] = currentDeadline + } + } + } + + override suspend fun resetAttempts(hotWalletId: HotWalletId) { + val authAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = true, + ) + val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = false, + ) + + appPreferencesStore.editData { + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) + it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + } + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun getAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Flow { + val flow = appPreferencesStore.data.map { + AttemptsPersistentData( + attempts = it[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0, + bootCount = it[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0, + deadline = it[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L, + ) + }.distinctUntilChanged() + + return flow.transformLatest { + while (true) { + emit(toState(id, it.attempts, it.deadline, it.bootCount)) + val remaining = remainingSeconds(it.deadline, it.bootCount) + if (remaining <= 0) break + delay(timeMillis = 1000) + } + }.distinctUntilChanged() + } + + override suspend fun getAttemptsSync(id: HotWalletAccessCodeAttemptsRepository.AttemptId): Attempts { + val prefs = appPreferencesStore.data.first() + val count = prefs[PreferencesKeys.getHotWalletUnlockAttemptsKey(id.attemptIdKey())] ?: 0 + val boot = prefs[PreferencesKeys.getHotWalletUnlockBootKey(id.attemptIdKey())] ?: 0 + val deadline = prefs[PreferencesKeys.getHotWalletUnlockDeadlineKey(id.attemptIdKey())] ?: 0L + return toState(id, count, deadline, boot) + } + + private fun remainingSeconds(deadline: Long, bootStored: Int): Int { + val now = SystemClock.elapsedRealtime() + val bootNow = currentBootCount() + if (bootNow != bootStored) { + // If the boot happened after the last attempt, we consider timer to start from the beginning + return maxOf(0, COOLDOWN_SECONDS - (now / 1000).toInt()) + } + return maxOf(0, ((deadline - now) / 1000).toInt()) + } + + private fun toState( + id: HotWalletAccessCodeAttemptsRepository.AttemptId, + count: Int, + deadlineElapsed: Long, + bootStored: Int, + ): Attempts { + val fast = MAX_FAST_FORWARD_ATTEMPTS + val attention = ATTEMPTS_BEFORE_DELETION + val deletion = MAX_ATTEMPTS_BEFORE_DELETION + + return when { + count < fast -> Attempts.FastForward(count) + id.auth && count >= deletion -> Attempts.Deletion + id.auth && count >= attention -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.BeforeDeletion(count, remaining, deletion - count) + } + else -> { + val remaining = remainingSeconds(deadlineElapsed, bootStored) + Attempts.WithDelay(count, remaining) + } + } + } + + private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { + return "${hotWalletId.value}_$auth" + } + + private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) + + private data class AttemptsPersistentData( + val attempts: Int, + val bootCount: Int, + val deadline: Long, + ) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt index b53cd193d1..b5ca40b2de 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/HotWalletAccessor.kt @@ -33,10 +33,16 @@ class HotWalletAccessor @Inject constructor( val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth - HotWalletId.AuthType.Password -> requestPassword(false) + HotWalletId.AuthType.Password -> requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) HotWalletId.AuthType.Biometry -> { if (isAccessCodeRequired) { - requestPassword(false) + requestPassword( + hotWalletId = hotWalletId, + hasBiometry = false, + ) } else { HotAuth.Biometry } @@ -56,6 +62,7 @@ class HotWalletAccessor @Inject constructor( block: suspend (auth: HotAuth) -> T, ): T { return runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = auth, auth = auth, block = { blockAuth -> @@ -97,6 +104,7 @@ class HotWalletAccessor @Inject constructor( } private suspend fun runCatchingWrongPassInternal( + hotWalletId: HotWalletId, originalAuth: HotAuth, auth: HotAuth, block: suspend (auth: HotAuth) -> T, @@ -105,9 +113,13 @@ class HotWalletAccessor @Inject constructor( }.getOrElse { exception -> if (auth is HotAuth.Biometry && exception.isBiometryError()) { // fallback to password if biometry fails - val passAuth = requestPassword(true) + val passAuth = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = true, + ) return@getOrElse runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = originalAuth, auth = passAuth, block = block, @@ -121,17 +133,28 @@ class HotWalletAccessor @Inject constructor( // If the exception is a wrong password, we need to request the password again hotWalletPasswordRequester.wrongPassword() - val passResult = requestPassword(originalAuth is HotAuth.Biometry) + val passResult = requestPassword( + hotWalletId = hotWalletId, + hasBiometry = originalAuth is HotAuth.Biometry, + ) runCatchingWrongPassInternal( + hotWalletId = hotWalletId, originalAuth = originalAuth, auth = passResult, block = block, ) } - private suspend fun requestPassword(hasBiometry: Boolean): HotAuth { - return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled() + private suspend fun requestPassword(hotWalletId: HotWalletId, hasBiometry: Boolean): HotAuth { + val attemptRequest = HotWalletPasswordRequester.AttemptRequest( + hotWalletId = hotWalletId, + authMode = false, + hasBiometry = hasBiometry, + ) + + return hotWalletPasswordRequester.requestPassword(attemptRequest).toAuth() + ?: throw TangemSdkError.UserCancelled() } private fun Throwable.isBiometryError(): Boolean { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt new file mode 100644 index 0000000000..48c37f6440 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletAccessCodeAttemptsRepository.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.wallets.hot + +import com.tangem.hot.sdk.model.HotWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository for managing access code attempts for hot wallets. + * It tracks the number of attempts made to access a hot wallet and applies cooldowns or deletion + * based on the number of attempts. + */ +interface HotWalletAccessCodeAttemptsRepository { + + /** + * Increments the number of attempts for the given [AttemptId]. + * If the number of attempts exceeds [MAX_FAST_FORWARD_ATTEMPTS], a cooldown period is initiated. + */ + suspend fun incrementAttempts(id: AttemptId) + + /** + * Resets the attempts for the given [HotWalletId]. + * This is typically called when the user successfully authenticates or when the wallet is deleted. + */ + suspend fun resetAttempts(hotWalletId: HotWalletId) + + /** + * Retrieves the current attempts for the given [AttemptId]. + * The result is a flow that emits the current state of attempts. + */ + fun getAttempts(id: AttemptId): Flow + + /** + * Synchronously retrieves the current attempts for the given [AttemptId]. + * This is useful when you need to get the attempts without using a flow. + */ + suspend fun getAttemptsSync(id: AttemptId): Attempts + + data class AttemptId( + val hotWalletId: HotWalletId, + val auth: Boolean, + ) + + sealed interface Attempts { + val count: Int + + data class FastForward( + override val count: Int, + ) : Attempts + + data class WithDelay( + override val count: Int, + val remainingSeconds: Int, + ) : Attempts + + data class BeforeDeletion( + override val count: Int, + val remainingSeconds: Int, + val remainingAttemptsCountBeforeDeletion: Int, + ) : Attempts + + data object Deletion : Attempts { + override val count: Int = MAX_ATTEMPTS_BEFORE_DELETION + } + } + + companion object { + const val COOLDOWN_SECONDS = 60 + const val MAX_FAST_FORWARD_ATTEMPTS = 5 + const val ATTEMPTS_BEFORE_DELETION = 20 + const val MAX_ATTEMPTS_BEFORE_DELETION = 30 + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index 7dcc5fa579..c3f3df8d95 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -1,17 +1,49 @@ package com.tangem.domain.wallets.hot import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId +/** + * Interface for requesting the password for a hot wallet. + * It provides methods to handle password requests, authentication states, and user interactions. + */ interface HotWalletPasswordRequester { + /** + * Sets state to show wrong password state. + */ suspend fun wrongPassword() + /** + * Sets state to show successful authentication state. + */ suspend fun successfulAuthentication() - suspend fun requestPassword(hasBiometry: Boolean): Result + /** + * Requests the user to enter the password for the hot wallet. + * @param attemptRequest Contains information about the hot wallet and authentication mode. + * @return Result of the password request, which can be either a password entry, biometric use, or dismissal. + */ + suspend fun requestPassword(attemptRequest: AttemptRequest): Result + /** + * Dismisses the password request dialog. + */ suspend fun dismiss() + /** + * Represents a request to authenticate with a hot wallet. + * @param hotWalletId The ID of the hot wallet to authenticate with. + * @param authMode Indicates whether the request is for authentication mode. + * In auth mode user can be deleted after failed attempts. + * @param hasBiometry Indicates whether to show biometric authentication option. + */ + data class AttemptRequest( + val hotWalletId: HotWalletId, + val authMode: Boolean, + val hasBiometry: Boolean, + ) + sealed class Result { data object UseBiometry : Result() data object Dismiss : Result() diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt index 6777d34243..4dd7d01895 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -29,8 +29,10 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor( model.successfulAuthentication() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result { - model.show(hasBiometry) + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result { + model.show(attemptRequest) return model.waitResult() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index f210270b41..4a75ad8438 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -3,37 +3,63 @@ package com.tangem.features.hotwallet.accesscoderequest import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.core.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository +import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletPasswordRequester import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM +import com.tangem.features.hotwallet.impl.R import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, + private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { private val result = MutableStateFlow(null) + private val currentRequest = MutableStateFlow(null) + private val attemptsRequestJobHolder = JobHolder() + + private val HotWalletPasswordRequester.AttemptRequest.attemptId + get() = HotWalletAccessCodeAttemptsRepository.AttemptId( + hotWalletId = hotWalletId, + auth = authMode, + ) val uiState: StateFlow field = MutableStateFlow(getInitialState()) - fun dismiss() { - result.value = HotWalletPasswordRequester.Result.Dismiss - dismissState() - } + suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) { + if (userWalletExists(attemptRequest.hotWalletId).not()) { + Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") + result.value = HotWalletPasswordRequester.Result.Dismiss + return + } - fun show(hasBiometry: Boolean) { + currentRequest.value = attemptRequest result.value = null // Reset the result when showing the dialog + subscribeToAttempts(id = attemptRequest.attemptId) uiState.update { it.copy( isShown = true, accessCode = "", - useBiometricVisible = hasBiometry, + useBiometricVisible = attemptRequest.hasBiometry, onAccessCodeChange = ::onAccessCodeChange, ) } @@ -43,7 +69,15 @@ internal class HotAccessCodeRequestModel @Inject constructor( return result.filterNotNull().first().also { result.value = null } } + fun dismiss() { + result.value = HotWalletPasswordRequester.Result.Dismiss + attemptsRequestJobHolder.cancel() + dismissState() + } + suspend fun wrongAccessCode() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.incrementAttempts(currentRequest.attemptId) uiState.update { it.copy( accessCodeColor = PinTextColor.WrongCode, @@ -54,6 +88,8 @@ internal class HotAccessCodeRequestModel @Inject constructor( } suspend fun successfulAuthentication() { + val currentRequest = currentRequest.value ?: return + hotAccessCodeAttemptsRepository.resetAttempts(currentRequest.hotWalletId) uiState.update { it.copy( accessCodeColor = PinTextColor.Success, @@ -92,6 +128,68 @@ internal class HotAccessCodeRequestModel @Inject constructor( } } + private fun subscribeToAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) { + fun remainingSecondsToText(remainingSeconds: Int): TextReference? { + return if (remainingSeconds > 0) { + resourceReference( + R.string.access_code_check_warining_wait, + wrappedList(remainingSeconds), + ) + } else { + null + } + } + + suspend fun collectAttempts(attempts: Attempts) { + when (attempts) { + is Attempts.FastForward -> { + /** ignore */ + } + is Attempts.WithDelay -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + is Attempts.BeforeDeletion -> { + uiState.update { + it.copy( + wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds) + ?: resourceReference( + R.string.access_code_check_warining_delete, + wrappedList(attempts.remainingAttemptsCountBeforeDeletion), + ), + onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 } + ?: {}, + ) + } + } + Attempts.Deletion -> deleteUserWallet() + } + } + + modelScope.launch { + hotAccessCodeAttemptsRepository.getAttempts(id) + .collectLatest { attempts -> collectAttempts(attempts) } + }.saveIn(attemptsRequestJobHolder) + } + + private suspend fun userWalletExists(id: HotWalletId): Boolean { + return userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot && it.hotWalletId == id } + } + + private suspend fun deleteUserWallet() { + val currentRequest = currentRequest.value ?: return + val userWallet = userWalletsListRepository.userWalletsSync() + .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return + userWalletsListRepository.delete(listOf(userWallet.walletId)) + dismiss() + } + private fun dismissState() { uiState.update { it.copy(isShown = false) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt index 62f2b4d051..78c3c269f6 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -1,11 +1,13 @@ package com.tangem.features.hotwallet.accesscoderequest.entity import com.tangem.core.ui.components.fields.PinTextColor +import com.tangem.core.ui.extensions.TextReference internal data class HotAccessCodeRequestUM( val isShown: Boolean = false, val accessCode: String = "", val accessCodeColor: PinTextColor = PinTextColor.Primary, + val wrongAccessCodeText: TextReference? = null, val useBiometricVisible: Boolean = true, val useBiometricClick: () -> Unit = {}, val onAccessCodeChange: (String) -> Unit = {}, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt index b7e7218a48..1968bae87c 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -13,20 +13,15 @@ class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordR val componentRequester = MutableStateFlow(null) - override suspend fun wrongPassword() { - call { wrongPassword() } - } + override suspend fun wrongPassword() = call { wrongPassword() } - override suspend fun successfulAuthentication() { - call { successfulAuthentication() } - } + override suspend fun successfulAuthentication() = call { successfulAuthentication() } - override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result = - call { requestPassword(hasBiometry) } + override suspend fun requestPassword( + attemptRequest: HotWalletPasswordRequester.AttemptRequest, + ): HotWalletPasswordRequester.Result = call { requestPassword(attemptRequest) } - override suspend fun dismiss() { - call { dismiss() } - } + override suspend fun dismiss() = call { dismiss() } private suspend fun call(block: suspend HotWalletPasswordRequester.() -> T): T { return withTimeout(timeMillis = 1000) { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index 9ae3230ec4..cc30961992 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -13,6 +13,8 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.LineBreak +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SecondaryButton @@ -22,6 +24,8 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager @@ -89,6 +93,33 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) + + SpacerH(20.dp) + + AnimatedVisibility( + modifier = Modifier.animateEnterExit( + enter = slideInVertically( + tween(), + initialOffsetY = { it + 200 }, + ) + fadeIn(tween()), + exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), + ), + visible = state.wrongAccessCodeText != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + val wrongAccessCodeText = + state.wrongAccessCodeText ?: return@AnimatedVisibility + + Text( + text = wrongAccessCodeText.resolveReference(), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2.copy( + lineBreak = LineBreak.Heading, + ), + color = TangemTheme.colors.text.warning, + ) + } } if (state.useBiometricVisible) { @@ -132,7 +163,10 @@ private fun Preview() { var isShown by remember { mutableStateOf(true) } HotAccessCodeRequestFullScreenContent( - state = HotAccessCodeRequestUM(isShown = isShown), + state = HotAccessCodeRequestUM( + isShown = isShown, + wrongAccessCodeText = stringReference("Wrong access code"), + ), modifier = Modifier, ) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 1901cc427a..8ef83bf9ec 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -76,7 +76,13 @@ internal class WelcomeModel @Inject constructor( launch { walletsFetcher.userWallets - .collectLatest { wallets.value = it } + .collectLatest { + if (it.isEmpty()) { + router.replaceAll(AppRoute.Home()) + } + + wallets.value = it + } } tryToUnlockRightAway() From c63d35bb8b48af04d9a09a16bc240414d9c59d97 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 16:13:28 +0500 Subject: [PATCH 72/87] Updated on 2026-08-14 --- .../hotwallet/accesscode/ui/AccessCode.kt | 2 +- .../model/AddExistingWalletImportModel.kt | 35 ++++++++++++++----- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt index 230f97bab4..3924d978c7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/ui/AccessCode.kt @@ -74,7 +74,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) { ) { PinTextField( length = state.accessCodeLength, - isPasswordVisual = true, + isPasswordVisual = !state.isConfirmMode, value = state.accessCode, pinTextColor = PinTextColor.Primary, onValueChange = state.onAccessCodeChange, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index d495361f44..60f3305500 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -12,8 +12,10 @@ import com.tangem.core.ui.components.bottomsheets.message.infoBlock import com.tangem.core.ui.components.bottomsheets.message.onClick import com.tangem.core.ui.components.bottomsheets.message.secondaryButton import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic +import com.tangem.domain.core.wallets.error.SaveWalletError import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.MnemonicRepository @@ -83,26 +85,41 @@ internal class AddExistingWalletImportModel @Inject constructor( @Suppress("UnusedPrivateMember") private fun importWallet(mnemonic: Mnemonic, passphrase: String?) { modelScope.launch { - uiState.update { - it.copy(importWalletProgress = true) - } + setImportProgress(true) runCatching { val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth) val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() - saveUserWalletUseCase(userWallet.copy(backedUp = true)) - params.callbacks.onWalletImported(userWallet.walletId) + saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true)) + .onLeft { + setImportProgress(false) + when (it) { + is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + is SaveWalletError.WalletAlreadySaved -> { + uiMessageSender.send( + SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), + ) + } + } + } + .onRight { + setImportProgress(false) + params.callbacks.onWalletImported(userWallet.walletId) + } }.onFailure { Timber.e(it) - - uiState.update { - it.copy(importWalletProgress = false) - } + setImportProgress(false) } } } + private fun setImportProgress(progress: Boolean) { + uiState.update { + it.copy(importWalletProgress = progress) + } + } + private fun onPassphraseInfoClick() { uiMessageSender.send(passphraseInfoAlertBS) } From 11eb1bbbdca87365ee10435873c73878218d5f24 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 14:31:47 +0300 Subject: [PATCH 73/87] Updated on 2026-08-14 --- .../com/tangem/tap/routing/utils/ChildFactory.kt | 5 +---- .../kotlin/com/tangem/common/routing/AppRoute.kt | 2 ++ .../ui/HotAccessCodeRequestFullScreenContent.kt | 5 ++++- .../com/tangem/features/welcome/WelcomeComponent.kt | 9 +-------- .../features/welcome/impl/DefaultWelcomeComponent.kt | 6 +++--- .../features/welcome/impl/model/WelcomeModel.kt | 2 -- .../features/welcome/impl/ui/AddWalletBottomSheet.kt | 11 ++++++----- .../features/welcome/impl/ui/WelcomeSelectWallet.kt | 12 ++++++++---- 8 files changed, 25 insertions(+), 27 deletions(-) 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 d4b9a8ee88..49bb638a53 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 @@ -149,10 +149,7 @@ internal class ChildFactory @Inject constructor( if (hotWalletFeatureToggles.isHotWalletEnabled) { createComponentChild( context = context, - params = NewWelcomeComponent.Params( - launchMode = route.launchMode, - intent = route.intent, - ), + params = Unit, componentFactory = newWelcomeComponentFactory, ) } else { 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 719d1dd05d..f797561ced 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 @@ -30,8 +30,10 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Welcome( + @Deprecated("No longer used, will be removed in future releases") val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard, // we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware + @Deprecated("No longer used, will be removed in future releases") val intent: SerializableIntent? = null, ) : AppRoute(path = "/welcome"), RouteBundleParams { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index cc30961992..2e3b61e43d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -129,7 +129,10 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM .fillMaxWidth() .navigationBarsPadding() .imePadding(), - text = "Use biometric", + text = stringResourceSafe( + id = R.string.welcome_unlock, + stringResourceSafe(R.string.common_biometrics), + ), onClick = state.useBiometricClick, ) } diff --git a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt index 6d9044f216..bc7092a29c 100644 --- a/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt +++ b/features/welcome/api/src/main/kotlin/com/tangem/features/welcome/WelcomeComponent.kt @@ -1,16 +1,9 @@ package com.tangem.features.welcome -import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface WelcomeComponent : ComposableContentComponent { - data class Params( - val launchMode: InitScreenLaunchMode, - val intent: SerializableIntent?, - ) - - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt index 7bc6a1d982..8638078499 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/DefaultWelcomeComponent.kt @@ -15,10 +15,10 @@ import dagger.assisted.AssistedInject internal class DefaultWelcomeComponent @AssistedInject constructor( @Assisted context: AppComponentContext, - @Assisted params: WelcomeComponent.Params, + @Assisted val params: Unit, ) : WelcomeComponent, AppComponentContext by context { - private val model: WelcomeModel = getOrCreateModel(params) + private val model: WelcomeModel = getOrCreateModel() @Composable override fun Content(modifier: Modifier) { @@ -32,6 +32,6 @@ internal class DefaultWelcomeComponent @AssistedInject constructor( @AssistedFactory interface Factory : WelcomeComponent.Factory { - override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent + override fun create(context: AppComponentContext, params: Unit): DefaultWelcomeComponent } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 8ef83bf9ec..c189ac83b5 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -48,8 +48,6 @@ internal class WelcomeModel @Inject constructor( private val walletsRepository: WalletsRepository, ) : Model() { - // TODO add intent handling - // val params val uiState: StateFlow field = MutableStateFlow(WelcomeUM.Plain) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt index 4331461929..5d26549787 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt @@ -12,16 +12,17 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM @Composable fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, - titleText = TextReference.Str("Add Wallet"), + titleText = resourceReference(R.string.auth_info_add_wallet_title), containerColor = TangemTheme.colors.background.tertiary, content = { Content(it) }, ) @@ -38,7 +39,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { ), ) { InputRowDefault( - text = TextReference.Str("Create New Wallet"), + text = resourceReference(R.string.home_button_create_new_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 0, @@ -49,7 +50,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, ) InputRowDefault( - text = TextReference.Str("Add Existing Wallet"), + text = resourceReference(R.string.home_button_add_existing_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 1, @@ -60,7 +61,7 @@ private fun Content(content: AddWalletBottomSheetContentUM) { .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, ) InputRowDefault( - text = TextReference.Str("Buy Tangem Wallet"), + text = resourceReference(R.string.details_buy_wallet), modifier = Modifier .roundedShapeItemDecoration( currentIndex = 2, diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index 305b00750c..9b3bba406d 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -22,6 +22,7 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.* import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R import com.tangem.features.welcome.impl.ui.state.WelcomeUM @@ -78,7 +79,10 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal .padding(16.dp) .navigationBarsPadding() .animateEnterExit(fadeIn(), fadeOut()), - text = "Unlock all with biometric", + text = stringResourceSafe( + R.string.user_wallet_list_unlock_all_with, + stringResourceSafe(id = R.string.common_biometrics), + ), onClick = state.onUnlockWithBiometricClick, ) } @@ -122,7 +126,7 @@ private fun AnimatedContentScope.TopBar(state: WelcomeUM.SelectWallet, modifier: TextButton( modifier = Modifier.clip(TangemTheme.shapes.roundedCornersLarge), - text = "Add Wallet", + text = stringResourceSafe(R.string.auth_info_add_wallet_title), colors = TangemButtonsDefaults.defaultTextButtonColors.copy( contentColor = TangemTheme.colors.text.primary1, ), @@ -146,7 +150,7 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Welcome back!", + text = stringResourceSafe(R.string.auth_info_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -161,7 +165,7 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { ) + fadeIn(tween(delayMillis = 300)), exit = fadeOut(), ), - text = "Select a wallet to log in", + text = stringResourceSafe(R.string.auth_info_subtitle), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, ) From be5786260aa9beb7581e82890cd8597e7be25229 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 15:25:19 +0300 Subject: [PATCH 74/87] Updated on 2026-08-14 --- app/build.gradle.kts | 2 +- .../tangem/common/rules/ApiEnvironmentRule.kt | 1 + .../screens/StakingDetailsPageObject.kt | 117 ++++++ .../screens/StakingSendDetailsPageObject.kt | 51 +++ .../tangem/screens/StakingSendPageObject.kt | 78 ++++ .../com/tangem/screens/SwapTokenPageObject.kt | 7 +- .../tangem/screens/TokenDetailsPageObject.kt | 60 ++- .../kotlin/com/tangem/tests/BuyTokenTest.kt | 7 - .../com/tangem/tests/OrganizeTokensTest.kt | 5 + .../kotlin/com/tangem/tests/StakingTest.kt | 379 ++++++++++++++++++ .../common/ui/amountScreen/ui/AmountBlock.kt | 8 +- .../ui/amountScreen/ui/AmountButtons.kt | 12 +- .../common/ui/amountScreen/ui/AmountField.kt | 5 +- .../amountScreen/ui/AmountFieldContainer.kt | 8 +- .../NavigationButtonsBlock.kt | 5 +- core/datasource/build.gradle.kts | 2 +- .../datasource/api/common/config/StakeKit.kt | 35 +- .../datasource/di/utils/RetrofitApiBuilder.kt | 3 +- .../ui/components/fields/AmountTextField.kt | 6 +- .../ui/components/inputrow/InputRowDefault.kt | 9 +- .../ui/components/rows/RoundableCornersRow.kt | 7 +- .../tangem/core/ui/test/BaseBlockTestTags.kt | 7 + .../ui/test/StakingDetailsScreenTestTags.kt | 15 + .../test/StakingSendDetailsScreenTestTags.kt | 10 + .../core/ui/test/StakingSendScreenTestTags.kt | 16 + .../core/ui/test/SwapTokenScreenTestTags.kt | 1 - .../ui/test/TokenDetailsScreenTestTags.kt | 12 + .../ui/StakingInitialInfoContent.kt | 12 +- .../impl/presentation/ui/StakingScreen.kt | 5 +- .../impl/presentation/ui/StakingTosText.kt | 4 + .../presentation/ui/block/StakingFeeBlock.kt | 5 +- .../presentation/ui/block/ValidatorBlock.kt | 5 +- .../components/staking/StakingBalanceBlock.kt | 11 +- .../components/staking/TokenStakingBlock.kt | 11 +- 34 files changed, 873 insertions(+), 48 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b22a62d437..4de1a30e45 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -349,7 +349,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt index 0b0f1a32d0..a8d1a2c038 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt @@ -127,6 +127,7 @@ class ApiEnvironmentRule : TestRule { ApiConfig.ID.TangemTech, ApiConfig.ID.Express, ApiConfig.ID.TangemPay, + ApiConfig.ID.StakeKit, ) } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt new file mode 100644 index 0000000000..7e118e51b1 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingDetailsPageObject.kt @@ -0,0 +1,117 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.* +import com.tangem.features.tokendetails.impl.R +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.staking.impl.R as StakingImplR +import androidx.compose.ui.test.hasTestTag as withTestTag + +class StakingDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) + } + + val stakingTitle: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val bannerImage: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.BANNER_IMAGE) + useUnmergedTree = true + } + + val bannerText: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.BANNER_TEXT) + useUnmergedTree = true + } + + val annualPercentageRate: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_annual_percentage_rate)) + useUnmergedTree = true + } + + val availableBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_available)) + useUnmergedTree = true + + } + + val unbondingPeriodBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_unbonding_period)) + useUnmergedTree = true + } + + val rewardClaimingBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_reward_claiming)) + useUnmergedTree = true + } + + val rewardScheduleBlock: KNode = child { + hasParent(withTestTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK)) + hasTestTag(StakingDetailsScreenTestTags.PARAMETER_NAME) + hasText(getResourceString(StakingImplR.string.staking_details_reward_schedule)) + useUnmergedTree = true + } + + val rewardsBlock: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK) + useUnmergedTree = true + } + + val rewardsBlockTitle: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK_TITLE) + useUnmergedTree = true + } + + val rewardsBlockText: KNode = child { + hasTestTag(BaseBlockTestTags.BLOCK_TEXT) + useUnmergedTree = true + } + + val yourStakesTitle: KNode = child { + hasText(getResourceString(StakingImplR.string.staking_your_stakes)) + useUnmergedTree = true + } + + val activeStakingBlock: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK) + useUnmergedTree = true + } + + val toSText: KNode = child { + hasTestTag(StakingDetailsScreenTestTags.TOS_TEXT) + useUnmergedTree = true + } + + val stakeMoreButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.staking_stake_more)) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingDetailsScreen(function: StakingDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt new file mode 100644 index 0000000000..79b38d7c9c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendDetailsPageObject.kt @@ -0,0 +1,51 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class StakingSendDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val primaryAmount: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT) + useUnmergedTree = true + } + + val secondaryAmount: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + val validatorBlock: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK) + useUnmergedTree = true + } + + val networkFeeBlock: KNode = child { + hasTestTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingSendDetailsScreen(function: StakingSendDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt new file mode 100644 index 0000000000..f71c9ad210 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/StakingSendPageObject.kt @@ -0,0 +1,78 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.StakingSendScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.features.send.v2.impl.R as SendR +import androidx.compose.ui.test.hasTestTag as withTestTag + +class StakingSendPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val screenContainer: KNode = child { + hasTestTag(StakingSendScreenTestTags.SCREEN_CONTAINER) + } + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + useUnmergedTree = true + } + + val amountContainerTitle: KNode = child { + hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE) + useUnmergedTree = true + } + + val amountContainerText: KNode = child { + hasTestTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT) + useUnmergedTree = true + } + + val amountInputTextField: KNode = child { + hasTestTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD) + useUnmergedTree = true + } + + val secondaryAmount: KNode = child { + hasTestTag(StakingSendScreenTestTags.SECONDARY_AMOUNT) + useUnmergedTree = true + } + + val currencyButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(StakingSendScreenTestTags.CURRENCY_ICON)) + useUnmergedTree = true + } + + val fiatButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.CURRENCY_BUTTON) + hasAnyChild(withTestTag(StakingSendScreenTestTags.FIAT_ICON)) + useUnmergedTree = true + } + + val maxButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.MAX_BUTTON) + useUnmergedTree = true + } + + val previousButton: KNode = child { + hasTestTag(StakingSendScreenTestTags.PREVIOUS_BUTTON) + useUnmergedTree = true + } + + val nextButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(SendR.string.common_next)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onStakingSendScreen(function: StakingSendPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 3ba4c711b5..dd53bc1dc5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -3,10 +3,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.R -import com.tangem.core.ui.test.BaseButtonTestTags -import com.tangem.core.ui.test.NotificationTestTags -import com.tangem.core.ui.test.SwapTokenScreenTestTags -import com.tangem.core.ui.test.TopAppBarTestTags +import com.tangem.core.ui.test.* import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode @@ -32,7 +29,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } val networkFeeBlock: KNode = child { - hasTestTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK) + hasTestTag(BaseBlockTestTags.BLOCK) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt index e6bb7ddeb4..128a24ad03 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/TokenDetailsPageObject.kt @@ -4,15 +4,16 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.TokenDetailsScreenTestTags -import com.tangem.core.ui.utils.LazyListItemPositionSemantics import com.tangem.features.tokendetails.impl.R +import com.tangem.core.ui.utils.LazyListItemPositionSemantics import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode -import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -21,6 +22,61 @@ class TokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvide hasTestTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER) } + val availableStakingBlock: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK) + useUnmergedTree = true + } + + val stakingBlock: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_BLOCK) + useUnmergedTree = true + } + + val availableStakingBlockTitle: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE) + useUnmergedTree = true + } + + val availableStakingBlockText: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT) + useUnmergedTree = true + } + + val availableStakingBlockCurrencyIcon: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON) + useUnmergedTree = true + } + + val stakeButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_stake)) + useUnmergedTree = true + } + + val stakingFiatAmount: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT) + useUnmergedTree = true + } + + val stakingDot: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_DOT) + useUnmergedTree = true + } + + val stakingTokenAmount: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT) + useUnmergedTree = true + } + + val stakingChevronIcon: KNode = child { + hasTestTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON) + useUnmergedTree = true + } + + val stakingTitle: KNode = child { + hasText(getResourceString(R.string.staking_native)) + } + val title: KNode = child { hasTestTag(TokenDetailsScreenTestTags.TOKEN_TITLE) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index 62365124a9..f553c1cbe0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -371,13 +371,6 @@ class BuyTokenTest : BaseTestCase() { step("Open 'Select Provider' bottom sheet") { onBuyTokenDetailsScreen { providerTitle.performClick() } } - step("Assert unavailable provider name is displayed") { - onSelectProviderBottomSheet { - flakySafely(WAIT_UNTIL_TIMEOUT) { - unavailableProviderItem.assertIsDisplayed() - } - } - } step("Assert available provider name is displayed") { onSelectProviderBottomSheet { flakySafely(WAIT_UNTIL_TIMEOUT) { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index 44cab70be3..76d8e34a7a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -31,6 +31,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -55,6 +56,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -105,6 +107,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -140,6 +143,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } @@ -192,6 +196,7 @@ class OrganizeTokensTest : BaseTestCase() { } step("Swipe to 'Organize tokens' button") { swipeUp() + swipeUp() } step("Click 'Organize tokens' button") { onMainScreen { organizeTokensButton().clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt new file mode 100644 index 0000000000..083478f475 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt @@ -0,0 +1,379 @@ +package com.tangem.tests + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.TOTAL_BALANCE +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.OpenMainScreenScenario +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class StakingTest : BaseTestCase() { + + @AllureId("3558") + @DisplayName("Staking: validate staking block on 'Token details' screen") + @Test + fun validateStakingBlockTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Staked" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Staking block' is displayed") { + onTokenDetailsScreen { stakingBlock.assertIsDisplayed() } + } + step("Assert 'Staking title' is displayed") { + onTokenDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert 'Staking fiat amount' is displayed") { + onTokenDetailsScreen { stakingFiatAmount.assertIsDisplayed() } + } + step("Assert 'Staking dot' is displayed") { + onTokenDetailsScreen { stakingDot.assertIsDisplayed() } + } + step("Assert 'Staking token amount' is displayed") { + onTokenDetailsScreen { stakingTokenAmount.assertIsDisplayed() } + } + step("Assert 'Staking block chevron icon' is displayed") { + onTokenDetailsScreen { stakingChevronIcon.assertIsDisplayed() } + } + } + } + + @AllureId("3550") + @DisplayName("Staking: validate staking more screens") + @Test + fun validateStakingMoreScreensTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Staked" + val stakingAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Click on 'Staking block'") { + onTokenDetailsScreen { stakingBlock.clickWithAssertion() } + } + step("Assert 'Title' is displayed") { + onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert 'Annual percentage rate' is displayed") { + onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() } + } + step("Assert 'Available' block is displayed") { + onStakingDetailsScreen { availableBlock.assertIsDisplayed() } + } + step("Assert 'Unbonding Period' block is displayed") { + onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() } + } + step("Assert 'Reward claiming' block is displayed") { + onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() } + } + step("Assert 'Reward schedule' block is displayed") { + onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() } + } + step("Assert 'Rewards block' is displayed") { + onStakingDetailsScreen { rewardsBlock.assertIsDisplayed() } + } + step("Assert 'Rewards block' title is displayed") { + onStakingDetailsScreen { rewardsBlockTitle.assertIsDisplayed() } + } + step("Assert 'Rewards block' text is displayed") { + onStakingDetailsScreen { rewardsBlockText.assertIsDisplayed() } + } + step("Assert 'Active staking block' is displayed") { + onStakingDetailsScreen { activeStakingBlock.assertIsDisplayed() } + } + step("Assert 'Your stakes' title is displayed") { + onStakingDetailsScreen { yourStakesTitle.assertIsDisplayed() } + } + step("Assert 'ToS' text is displayed") { + onStakingDetailsScreen { toSText.assertIsDisplayed() } + } + step("Assert 'Stake more' button is displayed") { + onStakingDetailsScreen { stakeMoreButton.assertIsDisplayed() } + } + step("Click 'Stake more' button") { + onStakingDetailsScreen { stakeMoreButton.performClick() } + } + step("Assert 'Send' screen is displayed") { + onStakingSendScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Send' screen title is displayed") { + onStakingSendScreen { title.assertIsDisplayed() } + } + step("Assert amount container title is displayed") { + onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + } + step("Assert amount container text is displayed") { + onStakingSendScreen { amountContainerText.assertIsDisplayed() } + } + step("Assert input text field is displayed") { + onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + } + step("Type '$stakingAmount' in input text field") { + onStakingSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(stakingAmount) + } + } + step("Assert input text field has value: '$stakingAmount'") { + onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert 'Max' button is displayed") { + onStakingSendScreen { maxButton.assertIsDisplayed() } + } + step("Assert previous button is displayed") { + onStakingSendScreen { previousButton.assertIsDisplayed() } + } + step("Assert 'Next' button is displayed") { + onStakingSendScreen { nextButton.assertIsDisplayed() } + } + step("Click on 'Next' button") { + onStakingSendScreen { nextButton.performClick() } + } + step("Assert 'Send details' screen title is displayed") { + onStakingSendDetailsScreen { title.assertIsDisplayed() } + } + step("Assert primary amount is displayed") { + onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + } + step("Assert 'Validator' block is displayed") { + onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + } + step("Assert 'Network Fee' block is displayed") { + onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + } + } + } + + @AllureId("3548") + @DisplayName("Staking: validate staking screens") + @Test + fun validateStakingScreensTest() { + val tokenTitle = "POL (ex-MATIC)" + val balance = TOTAL_BALANCE + val scenarioName = "staking_eth_pol_balances_android" + val scenarioState = "Started" + val stakingAmount = "1" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + scenario(OpenMainScreenScenario(composeTestRule)) + } + step("Click on 'Synchronize addresses' button") { + onMainScreen { synchronizeAddressesButton.clickWithAssertion() } + } + step("Assert wallet balance = $balance") { + onMainScreen { walletBalance().assertTextContains(balance) } + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Available staking block' is displayed") { + onTokenDetailsScreen { availableStakingBlock.assertIsDisplayed() } + } + step("Assert 'Available staking block' title is displayed") { + onTokenDetailsScreen { availableStakingBlockTitle.assertIsDisplayed() } + } + step("Assert 'Available staking block' text is displayed") { + onTokenDetailsScreen { availableStakingBlockText.assertIsDisplayed() } + } + step("Assert 'Available staking block' currency icon is displayed") { + onTokenDetailsScreen { availableStakingBlockCurrencyIcon.assertIsDisplayed() } + } + step("Click on 'Stake' button") { + onTokenDetailsScreen { stakeButton.clickWithAssertion() } + } + step("Assert 'Title' is displayed") { + onStakingDetailsScreen { stakingTitle.assertIsDisplayed() } + } + step("Assert banner image is displayed") { + onStakingDetailsScreen { bannerImage.assertIsDisplayed() } + } + step("Assert banner text is displayed") { + onStakingDetailsScreen { bannerText.assertIsDisplayed() } + } + step("Assert 'Annual percentage rate' is displayed") { + onStakingDetailsScreen { annualPercentageRate.assertIsDisplayed() } + } + step("Assert 'Available' block is displayed") { + onStakingDetailsScreen { availableBlock.assertIsDisplayed() } + } + step("Assert 'Unbonding Period' block is displayed") { + onStakingDetailsScreen { unbondingPeriodBlock.assertIsDisplayed() } + } + step("Assert 'Reward claiming' block is displayed") { + onStakingDetailsScreen { rewardClaimingBlock.assertIsDisplayed() } + } + step("Assert 'Reward schedule' block is displayed") { + onStakingDetailsScreen { rewardScheduleBlock.assertIsDisplayed() } + } + step("Assert 'ToS' text is displayed") { + onStakingDetailsScreen { toSText.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingDetailsScreen { stakeButton.assertIsDisplayed() } + } + step("Click 'Stake' button") { + onStakingDetailsScreen { stakeButton.performClick() } + } + step("Assert 'Send' screen is displayed") { + onStakingSendScreen { screenContainer.assertIsDisplayed() } + } + step("Assert 'Send' screen title is displayed") { + onStakingSendScreen { title.assertIsDisplayed() } + } + step("Assert amount container title is displayed") { + onStakingSendScreen { amountContainerTitle.assertIsDisplayed() } + } + step("Assert amount container text is displayed") { + onStakingSendScreen { amountContainerText.assertIsDisplayed() } + } + step("Assert input text field is displayed") { + onStakingSendScreen { amountInputTextField.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendScreen { secondaryAmount.assertIsDisplayed() } + } + step("Type '$stakingAmount' in input text field") { + onStakingSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(stakingAmount) + } + } + step("Assert input text field has value: '$stakingAmount'") { + onStakingSendScreen { amountInputTextField.assertTextContains(value = stakingAmount, substring = true) } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert currency button is displayed") { + onStakingSendScreen { currencyButton.assertIsDisplayed() } + } + step("Assert fiat button is displayed") { + onStakingSendScreen { fiatButton.assertIsDisplayed() } + } + step("Assert 'Max' button is displayed") { + onStakingSendScreen { maxButton.assertIsDisplayed() } + } + step("Assert previous button is displayed") { + onStakingSendScreen { previousButton.assertIsDisplayed() } + } + step("Assert 'Next' button is displayed") { + onStakingSendScreen { nextButton.assertIsDisplayed() } + } + step("Click on 'Next' button") { + onStakingSendScreen { nextButton.performClick() } + } + step("Assert 'Send details' screen title is displayed") { + onStakingSendDetailsScreen { title.assertIsDisplayed() } + } + step("Assert primary amount is displayed") { + onStakingSendDetailsScreen { primaryAmount.assertIsDisplayed() } + } + step("Assert secondary amount is displayed") { + onStakingSendDetailsScreen { secondaryAmount.assertIsDisplayed() } + } + step("Assert 'Validator' block is displayed") { + onStakingSendDetailsScreen { validatorBlock.assertIsDisplayed() } + } + step("Assert 'Network Fee' block is displayed") { + onStakingSendDetailsScreen { networkFeeBlock.assertIsDisplayed() } + } + step("Assert 'Stake' button is displayed") { + onStakingSendDetailsScreen { stakeButton.assertIsDisplayed() } + } + } + } +} \ 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 index e4f345ca11..47418a9338 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -11,6 +11,7 @@ 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.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -25,6 +26,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import java.math.BigDecimal @Composable @@ -63,7 +65,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis maxLines = 1, modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing24), + .padding(top = TangemTheme.dimens.spacing24) + .testTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT), ) Text( text = secondAmount, @@ -72,7 +75,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() - .padding(top = TangemTheme.dimens.spacing8), + .padding(top = TangemTheme.dimens.spacing8) + .testTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT), ) } } 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 f1d8e921a8..753a0fc99e 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 @@ -12,6 +12,7 @@ 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.platform.testTag import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig @@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import kotlinx.collections.immutable.PersistentList private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" @@ -77,7 +79,8 @@ internal fun LazyListScope.buttons( .padding( vertical = TangemTheme.dimens.spacing10, horizontal = TangemTheme.dimens.spacing34, - ), + ) + .testTag(StakingSendScreenTestTags.MAX_BUTTON), ) } } @@ -90,7 +93,8 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment .fillMaxSize() .padding( horizontal = TangemTheme.dimens.spacing10, - ), + ) + .testTag(StakingSendScreenTestTags.CURRENCY_BUTTON), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { @@ -102,13 +106,13 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment url = button.iconUrl, size = TangemTheme.dimens.size18, isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier, + modifier = iconModifier.testTag(StakingSendScreenTestTags.FIAT_ICON), ) } else if (button.iconState != null) { CurrencyIcon( state = button.iconState, shouldDisplayNetwork = false, - modifier = iconModifier, + modifier = iconModifier.testTag(StakingSendScreenTestTags.CURRENCY_ICON), ) } Text( 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 35d49a5387..f491629420 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 @@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection import com.tangem.common.ui.amountScreen.models.AmountFieldModel @@ -28,6 +29,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay @@ -116,7 +118,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri textAlign = TextAlign.Center, modifier = Modifier .align(TopCenter) - .padding(bottom = TangemTheme.dimens.spacing32), + .padding(bottom = TangemTheme.dimens.spacing32) + .testTag(StakingSendScreenTestTags.SECONDARY_AMOUNT), ) AmountFieldError( isError = amountField.isError, 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 9c31705709..ea0473e930 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 @@ -15,6 +15,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.R @@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags private const val AMOUNT_FIELD_KEY = "amountFieldKey" @@ -52,7 +54,8 @@ internal fun LazyListScope.amountField( style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14), + .padding(top = TangemTheme.dimens.spacing14) + .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE), ) val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() @@ -66,7 +69,8 @@ internal fun LazyListScope.amountField( color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2), + .padding(top = TangemTheme.dimens.spacing2) + .testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT), ) } CurrencyIcon( diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index 5f2d7ca019..b22f5341e4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.singleEvent +import com.tangem.core.ui.test.StakingSendScreenTestTags @Composable fun NavigationButtonsBlock( @@ -146,7 +148,8 @@ private fun PreviousButton(prevButton: NavigationButton?) { .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) .background(TangemTheme.colors.button.secondary) .clickable(onClick = button.onClick) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendScreenTestTags.PREVIOUS_BUTTON), ) } } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 0bd12e57fd..323153c119 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -77,7 +77,7 @@ dependencies { /** Chucker */ debugImplementation(deps.chucker) - mockedImplementation(deps.chuckerStub) + mockedImplementation(deps.chucker) externalImplementation(deps.chuckerStub) internalImplementation(deps.chuckerStub) releaseImplementation(deps.chuckerStub) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt index 0efb5068d1..8f0331ca8b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/StakeKit.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.api.common.config +import com.tangem.datasource.BuildConfig import com.tangem.lib.auth.StakeKitAuthProvider import com.tangem.utils.ProviderSuspend @@ -14,20 +15,44 @@ internal class StakeKit( private val stakeKitAuthProvider: StakeKitAuthProvider, ) : ApiConfig() { - override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() override val environmentConfigs: List = listOf( createProdEnvironment(), + createMockEnvironment(), ) + private fun getInitialEnvironment(): ApiEnvironment { + return when (BuildConfig.BUILD_TYPE) { + MOCKED_BUILD_TYPE, + -> ApiEnvironment.MOCK + DEBUG_BUILD_TYPE, + INTERNAL_BUILD_TYPE, + EXTERNAL_BUILD_TYPE, + RELEASE_BUILD_TYPE, + -> ApiEnvironment.PROD + else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]") + } + } + private fun createProdEnvironment(): ApiEnvironmentConfig { return ApiEnvironmentConfig( environment = ApiEnvironment.PROD, baseUrl = "https://api.stakek.it/v1/", - headers = mapOf( - "X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey), - "accept" to ProviderSuspend { "application/json" }, - ), + headers = createHeaders(), ) } + + private fun createMockEnvironment(): ApiEnvironmentConfig { + return ApiEnvironmentConfig( + environment = ApiEnvironment.MOCK, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(), + ) + } + + private fun createHeaders() = buildMap { + put(key = "X-API-KEY", value = ProviderSuspend(stakeKitAuthProvider::getApiKey)) + put(key = "accept", value = ProviderSuspend { "application/json" }) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt index ee3a3cffd6..aa2adc0817 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/utils/RetrofitApiBuilder.kt @@ -7,7 +7,6 @@ import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor import com.tangem.datasource.api.common.config.ApiConfig -import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager @@ -111,7 +110,7 @@ internal class RetrofitApiBuilder @Inject constructor( apiConfigId: ApiConfig.ID, environmentConfig: ApiEnvironmentConfig, ): OkHttpClient.Builder { - return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) { + return if (BuildConfig.TESTER_MENU_ENABLED) { addInterceptor( interceptor = SwitchEnvironmentInterceptor( id = apiConfigId, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index e4d65b95dd..2219e2642d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation @@ -23,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendScreenTestTags import com.tangem.core.ui.utils.* import java.math.BigDecimal import java.text.DecimalFormat @@ -102,7 +104,9 @@ fun AmountTextField( singleLine = true, readOnly = !isEnabled, visualTransformation = visualTransformation, - modifier = Modifier.background(backgroundColor), + modifier = Modifier + .background(backgroundColor) + .testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 2d0c1b4fb1..72111e3c58 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -27,7 +27,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.SwapTokenScreenTestTags +import com.tangem.core.ui.test.BaseBlockTestTags /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -67,20 +67,23 @@ fun InputRowDefault( Column( modifier = Modifier .weight(1f) - .testTag(SwapTokenScreenTestTags.NETWORK_FEE_BLOCK), + .testTag(BaseBlockTestTags.BLOCK), ) { title?.let { Text( text = title.resolveReference(), style = TangemTheme.typography.subtitle2, color = titleColor, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8), + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing8) + .testTag(BaseBlockTestTags.BLOCK_TITLE), ) } Text( text = text.resolveReference(), style = TangemTheme.typography.body2, color = textColor, + modifier = Modifier.testTag(BaseBlockTestTags.BLOCK_TEXT), ) } iconRes?.let { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt index 942e60deaf..9de699245f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +24,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags @Suppress("LongParameterList") @Composable @@ -54,7 +56,8 @@ fun RoundableCornersRow( .padding( horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12, - ), + ) + .testTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, ) { @@ -63,6 +66,7 @@ fun RoundableCornersRow( color = startTextColor, maxLines = 1, style = startTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_NAME), ) if (iconResId != null && iconClick != null) { Icon( @@ -85,6 +89,7 @@ fun RoundableCornersRow( color = endTextColor, maxLines = 1, style = endTextStyle, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_VALUE), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt new file mode 100644 index 0000000000..b2e4eb5d56 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/BaseBlockTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object BaseBlockTestTags { + const val BLOCK = "BASE_BLOCK" + const val BLOCK_TITLE = "BASE_BLOCK_TITLE" + const val BLOCK_TEXT = "BASE_BLOCK_REWARDS_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt new file mode 100644 index 0000000000..f368755c2f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingDetailsScreenTestTags.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.test + +object StakingDetailsScreenTestTags { + const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + + const val BANNER_IMAGE = "TOKEN_DETAILS_SCREEN_BANNER_IMAGE" + const val BANNER_TEXT = "TOKEN_DETAILS_SCREEN_BANNER_TEXT" + + const val PARAMETER_BLOCK = "STAKING_DETAILS_PARAMETER_BLOCK" + const val PARAMETER_NAME = "STAKING_DETAILS_PARAMETER_NAME" + const val PARAMETER_VALUE = "STAKING_DETAILS_PARAMETER_VALUE" + const val TOS_TEXT = "STAKING_DETAILS_TOS_TEXT" + + const val ACTIVE_STAKING_BLOCK = "STAKING_DETAILS_ACTIVE_STAKING_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt new file mode 100644 index 0000000000..138b27e35b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendDetailsScreenTestTags.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.test + +object StakingSendDetailsScreenTestTags { + + const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT" + const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT" + + const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK" + const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt new file mode 100644 index 0000000000..1dc36d3f79 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/StakingSendScreenTestTags.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.test + +object StakingSendScreenTestTags { + const val SCREEN_CONTAINER = "STAKING_SEND_SCREEN_CONTAINER" + + const val AMOUNT_CONTAINER_TITLE = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TITLE" + const val AMOUNT_CONTAINER_TEXT = "STAKING_SEND_SCREEN_AMOUNT_CONTAINER_TEXT" + const val INPUT_TEXT_FIELD = "STAKING_SEND_SCREEN_INPUT_TEXT_FIELD" + const val SECONDARY_AMOUNT = "STAKING_SEND_SCREEN_SECONDARY_AMOUNT" + + const val CURRENCY_BUTTON = "STAKING_SEND_SCREEN_CURRENCY_BUTTON" + const val FIAT_ICON = "STAKING_SEND_SCREEN_FIAT_ICON" + const val CURRENCY_ICON = "STAKING_SEND_SCREEN_CURRENCY_ICON" + const val MAX_BUTTON = "STAKING_SEND_SCREEN_MAX_BUTTON" + const val PREVIOUS_BUTTON = "STAKING_SEND_SCREEN_PREVIOUS_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt index d8be9a8e43..cd6cf947d1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -6,7 +6,6 @@ object SwapTokenScreenTestTags { const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" - const val NETWORK_FEE_BLOCK = "SWAP_TOKEN_SCREEN_NETWORK_FEE_BLOCK" const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt index f337c0bbc9..bcbee0ae13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenDetailsScreenTestTags.kt @@ -2,7 +2,19 @@ package com.tangem.core.ui.test object TokenDetailsScreenTestTags { const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER" + const val TOKEN_TITLE = "TOKEN_DETAILS_SCREEN_TOKEN_TITLE" const val ACTION_BUTTON = "TOKEN_DETAILS_SCREEN_ACTION_BUTTON" const val HORIZONTAL_ACTION_CHIPS = "TOKEN_DETAILS_SCREEN_HORIZONTAL_ACTION_CHIPS" + + const val STAKING_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_BLOCK" + const val STAKING_AVAILABLE_BLOCK = "TOKEN_DETAILS_SCREEN_STAKING_AVAILABLE_BLOCK" + const val STAKING_CURRENCY_ICON = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_CURRENCY_ICON" + const val STAKING_SERVICE_TITLE = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TITLE" + const val STAKING_SERVICE_TEXT = "TOKEN_DETAILS_SCREEN_STAKING_STAKING_SERVICE_TEXT" + const val STAKING_FIAT_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_FIAT_AMOUNT" + const val STAKING_DOT = "TOKEN_DETAILS_SCREEN_STAKING_DOT" + const val STAKING_TOKEN_AMOUNT = "TOKEN_DETAILS_SCREEN_STAKING_TOKEN_AMOUNT" + const val STAKING_REWARD_VALUE = "TOKEN_DETAILS_SCREEN_STAKING_REWARD_VALUE" + const val STAKING_CHEVRON_ICON = "TOKEN_DETAILS_SCREEN_STAKING_CHEVRON_ICON" } \ 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 462c5d22c0..27ba05eb91 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 @@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -42,6 +43,7 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.RewardBlockType import com.tangem.features.staking.impl.R @@ -171,7 +173,8 @@ private fun LazyListScope.activeStakingBlock( currentIndex = index + 1, lastIndex = state.yieldBalance.balances.lastIndex + 1, addDefaultPadding = false, - ), + ) + .testTag(StakingDetailsScreenTestTags.ACTIVE_STAKING_BLOCK), ) } } @@ -189,7 +192,9 @@ private fun BannerBlock(onClick: () -> Unit) { ), ) { Image( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .testTag(StakingDetailsScreenTestTags.BANNER_IMAGE), contentScale = ContentScale.FillWidth, painter = painterResource(R.drawable.img_staking_banner), contentDescription = null, @@ -197,7 +202,8 @@ private fun BannerBlock(onClick: () -> Unit) { Text( modifier = Modifier .align(Alignment.CenterStart) - .padding(TangemTheme.dimens.spacing16), + .padding(TangemTheme.dimens.spacing16) + .testTag(StakingDetailsScreenTestTags.BANNER_TEXT), text = buildAnnotatedString { withStyle(SpanStyle(Brush.linearGradient(textGradientColors))) { append(stringResourceSafe(R.string.staking_details_banner_text)) 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 9cc528d859..e1a7c81bd2 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 @@ -11,6 +11,7 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.amountScreen.AmountScreenContent import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig @@ -20,6 +21,7 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendScreenTestTags 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 @@ -44,7 +46,8 @@ internal fun StakingScreen(uiState: StakingUiState) { .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() .imePadding() - .systemBarsPadding(), + .systemBarsPadding() + .testTag(StakingSendScreenTestTags.SCREEN_CONTAINER), horizontalAlignment = Alignment.CenterHorizontally, ) { StakingAppBar( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt index c7e67f5415..b09a9ab029 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingTosText.kt @@ -2,11 +2,14 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.compose.foundation.text.ClickableText import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.extensions.appendColored import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingDetailsScreenTestTags import com.tangem.features.staking.impl.R private const val TERMS_OF_USE_KEY = "termsOfUse" @@ -58,5 +61,6 @@ internal fun StakingTosText(onTextClick: (String) -> Unit) { onTextClick(PRIVACY_POLICY_URL) } }, + modifier = Modifier.testTag(StakingDetailsScreenTestTags.TOS_TEXT), ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 0ca152acfb..7efc5b644c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -9,6 +9,7 @@ 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.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -27,6 +28,7 @@ import com.tangem.core.ui.format.bigdecimal.fee import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.utils.StringsSigns.DASH_SIGN @@ -39,7 +41,8 @@ internal fun StakingFeeBlock(feeState: FeeState) { .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), + .padding(TangemTheme.dimens.spacing12) + .testTag(StakingSendDetailsScreenTestTags.NETWORK_FEE_BLOCK), ) { Text( text = stringResourceSafe(R.string.common_network_fee_title), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 6e2d13e5e0..7182fc7bf4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -10,11 +10,13 @@ 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.platform.testTag import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder @@ -35,7 +37,8 @@ internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClic interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, - ), + ) + .testTag(StakingSendDetailsScreenTestTags.VALIDATOR_BLOCK), ) { InputRowImageInfo( title = resourceReference(R.string.staking_validator), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt index cd79642bbc..d077ce9d52 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -17,6 +18,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM import com.tangem.features.tokendetails.impl.R @@ -30,7 +32,9 @@ internal fun StakingBalanceBlock( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.STAKING_BLOCK), ) { Column( verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), @@ -47,16 +51,19 @@ internal fun StakingBalanceBlock( text = state.fiatValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT), ) Text( text = StringsSigns.DOT, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_DOT), ) Text( text = state.cryptoValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT), ) } if (state.rewardValue != TextReference.EMPTY) { @@ -64,6 +71,7 @@ internal fun StakingBalanceBlock( text = state.rewardValue.orMaskWithStars(isBalanceHidden).resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_REWARD_VALUE), ) } } @@ -71,6 +79,7 @@ internal fun StakingBalanceBlock( painter = painterResource(id = R.drawable.ic_chevron_right_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_CHEVRON_ICON), ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index 807664c039..39bfbef5ec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -23,6 +24,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags import com.tangem.core.ui.utils.getGreyScaleColorFilter import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingBalanceBlock @@ -77,7 +79,9 @@ internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean, @Composable private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifier: Modifier = Modifier) { Column( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .fillMaxWidth() + .testTag(TokenDetailsScreenTestTags.STAKING_AVAILABLE_BLOCK), ) { Row { val (alpha, colorFilter) = remember(state.iconState.isGrayscale) { @@ -87,7 +91,8 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi modifier = Modifier .size(TangemTheme.dimens.size20) .clip(TangemTheme.shapes.roundedCorners8) - .align(Alignment.CenterVertically), + .align(Alignment.CenterVertically) + .testTag(TokenDetailsScreenTestTags.STAKING_CURRENCY_ICON), icon = state.iconState, alpha = alpha, colorFilter = colorFilter, @@ -98,6 +103,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi text = state.titleText.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TITLE), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size4)) @@ -106,6 +112,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi text = state.subtitleText.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(TokenDetailsScreenTestTags.STAKING_SERVICE_TEXT), ) Spacer(modifier = Modifier.size(TangemTheme.dimens.size8)) From a08145498ca01e495d60d11dd8e05afa8d631c27 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 15:57:02 +0300 Subject: [PATCH 75/87] Updated on 2026-08-14 --- .../domain/walletconnect/WalletConnectSdkHelper.kt | 9 ++------- .../tangem/core/analytics/models/AnalyticsParam.kt | 9 +++++++-- .../java/com/tangem/core/analytics/models/Basic.kt | 4 +++- .../network/ethereum/WcEthSendTransactionUseCase.kt | 13 +++++++++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index ffb1dbb2b0..9df63ef08a 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -13,9 +13,6 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic -import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType import com.tangem.data.walletconnect.network.ethereum.LegacySdkHelper import com.tangem.domain.models.wallet.UserWallet import com.tangem.operations.sign.SignHashCommand @@ -42,7 +39,6 @@ import org.json.JSONArray import org.json.JSONObject import timber.log.Timber import java.math.BigDecimal -import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam @Suppress("LargeClass") class WalletConnectSdkHelper { @@ -227,8 +223,6 @@ class WalletConnectSdkHelper { ) return when (result) { is Result.Success -> { - val sentFrom = CoreAnalyticsParam.TxSentFrom.WalletConnect - Analytics.send(Basic.TransactionSent(sentFrom = sentFrom, memoType = MemoType.Null)) val hash = result.data.hash if (hash.startsWith(HEX_PREFIX)) { hash @@ -394,7 +388,8 @@ class WalletConnectSdkHelper { signature = signedHash, hash = hashToSign, publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ).asRSVLegacyEVM().toHexString().formatHex().lowercase() // use lowercase because some dapps cant handle UPPERCASE + ).asRSVLegacyEVM().toHexString().formatHex() + .lowercase() // use lowercase because some dapps cant handle UPPERCASE } } is CompletionResult.Failure -> { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 8eed0785bc..08021ec348 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -112,7 +112,12 @@ sealed class AnalyticsParam { val permissionType: String, ) : TxSentFrom("Approve"), TxData - data object WalletConnect : TxSentFrom("WalletConnect") + data class WalletConnect( + override val blockchain: String, + override val token: String, + override val feeType: FeeType?, + ) : TxSentFrom("WalletConnect"), TxData + data object Sell : TxSentFrom("Sell") data class NFT( @@ -125,7 +130,7 @@ sealed class AnalyticsParam { sealed interface TxData { val blockchain: String val token: String - val feeType: FeeType + val feeType: FeeType? } sealed class FeeType(val value: String) { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 110391778a..875cf24182 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -55,7 +55,9 @@ sealed class Basic( if (sentFrom is AnalyticsParam.TxData) { this[AnalyticsParam.BLOCKCHAIN] = sentFrom.blockchain this[AnalyticsParam.TOKEN_PARAM] = sentFrom.token - this[AnalyticsParam.FEE_TYPE] = sentFrom.feeType.value + sentFrom.feeType?.value?.let { + this[AnalyticsParam.FEE_TYPE] = it + } } if (sentFrom is AnalyticsParam.TxSentFrom.Approve) { this[AnalyticsParam.PERMISSION_TYPE] = sentFrom.permissionType diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 5358c3a317..e82c497072 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -7,6 +7,9 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam.TxSentFrom +import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -86,6 +89,16 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emit(state.toResult(parseSendError(error).left())) } .getOrNull() ?: return + analytics.send( + Basic.TransactionSent( + sentFrom = TxSentFrom.WalletConnect( + blockchain = network.name, + token = network.currencySymbol, + feeType = null, + ), + memoType = MemoType.Null, + ), + ) val respondResult = respondService.respond(rawSdkRequest, hash.formatHex()) emit(state.toResult(respondResult)) } From 27570efa938137de58746a4f57c9f3cd51abdb1d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 20:57:21 +0300 Subject: [PATCH 76/87] Updated on 2026-08-14 --- .../managetokens/DefaultManageTokensRepository.kt | 11 ++++++++++- .../data/managetokens/di/ManageTokensDataModule.kt | 2 ++ .../domain/managetokens/GetManagedTokensUseCase.kt | 1 + .../utils/list/ManageTokensListManager.kt | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 20cdc81e55..ce7a1c066a 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -9,6 +9,7 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher @@ -42,6 +43,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, + private val userTokenSaver: UserTokensSaver, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val userTokensResponseStore: UserTokensResponseStore, private val testnetTokensStorage: TestnetTokensStorage, @@ -127,7 +129,8 @@ internal class DefaultManageTokensRepository( val tokensResponse = request.params.userWalletId?.let { userWalletId -> if (loadUserTokensFromRemote && userWallet != null) { safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) { - createDefaultUserTokensResponse(userWallet) + // save tokens response only if loadUserTokensFromRemote is true and it means onboarding call + createAndSaveDefaultUserTokensResponse(userWallet = userWallet) } } else { getSavedUserTokensResponseSync(userWalletId) @@ -158,6 +161,12 @@ internal class DefaultManageTokensRepository( ) } + private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { + val userTokensResponse = createDefaultUserTokensResponse(userWallet) + userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false) + return userTokensResponse + } + private suspend fun fetchTestnetCurrencies( userWallet: UserWallet, request: Request, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 87943d1e88..412155dcda 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -32,6 +32,7 @@ internal object ManageTokensDataModule { userWalletsStore: UserWalletsStore, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, userTokensResponseStore: UserTokensResponseStore, + userTokensSaver: UserTokensSaver, testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, @@ -43,6 +44,7 @@ internal object ManageTokensDataModule { userWalletsStore = userWalletsStore, manageTokensUpdateFetcher = manageTokensUpdateFetcher, userTokensResponseStore = userTokensResponseStore, + userTokenSaver = userTokensSaver, testnetTokensStorage = testnetTokensStorage, excludedBlockchains = excludedBlockchains, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt index e8a4b8d205..6a9c3a47ed 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/GetManagedTokensUseCase.kt @@ -10,6 +10,7 @@ class GetManagedTokensUseCase( operator fun invoke( context: ManageTokensListBatchingContext, + // only for onboarding case, change carefully and check repository implementation loadUserTokensFromRemote: Boolean, batchSize: Int = 40, ): ManageTokensListBatchFlow { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index b486703bd8..7664a6ce6e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -93,6 +93,7 @@ internal class ManageTokensListManager @AssistedInject constructor( actionsFlow = actionsFlow, coroutineScope = this, ), + // only for onboarding case, change carefully and check repository implementation loadUserTokensFromRemote = userWalletId != null && source == ManageTokensSource.ONBOARDING, ) From ddb0957f2e94deeb66f867ad67a6332123ae0bdf Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Aug 2025 18:41:42 +0000 Subject: [PATCH 77/87] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 57e50cf4ac..fb79857d96 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.27.0-1148" +tangemBlockchainSdk = "develop-1140" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.27.0-519" +tangemCardSdk = "develop-518" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-454" +tangemHotSdk = "develop-461" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From ce878fdbe249f6376ef4c30abe20e5d9dc255ee0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 13:02:50 +0500 Subject: [PATCH 78/87] Updated on 2026-08-14 --- app/build.gradle.kts | 4 +++ .../main/java/com/tangem/tap/MainActivity.kt | 12 ++++++- .../tangem/tap/routing/utils/ChildFactory.kt | 9 +++++ .../com/tangem/common/routing/AppRoute.kt | 3 ++ .../configs/feature_toggles_config.json | 4 +++ features/tangempay/details/api/.gitignore | 1 + .../tangempay/details/api/build.gradle.kts | 18 ++++++++++ .../tangempay/TangemPayFeatureToggles.kt | 5 +++ .../components/TangemPayDetailsComponent.kt | 10 ++++++ features/tangempay/details/impl/.gitignore | 1 + .../tangempay/details/impl/build.gradle.kts | 32 ++++++++++++++++++ .../DefaultTangemPayFeatureToggles.kt | 10 ++++++ .../DefaultTangemPayDetailsComponent.kt | 33 +++++++++++++++++++ .../di/TangemPayDetailsFeatureModule.kt | 20 +++++++++++ .../tangempay/di/TangemPayDetailsModule.kt | 21 ++++++++++++ features/tangempay/main/api/.gitignore | 1 + features/tangempay/main/api/build.gradle.kts | 18 ++++++++++ features/tangempay/main/impl/.gitignore | 1 + features/tangempay/main/impl/build.gradle.kts | 32 ++++++++++++++++++ settings.gradle.kts | 6 ++++ 20 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 features/tangempay/details/api/.gitignore create mode 100644 features/tangempay/details/api/build.gradle.kts create mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt create mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt create mode 100644 features/tangempay/details/impl/.gitignore create mode 100644 features/tangempay/details/impl/build.gradle.kts create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt create mode 100644 features/tangempay/main/api/.gitignore create mode 100644 features/tangempay/main/api/build.gradle.kts create mode 100644 features/tangempay/main/impl/.gitignore create mode 100644 features/tangempay/main/impl/build.gradle.kts diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4de1a30e45..dd1295ea13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -238,6 +238,10 @@ dependencies { implementation(projects.features.home.impl) implementation(projects.features.account.api) implementation(projects.features.account.impl) + implementation(projects.features.tangempay.details.api) + implementation(projects.features.tangempay.details.impl) + implementation(projects.features.tangempay.main.api) + implementation(projects.features.tangempay.main.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2f037143f3..9cb6b6cd62 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -52,6 +52,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper @@ -197,6 +198,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles + @Inject + internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -518,7 +522,13 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity) - if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { + + // Workaround to navigate to TangemPayDetails screen. Will be deleted in next PRs + if (tangemPayFeatureToggles.isTangemPayEnabled) { + store.dispatchNavigationAction { + replaceAll(AppRoute.TangemPayDetails) + } + } else if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { replaceAll( AppRoute.Welcome( 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 49bb638a53..40e0686f77 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 @@ -39,6 +39,7 @@ import com.tangem.features.send.v2.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent import com.tangem.features.swap.v2.api.SendWithSwapComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -108,6 +109,7 @@ internal class ChildFactory @Inject constructor( private val updateAccessCodeComponentFactory: UpdateAccessCodeComponent.Factory, private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory, private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory, + private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -573,6 +575,13 @@ internal class ChildFactory @Inject constructor( componentFactory = archivedAccountListComponentFactory, ) } + is AppRoute.TangemPayDetails -> { + createComponentChild( + context = context, + params = TangemPayDetailsComponent.Params(), + componentFactory = tangemPayDetailsComponentFactory, + ) + } } } } \ 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 f797561ced..6d4e985501 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 @@ -353,4 +353,7 @@ sealed class AppRoute(val path: String) : Route { data class ArchivedAccountList( val userWalletId: UserWalletId, ) : AppRoute(path = "/archived_account/${userWalletId.stringValue}") + + @Serializable + data object TangemPayDetails : AppRoute(path = "/tangem_pay_details") } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 4cd82bc21e..0d8eea380b 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -54,5 +54,9 @@ { "name": "NFT_SEND_REDESIGN_ENABLED", "version": "undefined" + }, + { + "name": "TANGEM_PAY_ENABLED", + "version": "undefined" } ] diff --git a/features/tangempay/details/api/.gitignore b/features/tangempay/details/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/api/build.gradle.kts b/features/tangempay/details/api/build.gradle.kts new file mode 100644 index 0000000000..77acdd5c22 --- /dev/null +++ b/features/tangempay/details/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt new file mode 100644 index 0000000000..393e589bce --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tangempay + +interface TangemPayFeatureToggles { + val isTangemPayEnabled: Boolean +} \ No newline at end of file diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt new file mode 100644 index 0000000000..64e11cecbe --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface TangemPayDetailsComponent : ComposableContentComponent { + @Suppress("EmptyDefaultConstructor") // Will add params in Next PRs + class Params() + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/.gitignore b/features/tangempay/details/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/details/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts new file mode 100644 index 0000000000..4f9fb1c110 --- /dev/null +++ b/features/tangempay/details/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.details.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt new file mode 100644 index 0000000000..a51c11a3bc --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTangemPayFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TangemPayFeatureToggles { + override val isTangemPayEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt new file mode 100644 index 0000000000..2c55cb6a3a --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.decompose.context.AppComponentContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultTangemPayDetailsComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: TangemPayDetailsComponent.Params, +) : AppComponentContext by appComponentContext, TangemPayDetailsComponent { + + @Composable + override fun Content(modifier: Modifier) { + Box(modifier.fillMaxSize().background(Color.Red)) + // TODO("[REDACTED_JIRA]") + } + + @AssistedFactory + interface Factory : TangemPayDetailsComponent.Factory { + override fun create( + context: AppComponentContext, + params: TangemPayDetailsComponent.Params, + ): DefaultTangemPayDetailsComponent + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt new file mode 100644 index 0000000000..4cd92fc806 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.components.DefaultTangemPayDetailsComponent +import com.tangem.features.tangempay.components.TangemPayDetailsComponent +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 TangemPayDetailsFeatureModule { + + @Binds + @Singleton + fun bindTangemPayDetailsComponentFactory( + factory: DefaultTangemPayDetailsComponent.Factory, + ): TangemPayDetailsComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt new file mode 100644 index 0000000000..a6ea142d28 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.tangempay.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles +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 TangemPayDetailsModule { + + @Provides + @Singleton + fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { + return DefaultTangemPayFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/tangempay/main/api/.gitignore b/features/tangempay/main/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts new file mode 100644 index 0000000000..15fb515b8b --- /dev/null +++ b/features/tangempay/main/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/tangempay/main/impl/.gitignore b/features/tangempay/main/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/tangempay/main/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts new file mode 100644 index 0000000000..eb442c8f69 --- /dev/null +++ b/features/tangempay/main/impl/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.tangempay.main.impl" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.configToggles) + + /** Features api */ + implementation(projects.features.tangempay.details.api) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b77ede786a..35d08f3116 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -264,6 +264,12 @@ include(":features:kyc:api") //TODO disable for release because of the permissions // include(":features:kyc:impl") +include(":features:tangempay:main:api") +include(":features:tangempay:main:impl") + +include(":features:tangempay:details:api") +include(":features:tangempay:details:impl") + include(":features:create-wallet-selection:api") include(":features:create-wallet-selection:impl") From 18fe9257cb2910be6cde21d58196c04b461f12ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 12:08:16 +0300 Subject: [PATCH 79/87] Updated on 2026-08-14 --- .../impl/DefaultOnboardingNoteComponent.kt | 13 +- .../topup/OnboardingNoteTopUpComponent.kt | 39 --- .../topup/model/OnboardingNoteTopUpModel.kt | 253 ------------------ .../topup/ui/OnboardingNoteTopUpHeader.kt | 102 ------- .../topup/ui/OnboardingNoteTopUpScreen.kt | 130 --------- .../topup/ui/state/OnboardingNoteTopUpUM.kt | 19 -- .../v2/note/impl/di/ComponentModule.kt | 6 - .../v2/note/impl/model/OnboardingNoteModel.kt | 2 +- .../v2/note/impl/route/OnboardingNoteRoute.kt | 4 +- .../onboarding/v2/note/impl/route/Step.kt | 2 +- .../v2/twin/impl/model/OnboardingTwinModel.kt | 167 +----------- .../v2/twin/impl/ui/OnboardingTwin.kt | 70 ----- .../v2/twin/impl/ui/TwinWalletArtwork.kt | 103 +------ .../v2/twin/impl/ui/state/OnboardingTwinUM.kt | 21 -- 14 files changed, 23 insertions(+), 908 deletions(-) delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt delete mode 100644 features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index bea41ef278..7f15990f3c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -20,10 +20,10 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT @@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted val params: OnboardingNoteComponent.Params, + val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory, ) : OnboardingNoteComponent, AppComponentContext by context { private val model: OnboardingNoteModel = getOrCreateModel(params) @@ -98,14 +99,14 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( childParams = childParams, onWalletCreated = { userWallet -> model.onWalletCreated(userWallet) - model.stackNavigation.push(OnboardingNoteRoute.TopUp) + model.stackNavigation.push(OnboardingNoteRoute.Done) }, ), ) - OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent( - appComponentContext = factoryContext, - params = OnboardingNoteTopUpComponent.Params( - childParams = childParams, + OnboardingNoteRoute.Done -> onboardingDoneComponentFactory.create( + context = factoryContext, + params = OnboardingDoneComponent.Params( + mode = OnboardingDoneComponent.Mode.WalletCreated, onDone = { params.onDone() }, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt deleted file mode 100644 index 5b9cc4f1af..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/OnboardingNoteTopUpComponent.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp - -internal class OnboardingNoteTopUpComponent( - appComponentContext: AppComponentContext, - private val params: Params, -) : ComposableContentComponent, AppComponentContext by appComponentContext { - - private val model: OnboardingNoteTopUpModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - - BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) - - OnboardingNoteTopUp( - modifier = modifier, - state = state, - ) - } - - data class Params( - val childParams: DefaultOnboardingNoteComponent.ChildParams, - val onDone: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt deleted file mode 100644 index 4f0a65a810..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.model - -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.card.repository.CardRepository -import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent -import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.extensions.isPositive -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList") -@ModelScoped -internal class OnboardingNoteTopUpModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val rampStateManager: RampStateManager, - private val cardRepository: CardRepository, - private val saveWalletUseCase: SaveWalletUseCase, - private val walletBalanceFetcher: WalletBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, -) : Model() { - - private val params = paramsContainer.require() - private val commonState = params.childParams.commonState - private val scanResponse = params.childParams.commonState.value.scanResponse - private var userWallet = params.childParams.commonState.value.userWallet - - private val _uiState = MutableStateFlow( - OnboardingNoteTopUpUM( - onRefreshBalanceClick = ::refreshBalance, - onBuyCryptoClick = ::onBuyCryptoClick, - onShowWalletAddressClick = ::onShowWalletAddressClick, - onDismissBottomSheet = ::onDismissBottomSheet, - ), - ) - - val uiState: StateFlow = _uiState - - init { - Analytics.send(OnboardingEvent.Topup.ScreenOpened) - observeArtwork() - modelScope.launch { - createUserWalletIfNull() - cardRepository.finishCardActivation(scanResponse.card.cardId) - observeCryptoCurrencyStatus() - refreshBalance() - } - } - - private fun refreshBalance() { - modelScope.launch { - showBalanceLoadingProgress(true) - createUserWalletIfNull() - val userWalletId = requireNotNull(userWallet?.walletId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true) - } - showBalanceLoadingProgress(false) - } - } - - private fun onBuyCryptoClick() { - val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - modelScope.launch { - getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight { - urlOpener.openUrl(it) - } - } - Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency)) - } - - private fun onShowWalletAddressClick() { - val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return - val networkAddress = currencyStatus.value.networkAddress ?: return - - _uiState.update { - it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) - } - Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) - } - - private fun onDismissBottomSheet() { - _uiState.update { - it.copy(addressBottomSheetConfig = null) - } - } - - private suspend fun createUserWalletIfNull() { - if (userWallet != null) { - return - } - val commonState = params.childParams.commonState.value - userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse) - } - - private fun observeArtwork() { - modelScope.launch { - params.childParams.commonState.collect { - _uiState.value = _uiState.value.copy( - cardArtwork = it.cardArtwork, - ) - } - } - } - - private fun observeCryptoCurrencyStatus() { - val userWalletId = userWallet?.walletId ?: return - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId) - .map { it.getOrNull() } - .filterNotNull() - .onEach(::applyCryptoCurrencyStatusToState) - .launchIn(modelScope) - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - if (commonState.value.cryptoCurrencyStatus == null) { - loadAvailableForBuy(status) - } - - commonState.update { - it.copy(cryptoCurrencyStatus = status) - } - - val amount = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.amount - is CryptoCurrencyStatus.NoAccount -> status.value.amount - is CryptoCurrencyStatus.NoQuote -> status.value.amount - else -> null - } - val hasCurrentNetworkTransactions = when (status.value) { - is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions - is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions - else -> false - } - val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount - - if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { - params.onDone() - } - - _uiState.update { - it.copy( - amountToCreateAccount = amountToCreateAccount - ?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }, - balance = amount?.format { - crypto( - symbol = status.currency.symbol, - decimals = status.currency.decimals, - ) - }.orEmpty(), - isTopUpDataLoading = status.value.networkAddress == null, - ) - } - } - - private fun showBalanceLoadingProgress(value: Boolean) { - _uiState.update { - it.copy(isRefreshing = value) - } - } - - private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { - modelScope.launch { - val availableForBuy = rampStateManager.availableForBuy( - userWallet = userWallet ?: return@launch, - cryptoCurrency = cryptoCurrencyStatus.currency, - ) - _uiState.update { - it.copy( - availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None, - availableForBuyLoading = false, - ) - } - } - } - - private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) = - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = uiState.value.onDismissBottomSheet, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currencyStatus.currency.name, - symbol = currencyStatus.currency.symbol, - ), - network = currencyStatus.currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ) - - private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet { - val wallet = requireNotNull( - value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(), - lazyMessage = { "User wallet not created" }, - ) - saveWalletUseCase(wallet, false) - return wallet - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt deleted file mode 100644 index 672d5e838b..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -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.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.LocalTangemShimmer -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.common.ui.RefreshButton -import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R -import com.valentinilk.shimmer.shimmer - -@Composable -fun OnboardingNoteTopUpHeader( - balance: String, - cardArtwork: ArtworkUM?, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .heightIn(min = 180.dp) - .widthIn(max = 450.dp), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = 32.dp), - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - modifier = if (balance.isEmpty()) { - Modifier - .width(120.dp) - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) - .shimmer(LocalTangemShimmer.current) - } else { - Modifier - }, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - text = balance, - ) - SpacerHMax() - } - } - WalletCard( - modifier = Modifier.width(120.dp).align(Alignment.TopCenter), - artwork = cardArtwork, - ) - RefreshButton( - modifier = Modifier.align(Alignment.BottomCenter), - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardinNoteTopUpHeaderPreview() { - TangemThemePreview { - OnboardingNoteTopUpHeader( - balance = "0.00000001 BTC", - cardArtwork = ArtworkUM(null, ""), - onRefreshBalanceClick = {}, - isRefreshing = false, - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt deleted file mode 100644 index 476d06b378..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui - -import androidx.compose.animation.AnimatedVisibility -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.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerHMax -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT -import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM - -@Composable -fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .navigationBarsPadding(), - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnboardingNoteTopUpHeader( - balance = state.balance, - cardArtwork = state.cardArtwork, - onRefreshBalanceClick = state.onRefreshBalanceClick, - isRefreshing = state.isRefreshing, - modifier = Modifier - .padding(top = 64.dp) - .padding(horizontal = 24.dp) - .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth(), - ) - Column( - modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT) - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding(horizontal = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.padding(top = 16.dp), - ) - - val text = if (state.amountToCreateAccount != null) { - stringResourceSafe( - R.string.onboarding_top_up_min_create_account_amount, - state.amountToCreateAccount, - ) - } else { - stringResourceSafe(R.string.onboarding_top_up_body) - } - SpacerH16() - Text( - text = text, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - - BottomButtons(state) - - state.addressBottomSheetConfig?.let { config -> - TokenReceiveBottomSheet(config = config) - } - } -} - -@Composable -private fun BottomButtons(state: OnboardingNoteTopUpUM) { - if (state.availableForBuy) { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - } else { - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_button_receive_crypto), - onClick = state.onShowWalletAddressClick, - ) - } - AnimatedVisibility(visible = !state.availableForBuyLoading) { - if (state.availableForBuy) { - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowWalletAddressClick, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun OnboardingNoteTopUpPreview() { - TangemThemePreview { - OnboardingNoteTopUp( - state = OnboardingNoteTopUpUM( - availableForBuy = true, - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt deleted file mode 100644 index c9d9ca3541..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state - -import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig - -data class OnboardingNoteTopUpUM( - val cardArtwork: ArtworkUM? = null, - val availableForBuy: Boolean = false, - val availableForBuyLoading: Boolean = true, - val balance: String = "", - val isRefreshing: Boolean = false, - val isTopUpDataLoading: Boolean = true, - val amountToCreateAccount: String? = null, - val addressBottomSheetConfig: TangemBottomSheetConfig? = null, - val onBuyCryptoClick: () -> Unit = {}, - val onShowWalletAddressClick: () -> Unit = {}, - val onRefreshBalanceClick: () -> Unit = {}, - val onDismissBottomSheet: () -> Unit = {}, -) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt index be064600cc..136a2ae24d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/di/ComponentModule.kt @@ -5,7 +5,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel -import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel import dagger.Binds import dagger.Module @@ -37,9 +36,4 @@ internal interface ModelModule { @IntoMap @ClassKey(OnboardingNoteCreateWalletModel::class) fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model - - @Binds - @IntoMap - @ClassKey(OnboardingNoteTopUpModel::class) - fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 5d165cbe97..ae47a8ab9d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -84,7 +84,7 @@ internal class OnboardingNoteModel @Inject constructor( return if (card.wallets.isEmpty()) { OnboardingNoteRoute.CreateWallet } else { - OnboardingNoteRoute.TopUp + OnboardingNoteRoute.Done } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt index 1d7573e677..cc8101af19 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/OnboardingNoteRoute.kt @@ -9,7 +9,7 @@ internal sealed class OnboardingNoteRoute { data object CreateWallet : OnboardingNoteRoute() @Serializable - data object TopUp : OnboardingNoteRoute() + data object Done : OnboardingNoteRoute() } -internal const val ONBOARDING_NOTE_STEPS_COUNT = 3 \ No newline at end of file +internal const val ONBOARDING_NOTE_STEPS_COUNT = 2 \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt index 3df643e22b..83b41e6d8f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/route/Step.kt @@ -2,5 +2,5 @@ package com.tangem.features.onboarding.v2.note.impl.route internal fun OnboardingNoteRoute.stepNum() = when (this) { OnboardingNoteRoute.CreateWallet -> 1 - OnboardingNoteRoute.TopUp -> 2 + OnboardingNoteRoute.Done -> 2 } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index db44e189c2..ab86ceed99 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -7,40 +7,22 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig -import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.share.ShareManager -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.datasource.local.config.issuers.IssuersConfigStorage -import com.tangem.domain.card.common.util.twinsIsTwinned import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase -import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase -import com.tangem.domain.tokens.FetchCurrencyStatusUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensFeatureToggles -import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.usecase.DeleteWalletUseCase @@ -61,11 +43,9 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -81,16 +61,8 @@ internal class OnboardingTwinModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val issuersConfigStorage: IssuersConfigStorage, private val cardRepository: CardRepository, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, - private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, - private val urlOpener: UrlOpener, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val clipboardManager: ClipboardManager, - private val shareManager: ShareManager, - private val tokensFeatureToggles: TokensFeatureToggles, - private val walletBalanceFetcher: WalletBalanceFetcher, ) : Model() { private val params = paramsContainer.require() @@ -116,14 +88,10 @@ internal class OnboardingTwinModel @Inject constructor( ) } Mode.CreateWallet -> { - if (params.scanResponse.twinsIsTwinned()) { - OnboardingTwinUM.TopUpPrepare - } else { - OnboardingTwinUM.Welcome( - pairCardNumber = firstCardTwinNumber.pairNumber().number, - onContinueClick = ::navigateToFirstScan, - ) - } + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) } }, ) @@ -139,11 +107,6 @@ internal class OnboardingTwinModel @Inject constructor( saveTwinsOnboardingShownUseCase() } } - OnboardingTwinUM.TopUpPrepare -> { - modelScope.launch { - setTopUpState(params.scanResponse) - } - } else -> {} } } @@ -218,10 +181,7 @@ internal class OnboardingTwinModel @Inject constructor( }, ) } - - innerNavigationState.update { - it.copy(stackSize = 2) - } + innerNavigationState.update { it.copy(stackSize = 2) } } } } @@ -229,7 +189,6 @@ internal class OnboardingTwinModel @Inject constructor( private fun createSecondWallet(firstPublicKey: String) { setLoading(true) - modelScope.launch { val secondCardNumber = firstCardTwinNumber.pairNumber().number val result = tangemSdkManager.createSecondTwinWallet( @@ -318,13 +277,13 @@ internal class OnboardingTwinModel @Inject constructor( Mode.CreateWallet -> { modelScope.launch { setLoading(true) - setTopUpState(scanResponse) + finishActivation(scanResponse) }.saveIn(cryptoCurrencyStatusJobHolder) } } } - private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope { + private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope { val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run { Timber.e("User wallet not created") setLoading(false) @@ -342,117 +301,7 @@ internal class OnboardingTwinModel @Inject constructor( cardRepository.finishCardActivation(params.scanResponse.card.cardId) - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - } else { - fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true) - } - .onLeft { - Timber.e("Unable to fetch currency status: $it") - setLoading(false) - } - - val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .firstOrNull()?.getOrNull() - ?: run { - setLoading(false) - Timber.e("Unable to get currency status") - return@coroutineScope - } - - launch { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .collect { - it.onRight { status -> - applyCryptoCurrencyStatusToState(status) - } - } - } - - _uiState.value = OnboardingTwinUM.TopUp( - onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, - onRefreshClick = { onRefreshBalanceClick(userWallet) }, - onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, - isLoading = true, - ) - - innerNavigationState.update { - it.copy(stackSize = 4) - } - } - - private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { - val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return - if (amount > BigDecimal.ZERO) { - params.modelCallbacks.onDone() - } else { - update { - it.copy( - balance = BigDecimal.ZERO.format { crypto(status.currency) }, - onBuyCryptoClick = { onBuyCryptoClick(status) }, - onShowAddressClick = { onShowAddressClick(status) }, - isLoading = false, - ) - } - } - } - - private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { - modelScope.launch { - getLegacyTopUpUrlUseCase(status).onRight { - urlOpener.openUrl(it) - } - } - } - - private fun onShowAddressClick(status: CryptoCurrencyStatus) { - val currency = status.currency - val networkAddress = status.value.networkAddress ?: return - - update { - it.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = { - update { - it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) - } - }, - content = TokenReceiveBottomSheetConfig( - asset = TokenReceiveBottomSheetConfig.Asset.Currency( - name = currency.name, - symbol = currency.symbol, - ), - network = currency.network, - networkAddress = networkAddress, - showMemoDisclaimer = - currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) - clipboardManager.setText(text = it, isSensitive = true) - }, - onShareClick = { - Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) - shareManager.shareText(text = it) - }, - ), - ), - ) - } - } - - private fun onRefreshBalanceClick(userWallet: UserWallet) { - update { - it.copy(isLoading = true) - } - modelScope.launch { - if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) - .onLeft(Timber::e) - } else { - fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true) - } - } + params.modelCallbacks.onDone() } private fun saveWalletAndDone() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt index c67669148b..d2e579d459 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH16 -import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme @@ -43,11 +41,6 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi .weight(.48f) .fillMaxWidth(), state = state.artwork, - balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "", - isRefreshing = state.isLoading, - onRefreshBalanceClick = { - (state as? OnboardingTwinUM.TopUp)?.onRefreshClick() - }, ) AnimatedContent( @@ -60,16 +53,10 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi when (st) { is OnboardingTwinUM.ResetWarning -> ResetWarning(st) is OnboardingTwinUM.ScanCard -> ScanCard(st) - is OnboardingTwinUM.TopUp -> TopUp(st) is OnboardingTwinUM.Welcome -> Welcome(st) - OnboardingTwinUM.TopUpPrepare -> {} } } } - - if (state is OnboardingTwinUM.TopUp) { - TokenReceiveBottomSheet(config = state.bottomSheetConfig) - } } @Suppress("LongMethod") @@ -154,55 +141,6 @@ private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifie } } -@Composable -private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Column( - modifier = Modifier - .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) - .weight(1f) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResourceSafe(R.string.onboarding_topup_title), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - SpacerH16() - - Text( - text = stringResourceSafe(R.string.onboarding_top_up_body), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body1, - ) - } - - PrimaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), - onClick = state.onBuyCryptoClick, - ) - - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowAddressClick, - ) - } -} - @Composable private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) { Column( @@ -287,14 +225,6 @@ private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifi } } -@Preview(showBackground = true) -@Composable -private fun PreviewTopUp() { - TangemThemePreview { - OnboardingTwin(OnboardingTwinUM.TopUp()) - } -} - @Preview(showBackground = true) @Composable private fun PreviewWelcome() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt index 97758be4e1..3e0240af22 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -1,12 +1,10 @@ package com.tangem.features.onboarding.v2.twin.impl.ui +import android.annotation.SuppressLint import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Transition import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -16,21 +14,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.zIndex -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.components.artwork.ArtworkUM -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.wallets.models.Artwork -import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard -import com.tangem.features.onboarding.v2.impl.R import kotlinx.coroutines.delay import java.util.concurrent.TimeUnit @@ -45,8 +37,6 @@ internal sealed class TwinWalletArtworkUM { FirstCard, SecondCard } } - - data object TopUp : TwinWalletArtworkUM() } private data class CardsTransitionState( @@ -64,15 +54,10 @@ private data class WalletCardTransitionState( val zIndex: Float = 0f, ) +@SuppressLint("UnusedBoxWithConstraintsScope") @Suppress("LongMethod") @Composable -internal fun TwinWalletArtworks( - state: TwinWalletArtworkUM, - balance: String, - isRefreshing: Boolean, - onRefreshBalanceClick: () -> Unit, - modifier: Modifier = Modifier, -) { +internal fun TwinWalletArtworks(state: TwinWalletArtworkUM, modifier: Modifier = Modifier) { BoxWithConstraints( modifier .heightIn(min = 180.dp) @@ -110,22 +95,6 @@ internal fun TwinWalletArtworks( } } - AnimatedVisibility( - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Box( - modifier = Modifier - .padding(vertical = 24.dp, horizontal = 16.dp) - .fillMaxSize() - .background( - TangemTheme.colors.button.secondary, - shape = TangemTheme.shapes.roundedCornersMedium, - ), - ) - } - AnimatedTwinCards( transition1 = transition1, transition2 = transition2, @@ -133,46 +102,6 @@ internal fun TwinWalletArtworks( .widthIn(max = 450.dp) .matchParentSize(), ) - - AnimatedVisibility( - modifier = Modifier.align(Alignment.Center), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Text( - text = stringResourceSafe(R.string.common_balance_title), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerH8() - Text( - text = balance, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerHMax() - } - } - - AnimatedVisibility( - modifier = Modifier.align(Alignment.BottomCenter), - visible = state == TwinWalletArtworkUM.TopUp, - enter = fadeIn(), - exit = fadeOut(), - ) { - RefreshButton( - isRefreshing = isRefreshing, - onRefreshBalanceClick = onRefreshBalanceClick, - ) - } } } @@ -308,26 +237,6 @@ private fun TwinWalletArtworkUM.toTransitionSetState( ) } } - TwinWalletArtworkUM.TopUp -> { - val scale = 0.4f - val yTranslation = -maxHeightDp * density - 24 * density - listOf( - CardsTransitionState( - walletCard1 = WalletCardTransitionState( - yTranslation = yTranslation, - xScale = scale, - yScale = scale, - zIndex = 2f, - ), - walletCard2 = WalletCardTransitionState( - yTranslation = yTranslation * 0.35f, - xScale = scale * 0.8f, - yScale = scale * 0.8f, - zIndex = 1f, - ), - ), - ) - } } @Preview(showBackground = true, widthDp = 360, heightDp = 640) @@ -341,16 +250,13 @@ private fun Preview() { .fillMaxSize(), contentAlignment = Alignment.Center, ) { - var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.Spread) } TwinWalletArtworks( state = state, modifier = Modifier .padding(top = 250.dp) .fillMaxWidth(), - balance = "1 USD", - isRefreshing = false, - onRefreshBalanceClick = {}, ) var index by remember { mutableIntStateOf(0) } @@ -366,7 +272,6 @@ private fun Preview() { TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), - TwinWalletArtworkUM.TopUp, ) state = list[index % list.size] diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt index 7341af8545..defa7831a4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -1,7 +1,6 @@ package com.tangem.features.onboarding.v2.twin.impl.ui.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM @Immutable @@ -11,12 +10,6 @@ internal sealed class OnboardingTwinUM { abstract val isLoading: Boolean abstract val artwork: TwinWalletArtworkUM - data object TopUpPrepare : OnboardingTwinUM() { - override val stepIndex: Int = 0 - override val isLoading: Boolean = false - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread - } - data class Welcome( override val isLoading: Boolean = false, val pairCardNumber: Int = 2, @@ -56,23 +49,9 @@ internal sealed class OnboardingTwinUM { override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) } - data class TopUp( - override val isLoading: Boolean = false, - val balance: String = "", - val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, - val onBuyCryptoClick: () -> Unit = {}, - val onShowAddressClick: () -> Unit = {}, - val onRefreshClick: () -> Unit = {}, - ) : OnboardingTwinUM() { - override val stepIndex: Int = 2 - override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp - } - fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { is Welcome -> copy(isLoading = isLoading) is ResetWarning -> copy() is ScanCard -> copy(isLoading = isLoading) - is TopUp -> copy(isLoading = isLoading) - TopUpPrepare -> this } } \ No newline at end of file From b9e992c5338ed285442a93e6da8c72022530c297 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 18:02:23 +0700 Subject: [PATCH 80/87] Updated on 2026-08-14 --- .../tangem/common/ui/account/AccountIcon.kt | 134 ++++++++++++++++++ .../tangem/common/ui/account/AccountRow.kt | 110 ++++++++++++++ .../ui/account/CryptoPortfolioIconExt.kt | 5 +- .../ui/account/CryptoPortfolioIconUM.kt | 7 +- .../archived/entity/AccountArchivedUM.kt | 6 +- .../archived/ui/ArchivedAccountListContent.kt | 42 ++---- .../createedit/AccountCreateEditModel.kt | 2 +- .../createedit/entity/AccountCreateEditUM.kt | 2 +- .../entity/AccountCreateEditUMBuilder.kt | 2 +- .../createedit/ui/AccountCreateEditContent.kt | 52 ++----- .../account/details/AccountDetailsModel.kt | 2 +- .../details/entity/AccountDetailsUM.kt | 2 +- .../details/ui/AccountDetailsContent.kt | 75 ++-------- 13 files changed, 296 insertions(+), 145 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt rename features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt => common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt (66%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt new file mode 100644 index 0000000000..8a441922f5 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIcon.kt @@ -0,0 +1,134 @@ +package com.tangem.common.ui.account + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.account.CryptoPortfolioIcon.Color + +enum class AccountIconSize { + Default, Large, Medium, Small, ExtraSmall +} + +/** + * Displays an account icon that can either show a letter (derived from [name]) + * or a predefined vector resource (from [icon]). + * + * The background color is determined by the icon's [CryptoPortfolioIconUM.color], + * and the icon size, text style, and box modifier are adapted based on the given [size]. + * + * @param name The text reference used to resolve and display the first letter + * when [icon] is set to [CryptoPortfolioIcon.Icon.Letter]. + * @param icon The account icon definition, which can be a letter or a drawable resource. + * @param size The size of the icon, defined by [AccountIconSize]. + */ +@Composable +fun AccountIcon( + name: TextReference, + icon: CryptoPortfolioIconUM, + size: AccountIconSize, + modifier: Modifier = Modifier, +) { + val boxModifier = modifier.selectBoxModifier(size) + val iconSize = Modifier.selectIconSize(size) + val textStyle = when (size) { + AccountIconSize.Default -> TangemTheme.typography.h3 + AccountIconSize.Large -> TangemTheme.typography.h1 + AccountIconSize.Medium -> TangemTheme.typography.subtitle1 + AccountIconSize.Small -> TangemTheme.typography.subtitle2 + AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1 + } + Box( + contentAlignment = Alignment.Center, + modifier = boxModifier.background(icon.color.getUiColor()), + ) { + val icon = icon.value + val letter = name.resolveReference().firstOrNull() + when { + icon == CryptoPortfolioIcon.Icon.Letter -> Text( + text = letter?.uppercase() ?: "", + style = textStyle, + color = TangemTheme.colors.text.constantWhite, + ) + else -> Icon( + modifier = iconSize, + tint = TangemTheme.colors.text.constantWhite, + imageVector = ImageVector.vectorResource(id = icon.getResId()), + contentDescription = null, + ) + } + } +} + +private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) { + AccountIconSize.Default -> this.size(20.dp) + AccountIconSize.Large -> this.size(40.dp) + AccountIconSize.Medium -> this.size(16.dp) + AccountIconSize.Small -> this.size(12.dp) + AccountIconSize.ExtraSmall -> this.size(8.dp) +} + +private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) { + AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp)) + AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp)) + AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp)) + AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp)) + AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp)) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_AccountIcon() { + TangemThemePreview { + Sample() + } +} + +@Composable +private fun Sample() { + val name = stringReference("Account Name") + Row( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small) + AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall) + } + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small) + AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall) + } + } +} + +object AccountIconPreviewData { + + fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM( + value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(), + color = Color.entries.random(), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt new file mode 100644 index 0000000000..c48fa40072 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountRow.kt @@ -0,0 +1,110 @@ +package com.tangem.common.ui.account + +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.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.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Displays a row representing an account with an icon, title, and subtitle. + * + * The row consists of: + * - An [AccountIcon] on the left. + * - A column with the [title] and [subtitle] texts, which can be displayed in normal + * or reversed order depending on [isReverse]. + * + * The layout uses horizontal spacing between the icon and text, and vertical spacing + * between the title and subtitle. + * + * @param title The main text shown in the row, usually representing the account name. + * @param subtitle The secondary text, typically providing additional details about the account. + * @param icon The account icon definition, displayed using [AccountIcon]. + * @param isReverse If `true`, the [subtitle] is displayed above the [title]. + * Otherwise, the [title] is displayed above the [subtitle]. + */ +@Composable +fun AccountRow( + title: TextReference, + subtitle: TextReference, + icon: CryptoPortfolioIconUM, + modifier: Modifier = Modifier, + isReverse: Boolean = false, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AccountIcon( + name = title, + icon = icon, + size = AccountIconSize.Default, + ) + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + if (isReverse) { + Subtitle(subtitle) + Title(title) + } else { + Title(title) + Subtitle(subtitle) + } + } + } +} + +@Composable +private fun Title(title: TextReference) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) +} + +@Composable +private fun Subtitle(subtitle: TextReference) { + Text( + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + text = subtitle.resolveReference(), + ) +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Sample() + } +} + +@Composable +private fun Sample() { + val name = stringReference("Main account") + val info = stringReference("10 tokens in 2 networks") + val subtitle = resourceReference(R.string.account_form_name) + fun icon(letter: Boolean = false) = AccountIconPreviewData.randomAccountIcon(letter) + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + AccountRow(title = name, subtitle = info, icon = icon()) + AccountRow(title = name, subtitle = subtitle, icon = icon(), isReverse = true) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt index 14015c86b2..d97401c0a3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconExt.kt @@ -47,4 +47,7 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int { CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24 CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24 } -} \ No newline at end of file +} + +fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this) +fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt similarity index 66% rename from features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt rename to common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt index 299fb679dc..cb8b2d22af 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/common/UM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/CryptoPortfolioIconUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.account.common +package com.tangem.common.ui.account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.CryptoPortfolioIcon.Color @@ -12,7 +12,4 @@ data class CryptoPortfolioIconUM( value = domainModel.value, color = domainModel.color, ) -} - -fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this) -fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color) \ No newline at end of file +} \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt index ee42b871ff..bd72960062 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.account.archived.entity +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.account.common.CryptoPortfolioIconUM import kotlinx.collections.immutable.ImmutableList internal sealed interface AccountArchivedUM { @@ -20,8 +20,8 @@ internal sealed interface AccountArchivedUM { internal data class ArchivedAccountUM( val accountId: String, - val accountName: String, - val accountIcon: CryptoPortfolioIconUM, + val accountName: TextReference, + val accountIconUM: CryptoPortfolioIconUM, val tokensInfo: TextReference, val onClick: (accountId: String) -> Unit, ) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt index 65b5e1794c..a563f952d3 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ui/ArchivedAccountListContent.kt @@ -6,33 +6,28 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator 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.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountRow import com.tangem.core.res.R import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.archived.entity.AccountArchivedUM import com.tangem.features.account.archived.entity.ArchivedAccountUM -import com.tangem.features.account.common.toUM -import com.tangem.features.account.details.ui.AccountIcon import kotlinx.collections.immutable.toImmutableList @Composable @@ -135,28 +130,12 @@ private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Mod verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - AccountIcon( - modifier = Modifier - .size(36.dp) - .clip(RoundedCornerShape(9.dp)), - accountName = item.accountName, - accountIcon = item.accountIcon, - ) - Column( + AccountRow( + title = item.accountName, + subtitle = item.tokensInfo, + icon = item.accountIconUM, modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = item.accountName, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - text = item.tokensInfo.resolveReference(), - ) - } + ) SecondarySmallButton( config = SmallButtonConfig( @@ -179,13 +158,14 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider:: @Suppress("MagicNumber") private class PreviewStateProvider : CollectionPreviewParameterProvider( buildList { - fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + fun portfolioIcon() = AccountIconPreviewData.randomAccountIcon() + val accountName = stringReference("Account name") val firstList = List(10) { ArchivedAccountUM( accountId = it.toString(), - accountName = "Account name", - accountIcon = portfolioIcon(), + accountName = accountName, + accountIconUM = portfolioIcon(), tokensInfo = stringReference("10 tokens in 2 networks"), onClick = {}, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 0f5a66dedf..af1c7d4ed1 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.account.createedit import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.common.ui.account.toDomain import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,7 +21,6 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent -import com.tangem.features.account.common.toDomain import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt index df93dde4b9..330fc2352f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUM.kt @@ -1,8 +1,8 @@ package com.tangem.features.account.createedit.entity +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.features.account.common.CryptoPortfolioIconUM import kotlinx.collections.immutable.ImmutableList data class AccountCreateEditUM( diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index bacbd306ab..654515e302 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.features.account.createedit.entity +import com.tangem.common.ui.account.toUM import com.tangem.core.res.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -7,7 +8,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent -import com.tangem.features.account.common.toUM import kotlinx.collections.immutable.toImmutableList internal class AccountCreateEditUMBuilder( diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index b94e9198fe..639b44419e 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -24,6 +24,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.R +import com.tangem.common.ui.account.AccountIcon +import com.tangem.common.ui.account.AccountIconPreviewData +import com.tangem.common.ui.account.AccountIconSize import com.tangem.common.ui.account.getResId import com.tangem.common.ui.account.getUiColor import com.tangem.core.ui.components.PrimaryButton @@ -36,7 +39,6 @@ import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.features.account.common.toUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account import kotlinx.collections.immutable.toImmutableList @@ -69,7 +71,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi SpacerH24() AccountColor(state.colorsState) SpacerH24() - AccountIcon(state.iconsState) + AccountIcons(state.iconsState) SpacerH8() Text( modifier = Modifier.padding(horizontal = 8.dp), @@ -100,7 +102,11 @@ private fun AccountSummary(account: Account) { ) { Spacer(modifier = Modifier.height(24.dp)) - AccountIcon(account) + AccountIcon( + name = stringReference(account.name), + icon = account.portfolioIcon, + size = AccountIconSize.Large, + ) Spacer(modifier = Modifier.height(24.dp)) Text( @@ -122,34 +128,6 @@ private fun AccountSummary(account: Account) { } } -@Composable -private fun AccountIcon(account: Account) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(88.dp) - .clip(RoundedCornerShape(TangemTheme.dimens.radius24)) - .background(account.portfolioIcon.color.getUiColor()), - ) { - val icon = account.portfolioIcon.value - val letter = account.name.firstOrNull() - ?: account.inputPlaceholder.resolveReference().first() - when { - icon == CryptoPortfolioIcon.Icon.Letter -> Text( - text = letter.uppercase(), - style = TangemTheme.typography.head, - color = TangemTheme.colors.text.constantWhite, - ) - else -> Icon( - modifier = Modifier.size(44.dp), - tint = TangemTheme.colors.text.constantWhite, - imageVector = ImageVector.vectorResource(id = icon.getResId()), - contentDescription = null, - ) - } - } -} - @Suppress("LongMethod", "MagicNumber") @Composable private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { @@ -201,7 +179,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) { @Suppress("LongMethod", "MagicNumber") @Composable -private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) { +private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) { Box( Modifier .clip(RoundedCornerShape(16.dp)) @@ -296,7 +274,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider Text( - text = letter.uppercase(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.constantWhite, - ) - else -> Icon( - modifier = Modifier.size(20.dp), - tint = TangemTheme.colors.text.constantWhite, - imageVector = ImageVector.vectorResource(id = icon.getResId()), - contentDescription = null, - ) - } - } -} - @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -217,20 +170,18 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider:: private class PreviewStateProvider : CollectionPreviewParameterProvider( buildList { - var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM() + val accountName = "Main" + var portfolioIcon = AccountIconPreviewData.randomAccountIcon() val first = AccountDetailsUM( onCloseClick = {}, onAccountEditClick = {}, onManageTokensClick = {}, onArchiveAccountClick = {}, - accountName = "Main", + accountName = accountName, accountIcon = portfolioIcon, ) add(first) - portfolioIcon = portfolioIcon.copy( - value = CryptoPortfolioIcon.Icon.Letter, - color = CryptoPortfolioIcon.Color.entries.random(), - ) + portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true) add(first.copy(accountIcon = portfolioIcon)) }, ) \ No newline at end of file From 369f449c09cc0e53488abbf06fd233fe00a76985 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:09:31 +0500 Subject: [PATCH 81/87] Updated on 2026-08-14 --- .../bottomsheets/OptionsBottomSheet.kt | 99 +++++++++++++++++++ .../bottomsheets/OptionsBottomSheetContent.kt | 23 +++++ features/details/impl/build.gradle.kts | 1 + .../details/entity/UserWalletListUM.kt | 2 + .../details/model/UserWalletListModel.kt | 75 ++++++++++++-- .../details/ui/UserWalletListBlock.kt | 15 +++ features/welcome/impl/build.gradle.kts | 1 + .../welcome/impl/model/WelcomeModel.kt | 50 ++++++++-- .../welcome/impl/ui/AddWalletBottomSheet.kt | 90 ----------------- .../welcome/impl/ui/WelcomeSelectWallet.kt | 13 +++ 10 files changed, 263 insertions(+), 106 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt delete mode 100644 features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt new file mode 100644 index 0000000000..d355f8ff22 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheet.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.components.bottomsheets + +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.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Generic options bottom sheet component + * + * @param config Bottom sheet configuration containing OptionsBottomSheetContent + * @param title Title text for the bottom sheet + * @param containerColor Background color of the bottom sheet + */ +@Composable +fun OptionsBottomSheet( + config: TangemBottomSheetConfig, + title: TextReference, + containerColor: androidx.compose.ui.graphics.Color = TangemTheme.colors.background.tertiary, +) { + TangemBottomSheet( + config = config, + titleText = title, + containerColor = containerColor, + content = { content -> + OptionsBottomSheetContent(content = content) + }, + ) +} + +@Composable +private fun OptionsBottomSheetContent(content: OptionsBottomSheetContent) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + content.options.forEachIndexed { index, option -> + InputRowDefault( + text = option.label, + showDivider = index < content.options.size - 1, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = content.options.size - 1, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(option.key) }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun OptionsBottomSheetPreview() { + TangemThemePreview { + OptionsBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = "option1", + label = TextReference.Str("First Option"), + ), + BottomSheetOption( + key = "option2", + label = TextReference.Str("Second Option"), + ), + BottomSheetOption( + key = "option3", + label = TextReference.Str("Third Option"), + ), + ), + onOptionClick = {}, + ), + ), + title = TextReference.Str("Select Option"), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt new file mode 100644 index 0000000000..02d3ab9479 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/OptionsBottomSheetContent.kt @@ -0,0 +1,23 @@ +package com.tangem.core.ui.components.bottomsheets + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param key Unique identifier for the option + * @param label Display text for the option + */ +data class BottomSheetOption( + val key: String, + val label: TextReference, +) + +/** + * @param options List of options to display + * @param onOptionClick Callback when an option is clicked, receives the option key + */ +data class OptionsBottomSheetContent( + val options: ImmutableList = persistentListOf(), + val onOptionClick: (String) -> Unit = {}, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index e44ec003ea..4f3b8566e0 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) + implementation(projects.features.createWalletSelection.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..f217f837ce 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -11,4 +12,5 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, ) \ 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 index d1d008e54f..01b1ebb3b1 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -6,12 +6,17 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.R.* +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R -import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -20,16 +25,19 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class UserWalletListModel @Inject constructor( userWalletsFetcherFactory: UserWalletsFetcher.Factory, shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val router: Router, private val messageSender: UiMessageSender, - private val userWalletSaver: UserWalletSaver, override val dispatchers: CoroutineDispatcherProvider, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -45,7 +53,8 @@ internal class UserWalletListModel @Inject constructor( userWallets = persistentListOf(), isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, - onAddNewWalletClick = ::addUserWallet, + onAddNewWalletClick = ::showAddWalletBottomSheet, + addWalletBottomSheet = TangemBottomSheetConfig.Empty, ), ) @@ -54,8 +63,9 @@ internal class UserWalletListModel @Inject constructor( flow = userWalletsFetcher.userWallets, flow2 = shouldSaveUserWalletsUseCase(), flow3 = isWalletSavingInProgress, - transform = ::updateState, - ).launchIn(modelScope) + ) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress -> + updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress) + }.launchIn(modelScope) } private fun updateState( @@ -74,7 +84,58 @@ internal class UserWalletListModel @Inject constructor( ) } - private fun addUserWallet() = withProgress(isWalletSavingInProgress) { - userWalletSaver.scanAndSaveUserWallet(modelScope) + private fun showAddWalletBottomSheet() { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismissAddWalletBottomSheet, + content = createAddWalletBottomSheetContent(), + ), + ) + } + } + + private fun dismissAddWalletBottomSheet() { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false), + ) + } + } + + private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent { + return OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + dismissAddWalletBottomSheet() + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } + } + }, + ) + } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" } } \ 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 ec27900868..bea3f8c35d 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 @@ -15,9 +15,13 @@ 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.userwallet.UserWalletItem +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent @@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M onClick = state.onAddNewWalletClick, ) } + + AddWalletBottomSheet(state.addWalletBottomSheet) } @Composable @@ -94,6 +100,15 @@ private fun AddWalletButton( } } +@Composable +private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 8a2a3c3c3f..78977b9216 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** Core */ implementation(projects.core.configToggles) implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.analytics) implementation(projects.common.routing) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index c189ac83b5..6a432d7e17 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -6,8 +6,12 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.BottomSheetOption +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked @@ -15,11 +19,10 @@ import com.tangem.domain.core.wallets.UserWalletsListRepository import com.tangem.domain.core.wallets.error.UnlockWalletError import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.* import com.tangem.features.welcome.impl.ui.state.WelcomeUM import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -46,6 +49,8 @@ internal class WelcomeModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase, private val walletsRepository: WalletsRepository, + private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, + private val urlOpener: UrlOpener, ) : Model() { val uiState: StateFlow @@ -149,8 +154,27 @@ internal class WelcomeModel @Inject constructor( currentState.copy( addWalletBottomSheet = TangemBottomSheetConfig( isShown = true, - content = AddWalletBottomSheetContentUM( - onOptionClick = ::onAddWalletOptionClick, + content = OptionsBottomSheetContent( + options = persistentListOf( + BottomSheetOption( + key = ADD_WALLET_KEY_CREATE, + label = resourceReference(R.string.home_button_create_new_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_ADD, + label = resourceReference(R.string.home_button_add_existing_wallet), + ), + BottomSheetOption( + key = ADD_WALLET_KEY_BUY, + label = resourceReference(R.string.details_buy_wallet), + ), + ), + onOptionClick = { optionKey -> + updateSelectState { + it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false)) + } + onAddWalletOptionClick(optionKey) + }, ), onDismissRequest = { updateSelectState { @@ -162,11 +186,13 @@ internal class WelcomeModel @Inject constructor( } } - private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) { - when (option) { - Create -> router.push(AppRoute.CreateWalletSelection) - Add -> router.push(AppRoute.AddExistingWallet) - Buy -> Unit // TODO + private fun onAddWalletOptionClick(optionKey: String) { + when (optionKey) { + ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection) + ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet) + ADD_WALLET_KEY_BUY -> modelScope.launch { + generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) } + } } } @@ -246,4 +272,10 @@ internal class WelcomeModel @Inject constructor( } } } + + companion object { + private const val ADD_WALLET_KEY_CREATE = "create" + private const val ADD_WALLET_KEY_ADD = "add" + private const val ADD_WALLET_KEY_BUY = "buy" + } } \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt deleted file mode 100644 index 5d26549787..0000000000 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/AddWalletBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.welcome.impl.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.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.inputrow.InputRowDefault -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.welcome.impl.R -import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM - -@Composable -fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - titleText = resourceReference(R.string.auth_info_add_wallet_title), - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: AddWalletBottomSheetContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - InputRowDefault( - text = resourceReference(R.string.home_button_create_new_wallet), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 0, - lastIndex = 3, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Create) }, - ) - InputRowDefault( - text = resourceReference(R.string.home_button_add_existing_wallet), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 1, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Add) }, - ) - InputRowDefault( - text = resourceReference(R.string.details_buy_wallet), - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = 2, - lastIndex = 2, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action) - .clickable { content.onOptionClick(AddWalletBottomSheetContentUM.Option.Buy) }, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - AddWalletBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = AddWalletBottomSheetContentUM(), - ), - ) - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt index 9b3bba406d..7e9e671696 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/ui/WelcomeSelectWallet.kt @@ -19,9 +19,13 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.tangem.common.ui.userwallet.UserWalletItem import com.tangem.common.ui.userwallet.state.UserWalletItemUM +import com.tangem.core.ui.R.* import com.tangem.core.ui.components.* import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.welcome.impl.R @@ -170,4 +174,13 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) { color = TangemTheme.colors.text.secondary, ) } +} + +@Composable +fun AddWalletBottomSheet(config: TangemBottomSheetConfig) { + OptionsBottomSheet( + config = config, + title = resourceReference(string.auth_info_add_wallet_title), + containerColor = TangemTheme.colors.background.tertiary, + ) } \ No newline at end of file From b6fad30af95c395aa0e68493f8b7c7a2f3b424e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 13:35:17 +0400 Subject: [PATCH 82/87] Updated on 2026-08-14 --- .../domain/account/models/AccountList.kt | 35 ++++- .../usecase/AddCryptoPortfolioUseCase.kt | 17 +-- .../usecase/RecoverCryptoPortfolioUseCase.kt | 12 +- .../usecase/UpdateCryptoPortfolioUseCase.kt | 2 +- .../domain/account/models/AccountListTest.kt | 2 +- .../usecase/AddCryptoPortfolioUseCaseTest.kt | 10 +- .../ArchiveCryptoPortfolioUseCaseTest.kt | 6 +- .../GetUnoccupiedAccountIndexUseCaseTest.kt | 4 +- .../RecoverCryptoPortfolioUseCaseTest.kt | 12 +- .../UpdateCryptoPortfolioUseCaseTest.kt | 4 +- .../tangem/domain/account/utils/AccountExt.kt | 11 +- .../tangem/domain/models/account/Account.kt | 123 +++++++----------- .../domain/models/account/AccountTest.kt | 37 ++---- .../archived/ArchivedAccountListModel.kt | 2 +- .../createedit/AccountCreateEditModel.kt | 4 +- .../entity/AccountCreateEditUMBuilder.kt | 2 +- .../account/details/AccountDetailsModel.kt | 2 +- 17 files changed, 126 insertions(+), 159 deletions(-) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 319babf91b..89f7fd33c8 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -3,7 +3,10 @@ package com.tangem.domain.account.models import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure +import com.tangem.domain.models.TokensGroupType +import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable @@ -22,6 +25,8 @@ data class AccountList private constructor( val userWallet: UserWallet, val accounts: Set, val totalAccounts: Int, + val sortType: TokensSortType, + val groupType: TokensGroupType, ) { /** Retrieves the main crypto portfolio account from the list of accounts */ @@ -48,6 +53,8 @@ data class AccountList private constructor( userWallet = this.userWallet, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, + sortType = this.sortType, + groupType = this.groupType, ) } @@ -68,6 +75,8 @@ data class AccountList private constructor( userWallet = this.userWallet, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, + sortType = this.sortType, + groupType = this.groupType, ) } @@ -132,6 +141,8 @@ data class AccountList private constructor( userWallet: UserWallet, accounts: Set, totalAccounts: Int, + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, ): Either = either { ensure(accounts.isNotEmpty()) { Error.EmptyAccountsList } @@ -149,10 +160,16 @@ data class AccountList private constructor( val uniqueAccountIdsCount = accounts.map { it.accountId.value }.distinct().size ensure(accounts.size == uniqueAccountIdsCount) { Error.DuplicateAccountIds } - val uniqueAccountNameCount = accounts.map { it.name.value }.distinct().size + val uniqueAccountNameCount = accounts.map { it.accountName.value }.distinct().size ensure(accounts.size == uniqueAccountNameCount) { Error.DuplicateAccountNames } - AccountList(userWallet = userWallet, accounts = accounts, totalAccounts = totalAccounts) + AccountList( + userWallet = userWallet, + accounts = accounts, + totalAccounts = totalAccounts, + sortType = sortType, + groupType = groupType, + ) } /** @@ -160,13 +177,23 @@ data class AccountList private constructor( * * @param userWallet the user wallet associated with the account list */ - fun empty(userWallet: UserWallet): AccountList { + fun empty( + userWallet: UserWallet, + cryptoCurrencies: Set = emptySet(), + sortType: TokensSortType = TokensSortType.NONE, + groupType: TokensGroupType = TokensGroupType.NONE, + ): AccountList { return AccountList( userWallet = userWallet, accounts = setOf( - Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId), + Account.CryptoPortfolio.createMainAccount( + userWalletId = userWallet.walletId, + cryptoCurrencies = cryptoCurrencies, + ), ), totalAccounts = 1, + sortType = sortType, + groupType = groupType, ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index 75722f9824..6a3866bc4b 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -64,14 +64,9 @@ class AddCryptoPortfolioUseCase( return Account.CryptoPortfolio( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = accountName, - accountIcon = icon, + icon = icon, derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) } @@ -88,7 +83,13 @@ class AddCryptoPortfolioUseCase( catch = { raise(Error.DataOperationFailed(cause = it)) }, ) - return AccountList.empty(userWallet = userWallet) + // TODO: [REDACTED_JIRA] + return AccountList.empty( + userWallet = userWallet, + cryptoCurrencies = emptySet(), + sortType = TokensSortType.NONE, + groupType = TokensGroupType.NONE, + ) } private suspend fun Raise.saveAccounts(accountList: AccountList) { diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt index 9679a036b6..f5dcec41aa 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCase.kt @@ -8,8 +8,6 @@ import arrow.core.raise.either import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.repository.AccountsCRUDRepository -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId @@ -66,14 +64,10 @@ class RecoverCryptoPortfolioUseCase( return Account.CryptoPortfolio( accountId = this.accountId, accountName = this.name, - accountIcon = this.icon, + icon = this.icon, derivationIndex = this.derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + // TODO: [REDACTED_JIRA] + cryptoCurrencies = emptySet(), ) } diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt index 8c2208e552..4451a5f50d 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCase.kt @@ -79,7 +79,7 @@ class UpdateCryptoPortfolioUseCase( } private fun Account.CryptoPortfolio.setIcon(icon: CryptoPortfolioIcon?): Account.CryptoPortfolio { - return if (icon != null) this.copy(accountIcon = icon) else this + return if (icon != null) this.copy(icon = icon) else this } /** diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index d5323c0a85..0c43198fa0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -123,7 +123,7 @@ class AccountListTest { accounts = setOf( Account.CryptoPortfolio.createMainAccount(userWalletId), Account.CryptoPortfolio.createMainAccount(userWalletId).copy( - accountIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), + icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), ), ), expected = AccountList.Error.ExceedsMaxMainAccountsCount.left(), diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index cf47f9b807..f30bc1dfda 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -46,7 +46,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -75,7 +75,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -107,7 +107,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -138,7 +138,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) @@ -170,7 +170,7 @@ class AddCryptoPortfolioUseCaseTest { // Act val actual = useCase( userWalletId = userWalletId, - accountName = newAccount.name, + accountName = newAccount.accountName, icon = newAccount.icon, derivationIndex = newAccount.derivationIndex, ) diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index aaea0a5379..1e442938fa 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -39,8 +39,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! val accountId = account.accountId - val archivedAccount = account.copy(isArchived = true) - val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + val updatedAccountList = (accountList - account).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() @@ -130,8 +129,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! val accountId = account.accountId - val archivedAccount = account.copy(isArchived = true) - val updatedAccountList = (accountList - archivedAccount).getOrNull()!! + val updatedAccountList = (accountList - account).getOrNull()!! val exception = IllegalStateException("Save failed") diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt index ec8af7f2b7..4c994aeb21 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/GetUnoccupiedAccountIndexUseCaseTest.kt @@ -1,9 +1,9 @@ package com.tangem.domain.account.usecase import arrow.core.left -import arrow.core.right import com.google.common.truth.Truth import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import io.mockk.clearMocks import io.mockk.coEvery @@ -35,7 +35,7 @@ class GetUnoccupiedAccountIndexUseCaseTest { val actual = useCase(userWalletId = userWalletId) // Assert - val expected = 4.right() + val expected = DerivationIndex(4) Truth.assertThat(actual).isEqualTo(expected) coVerify { crudRepository.getTotalAccountsCount(userWalletId) } diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index 316e789754..7c8f1a847f 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -43,15 +43,14 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val archivedAccount = ArchivedAccount( accountId = account.accountId, - name = account.name, + name = account.accountName, icon = account.icon, derivationIndex = account.derivationIndex, tokensCount = 1, networksCount = 1, ) - val recoveredAccount = account.copy(isArchived = false) - val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val updatedAccountList = (accountList + account).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccount(account.accountId) } returns archivedAccount.toOption() @@ -60,7 +59,7 @@ class RecoverCryptoPortfolioUseCaseTest { val actual = useCase(account.accountId) // Assert - val expected = recoveredAccount.right() + val expected = account.right() Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { @@ -173,15 +172,14 @@ class RecoverCryptoPortfolioUseCaseTest { val accountList = AccountList.empty(userWallet) val archivedAccount = ArchivedAccount( accountId = account.accountId, - name = account.name, + name = account.accountName, icon = account.icon, derivationIndex = account.derivationIndex, tokensCount = 1, networksCount = 1, ) - val recoveredAccount = account.copy(isArchived = false) - val updatedAccountList = (accountList + recoveredAccount).getOrNull()!! + val updatedAccountList = (accountList + account).getOrNull()!! val exception = IllegalStateException("Save failed") coEvery { crudRepository.getAccounts(userWalletId) } returns accountList.toOption() diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index d6638b5c05..f1ba896a7c 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -73,7 +73,7 @@ class UpdateCryptoPortfolioUseCaseTest { value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) - val updatedAccount = accountList.mainAccount.copy(accountIcon = newAccountIcon) + val updatedAccount = accountList.mainAccount.copy(icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() @@ -102,7 +102,7 @@ class UpdateCryptoPortfolioUseCaseTest { value = CryptoPortfolioIcon.Icon.Star, color = CryptoPortfolioIcon.Color.CaribbeanBlue, ) - val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, accountIcon = newAccountIcon) + val updatedAccount = accountList.mainAccount.copy(accountName = newAccountName, icon = newAccountIcon) val updatedAccountList = (accountList + updatedAccount).getOrNull()!! coEvery { crudRepository.getAccounts(userWalletId = userWalletId) } returns accountList.toOption() diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt index 597d6aa059..9f452c4145 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/utils/AccountExt.kt @@ -1,7 +1,5 @@ package com.tangem.domain.account.utils -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.* import com.tangem.domain.models.wallet.UserWalletId import kotlin.random.Random @@ -33,13 +31,8 @@ fun createAccount( return Account.CryptoPortfolio( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = derivationIndex), accountName = AccountName(name).getOrNull()!!, - accountIcon = icon, + icon = icon, derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = Account.CryptoPortfolio.CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index db4a5b164c..b376be0db1 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -1,9 +1,8 @@ package com.tangem.domain.models.account import arrow.core.Either +import arrow.core.getOrElse import arrow.core.raise.either -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.DerivationIndexError import com.tangem.domain.models.currency.CryptoCurrency @@ -22,7 +21,7 @@ sealed interface Account { val accountId: AccountId /** Name of the account */ - val name: AccountName + val accountName: AccountName /** The identifier of the user wallet associated with the account */ val userWalletId: UserWalletId @@ -31,21 +30,19 @@ sealed interface Account { /** * Represents a crypto portfolio account * - * @property accountId unique identifier of the account - * @property name name of the account - * @property icon icon representing the account - * @property derivationIndex index used for derivation of the account - * @property isArchived indicates whether the account is archived - * @property cryptoCurrencyList list of tokens associated with the account + * @property accountId unique identifier of the account + * @property accountName name of the account + * @property icon icon representing the account + * @property derivationIndex index used for derivation of the account + * @property cryptoCurrencies set of tokens associated with the account */ @Serializable data class CryptoPortfolio private constructor( override val accountId: AccountId, - override val name: AccountName, + override val accountName: AccountName, val icon: CryptoPortfolioIcon, val derivationIndex: DerivationIndex, - val isArchived: Boolean, - val cryptoCurrencyList: CryptoCurrencyList, + val cryptoCurrencies: Set, ) : Account { /** Indicates if the account is the main account */ @@ -54,41 +51,22 @@ sealed interface Account { /** Number of tokens in the account */ val tokensCount: Int - get() = cryptoCurrencyList.currencies.size + get() = cryptoCurrencies.size /** Number of distinct networks in the account */ val networksCount: Int - get() = cryptoCurrencyList.currencies.map(CryptoCurrency::network).distinct().size + get() = cryptoCurrencies.map(CryptoCurrency::network).distinct().size - fun copy( - accountName: AccountName = this.name, - accountIcon: CryptoPortfolioIcon = this.icon, - isArchived: Boolean = this.isArchived, - ): CryptoPortfolio { + fun copy(accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon): CryptoPortfolio { return CryptoPortfolio( accountId = this.accountId, - name = accountName, - icon = accountIcon, + accountName = accountName, + icon = icon, derivationIndex = this.derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = this.cryptoCurrencyList, + cryptoCurrencies = this.cryptoCurrencies, ) } - /** - * Represents a list of tokens in the account - * - * @property currencies set of cryptocurrencies in the account - * @property sortType sorting type for the tokens - * @property groupType grouping type for the tokens - */ - @Serializable - data class CryptoCurrencyList( - val currencies: Set, - val sortType: TokensSortType, - val groupType: TokensGroupType, - ) - /** * Represents possible errors when creating a crypto portfolio account */ @@ -109,33 +87,34 @@ sealed interface Account { /** * Constructor for creating a [CryptoPortfolio] instance * - * @param accountId unique identifier of the account - * @param name name of the account - * @param accountIcon icon representing the account - * @param derivationIndex index used for derivation of the account - * @param isArchived indicates whether the account is archived - * @param cryptoCurrencyList list of tokens associated with the account + * @param accountId unique identifier of the account + * @param name name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account */ - @Suppress("LongParameterList") operator fun invoke( accountId: AccountId, name: String, - accountIcon: CryptoPortfolioIcon, + icon: CryptoPortfolioIcon, derivationIndex: Int, - isArchived: Boolean, - cryptoCurrencyList: CryptoCurrencyList, + cryptoCurrencies: Set = emptySet(), ): Either { return either { - val accountName = AccountName(value = name).mapLeft(::AccountNameError).bind() - val derivationIndex = DerivationIndex(derivationIndex).mapLeft(::DerivationIndexError).bind() + val accountName = AccountName(value = name).getOrElse { + raise(AccountNameError(cause = it)) + } + + val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse { + raise(DerivationIndexError(cause = it)) + } invoke( accountId = accountId, accountName = accountName, - accountIcon = accountIcon, + icon = icon, derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, + cryptoCurrencies = cryptoCurrencies, ) } } @@ -143,38 +122,39 @@ sealed interface Account { /** * Constructor for creating a [CryptoPortfolio] instance * - * @param accountId unique identifier of the account - * @param accountName name of the account - * @param accountIcon icon representing the account - * @param derivationIndex index used for derivation of the account - * @param isArchived indicates whether the account is archived - * @param cryptoCurrencyList list of tokens associated with the account + * @param accountId unique identifier of the account + * @param accountName name of the account + * @param icon icon representing the account + * @param derivationIndex index used for derivation of the account + * @param cryptoCurrencies set of tokens associated with the account */ @Suppress("LongParameterList") operator fun invoke( accountId: AccountId, accountName: AccountName, - accountIcon: CryptoPortfolioIcon, + icon: CryptoPortfolioIcon, derivationIndex: DerivationIndex, - isArchived: Boolean, - cryptoCurrencyList: CryptoCurrencyList, + cryptoCurrencies: Set = emptySet(), ): CryptoPortfolio { return CryptoPortfolio( accountId = accountId, - name = accountName, - icon = accountIcon, + accountName = accountName, + icon = icon, derivationIndex = derivationIndex, - isArchived = isArchived, - cryptoCurrencyList = cryptoCurrencyList, + cryptoCurrencies = cryptoCurrencies, ) } /** * Creates a main account for the given user wallet ID * - * @param userWalletId the ID of the user wallet + * @param userWalletId the ID of the user wallet + * @param cryptoCurrencies set of tokens associated with the account */ - fun createMainAccount(userWalletId: UserWalletId): CryptoPortfolio { + fun createMainAccount( + userWalletId: UserWalletId, + cryptoCurrencies: Set = emptySet(), + ): CryptoPortfolio { val derivationIndex = DerivationIndex.Main return CryptoPortfolio( @@ -182,15 +162,10 @@ sealed interface Account { userWalletId = userWalletId, derivationIndex = derivationIndex, ), - name = AccountName.Main, + accountName = AccountName.Main, icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = cryptoCurrencies, ) } } diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index eabb287672..91baaf2a76 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -1,10 +1,7 @@ package com.tangem.domain.models.account import com.google.common.truth.Truth -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account.CryptoPortfolio -import com.tangem.domain.models.account.Account.CryptoPortfolio.CryptoCurrencyList import com.tangem.domain.models.account.Account.CryptoPortfolio.Error.AccountNameError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -100,13 +97,12 @@ class AccountTest { val name = "" // Act - val actual = CryptoPortfolio( + val actual = CryptoPortfolio.invoke( accountId = mockk(), name = name, - accountIcon = mockk(), + icon = mockk(), derivationIndex = 0, - isArchived = false, - cryptoCurrencyList = mockk(), + cryptoCurrencies = emptySet(), ) .leftOrNull()!! @@ -125,14 +121,9 @@ class AccountTest { derivationIndex = derivationIndex, ), name = "Test Account", - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), derivationIndex = derivationIndex.value, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) .getOrNull()!! @@ -157,14 +148,9 @@ class AccountTest { derivationIndex = derivationIndex, ), accountName = AccountName.Main, - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = emptySet(), - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = emptySet(), ) Truth.assertThat(actual).isEqualTo(expected) @@ -182,14 +168,9 @@ class AccountTest { return CryptoPortfolio.invoke( accountId = AccountId.forCryptoPortfolio(userWalletId = userWalletId, derivationIndex = accountIndex), name = name, - accountIcon = CryptoPortfolioIcon.ofMainAccount(userWalletId), + icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - isArchived = false, - cryptoCurrencyList = CryptoCurrencyList( - currencies = currencies, - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ), + cryptoCurrencies = currencies, ) .getOrNull()!! } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index 3c2aed7f6b..a93cdcca2b 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -48,7 +48,7 @@ internal class ArchivedAccountListModel @Inject constructor( ) messageSender.send( DialogMessage( - title = stringReference(account.name.value), + title = stringReference(account.accountName.value), message = TextReference.EMPTY, firstActionBuilder = { firstAction }, secondActionBuilder = { secondAction }, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index af1c7d4ed1..5c61ebc3f4 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -109,7 +109,7 @@ internal class AccountCreateEditModel @Inject constructor( val state = uiState.value val name = AccountName(state.account.name).getOrNull() ?: return val icon = state.account.portfolioIcon.toDomain() - val isNewName = name != params.account.name + val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon updateCryptoPortfolioUseCase( icon = if (isNewIcon) icon else null, @@ -143,7 +143,7 @@ internal class AccountCreateEditModel @Inject constructor( val isAvailableForConfirm = when (params) { is AccountCreateEditComponent.Params.Create -> isValidName is AccountCreateEditComponent.Params.Edit -> { - val isNewName = this.account.name != params.account.name.value + val isNewName = this.account.name != params.account.accountName.value val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon isValidName && (isNewName || isNewIcon) } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt index 654515e302..24b69681c9 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/entity/AccountCreateEditUMBuilder.kt @@ -34,7 +34,7 @@ internal class AccountCreateEditUMBuilder( onNameChange = onNameChange, ) is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account( - name = params.account.name.value, + name = params.account.accountName.value, portfolioIcon = params.account.portfolioIcon.toUM(), derivationInfo = createAccountDerivationInfo( index = (params.account as Account.CryptoPortfolio).derivationIndex.value, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index d6fc77713e..0e40b6b39d 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -74,7 +74,7 @@ internal class AccountDetailsModel @Inject constructor( private fun getInitialState(): AccountDetailsUM { return AccountDetailsUM( - accountName = params.account.name.value, + accountName = params.account.accountName.value, accountIcon = params.account.portfolioIcon.toUM(), onCloseClick = { router.pop() }, onAccountEditClick = ::onEditAccountClick, From 30a7d0dcecd4cf29220e16e25b966f851562e8d3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 18:27:08 +0400 Subject: [PATCH 83/87] Updated on 2026-08-14 --- .../CachedCurrenciesStatusesOperations.kt | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index c82e831875..70700beea8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -168,15 +168,18 @@ class CachedCurrenciesStatusesOperations( combine( flow = getQuotes(currenciesIds), flow2 = networksStatusesUpdates, - flow3 = networksStatusesUpdates.flatMapLatest { - val currenciesAddresses = it.getOrElse(default = { emptySet() }) - .mapNotNull { - val currency = currencies.firstOrNull { currency -> currency.network == it.network } - ?: return@mapNotNull null + flow3 = networksStatusesUpdates.flatMapLatest { maybeNetworksStatuses -> + val networksStatuses = maybeNetworksStatuses.getOrNull() - currency.id to extractAddress(it) + val currenciesAddresses = if (networksStatuses == null) { + emptyMap() + } else { + currencies.associate { currency -> + val networkStatus = networksStatuses.firstOrNull { it.network == currency.network } + + currency.id to extractAddress(networkStatus) } - .toMap() + } getYieldsBalancesUpdates(userWalletId, currenciesAddresses) }, @@ -397,8 +400,11 @@ class CachedCurrenciesStatusesOperations( return channelFlow { val state = MutableStateFlow(emptyList()) - val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(currencyId = it.key, defaultAddress = it.value) + val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> + stakingIdFactory.create( + currencyId = currencyWithAddress.key, + defaultAddress = currencyWithAddress.value, + ) .getOrNull() } From 009f636520c0f52dfaa77dfc78e1348d94114670 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:50:30 +0300 Subject: [PATCH 84/87] Updated on 2026-08-14 --- .../main/assets/configs/feature_toggles_config.json | 10 +++++----- gradle/tangem_dependencies.toml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 0d8eea380b..446b607df1 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -9,7 +9,7 @@ }, { "name": "STAKING_TON_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "NFT_MEDIA_CONTENT_ENABLED", @@ -17,7 +17,7 @@ }, { "name": "STAKING_CARDANO_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "WALLET_CONNECT_REDESIGN_ENABLED", @@ -33,15 +33,15 @@ }, { "name": "SEND_VIA_SWAP_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "SWAP_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "SEND_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "WALLET_BALANCE_FETCHER_ENABLED", diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index f430a4dd17..418aa40d43 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1205" +tangemBlockchainSdk = "releases-5.28-1206" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-557" +tangemCardSdk = "releases-5.28-559" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 4b719be68338460597b3ae3ed8d6539db9bd86d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:54:25 +0300 Subject: [PATCH 85/87] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 446b607df1..6c01d5db1e 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -37,7 +37,7 @@ }, { "name": "SWAP_REDESIGN_ENABLED", - "version": "5.28.0" + "version": "undefined" }, { "name": "SEND_REDESIGN_ENABLED", From 088a3471d953c0d258a950ed67cf8a8e79580dca Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Aug 2025 17:55:47 +0300 Subject: [PATCH 86/87] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 6c01d5db1e..4fa13e88c4 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -53,7 +53,7 @@ }, { "name": "NFT_SEND_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.28.0" }, { "name": "TANGEM_PAY_ENABLED", From 7de9a905988c32aa0f983220da08b888921dfc78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Aug 2025 01:32:14 +0500 Subject: [PATCH 87/87] Updated on 2026-08-14 --- features/details/impl/build.gradle.kts | 1 + .../details/model/UserWalletListModel.kt | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 4f3b8566e0..8f41e0f6c1 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) implementation(projects.features.createWalletSelection.api) + implementation(projects.features.hotWallet.api) /* Project - Core */ implementation(projects.core.decompose) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 01b1ebb3b1..bfdc9b284b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -17,6 +17,8 @@ import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R +import com.tangem.features.details.utils.UserWalletSaver +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList @@ -38,6 +40,8 @@ internal class UserWalletListModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, + private val userWalletSaver: UserWalletSaver, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -53,7 +57,7 @@ internal class UserWalletListModel @Inject constructor( userWallets = persistentListOf(), isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, - onAddNewWalletClick = ::showAddWalletBottomSheet, + onAddNewWalletClick = ::onAddNewWalletClick, addWalletBottomSheet = TangemBottomSheetConfig.Empty, ), ) @@ -76,7 +80,7 @@ internal class UserWalletListModel @Inject constructor( value.copy( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, - addNewWalletText = if (shouldSaveUserWallets) { + addNewWalletText = if (shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled) { resourceReference(R.string.user_wallet_list_add_button) } else { resourceReference(R.string.scan_card_settings_button) @@ -84,15 +88,21 @@ internal class UserWalletListModel @Inject constructor( ) } - private fun showAddWalletBottomSheet() { - state.update { currentState -> - currentState.copy( - addWalletBottomSheet = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = ::dismissAddWalletBottomSheet, - content = createAddWalletBottomSheetContent(), - ), - ) + private fun onAddNewWalletClick() { + if (hotWalletFeatureToggles.isHotWalletEnabled) { + state.update { currentState -> + currentState.copy( + addWalletBottomSheet = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismissAddWalletBottomSheet, + content = createAddWalletBottomSheetContent(), + ), + ) + } + } else { + withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet(modelScope) + } } }