Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-06 14:24:03 +03:00
commit 38df746381
41 changed files with 392 additions and 102 deletions

View file

@ -500,4 +500,10 @@ internal object TokensDomainModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun provideGetAssetRequirementsUseCase(walletManagersFacade: WalletManagersFacade): GetAssetRequirementsUseCase {
return GetAssetRequirementsUseCase(walletManagersFacade)
}
}

View file

@ -223,5 +223,6 @@ sealed class AnalyticsParam {
const val NONCE = "Nonce"
const val STANDARD = "Standard"
const val NO_COLLECTION = "No collection"
const val EMULATION_STATUS = "Emulation Status"
}
}

View file

@ -41,12 +41,6 @@ internal class WcSignUseCaseDelegate<MiddleAction, SignModel>(
operator fun invoke(initModel: SignModel) = channelFlow {
val state = MutableStateFlow(WcSignState(initModel, WcSignStep.PreSign))
analytics.send(
WcAnalyticEvents.SignatureRequestReceived(
rawRequest = context.rawSdkRequest,
network = context.network,
),
)
state
.onEach { newState -> channel.send(newState) }

View file

@ -0,0 +1,21 @@
package com.tangem.domain.tokens
import arrow.core.Either
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
class GetAssetRequirementsUseCase(
private val walletManagersFacade: WalletManagersFacade,
) {
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<Throwable, AssetRequirementsCondition?> {
return Either.Companion.catch {
walletManagersFacade.getAssetRequirements(userWalletId, currency)
}
}
}

View file

@ -56,15 +56,24 @@ internal open class BaseActionsFactory(
*
* @param userWallet the user's cold wallet
* @param currency the cryptocurrency to check
* @param requirementsDeferred a deferred object containing the asset requirements condition
*/
protected suspend fun getOnrampUnavailabilityReason(
userWallet: UserWallet,
currency: CryptoCurrency,
requirementsDeferred: Deferred<AssetRequirementsCondition?>?,
): ScenarioUnavailabilityReason {
return rampStateManager.availableForBuy(
val onrampUnavailabilityReason = rampStateManager.availableForBuy(
userWallet = userWallet,
cryptoCurrency = currency,
)
val shouldCheckAssetRequirements =
onrampUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null
return if (shouldCheckAssetRequirements) {
getReceiveScenario(requirementsDeferred.await())
} else {
onrampUnavailabilityReason
}
}
/**

View file

@ -52,7 +52,11 @@ internal class CommonActionsFactory(
}
val onrampUnavailabilityReasonDeferred = async {
getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency)
getOnrampUnavailabilityReason(
userWallet = userWallet,
currency = cryptoCurrencyStatus.currency,
requirementsDeferred = requirementsDeferred,
)
}
val sendUnavailabilityReasonDeferred = async {

View file

@ -50,7 +50,11 @@ internal class OutdatedDataActionsFactory(
}
val onrampUnavailabilityReasonDeferred = async {
getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency)
getOnrampUnavailabilityReason(
userWallet = userWallet,
currency = cryptoCurrencyStatus.currency,
requirementsDeferred = requirementsDeferred,
)
}
val sendUnavailabilityReasonDeferred = if (sources.networkSource == StatusSource.ACTUAL) {

View file

@ -36,7 +36,11 @@ internal class UnreachableActionsFactory(
}
val onrampUnavailabilityReasonDeferred = async {
getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency)
getOnrampUnavailabilityReason(
userWallet = userWallet,
currency = cryptoCurrencyStatus.currency,
requirementsDeferred = requirementsDeferred,
)
}
// endregion

View file

@ -87,11 +87,11 @@ sealed class WcAnalyticEvents(
),
)
class SignatureRequestReceived(
class TransactionDetailsOpened(
rawRequest: WcSdkSessionRequest,
network: Network,
) : WcAnalyticEvents(
event = "Signature Request Received",
event = "Transaction Details Opened",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to rawRequest.dAppMetaData.name,
AnalyticsParam.Key.DAPP_URL to rawRequest.dAppMetaData.url,
@ -100,6 +100,27 @@ sealed class WcAnalyticEvents(
),
)
class SignatureRequestReceived(
rawRequest: WcSdkSessionRequest,
network: Network,
emulationStatus: EmulationStatus,
) : WcAnalyticEvents(
event = "Signature Request Received",
params = mapOf(
AnalyticsParam.Key.DAPP_NAME to rawRequest.dAppMetaData.name,
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,
),
) {
enum class EmulationStatus(val status: String) {
Emulated("Emulated"),
Error("Error"),
CanNotEmulate("Can't emulate"),
}
}
class SignatureRequestHandled(
rawRequest: WcSdkSessionRequest,
network: Network,

View file

@ -45,6 +45,7 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.settings)
implementation(projects.domain.transaction.models)
/** DI */
implementation(deps.hilt.android)

View file

@ -17,9 +17,11 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.tokens.GetAssetRequirementsUseCase
import com.tangem.domain.tokens.GetTokenListUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
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
@ -53,6 +55,7 @@ internal class OnrampTokenListModel @Inject constructor(
private val getWalletsUseCase: GetWalletsUseCase,
private val rampStateManager: RampStateManager,
private val getUserCountryUseCase: GetUserCountryUseCase,
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
) : Model() {
val state: StateFlow<TokenListUM> = tokenListUMController.state
@ -204,17 +207,28 @@ internal class OnrampTokenListModel @Inject constructor(
return coroutineScope {
map { status ->
async {
val isAvailable = checkAvailabilityByOperation(status = status)
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 isNotUnreachable = when (params.filterOperation) {
OnrampOperation.BUY -> true // unreachable state is available for Buy operation
OnrampOperation.SELL -> status.value !is CryptoCurrencyStatus.Unreachable
OnrampOperation.SWAP -> status.value !is CryptoCurrencyStatus.Unreachable
val isNotTrustlineRequired = requirements !is AssetRequirementsCondition.RequiredTrustline
val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable
val isAvailable = when (params.filterOperation) {
OnrampOperation.BUY -> {
isNotTrustlineRequired
} // unreachable state is available for Buy operation
OnrampOperation.SELL -> isNotUnreachable
OnrampOperation.SWAP -> {
isNotUnreachable && isNotTrustlineRequired
}
}
status to (isAvailable && isNotMissedDerivation && isNotLoading && isNotUnreachable)
status to (isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable)
}
}
.awaitAll()

View file

@ -13,11 +13,18 @@ import java.math.BigInteger
@Immutable
sealed class FeeSelectorUM {
data object Loading : FeeSelectorUM()
abstract val isPrimaryButtonEnabled: Boolean
data class Error(val error: GetFeeError) : FeeSelectorUM()
data object Loading : FeeSelectorUM() {
override val isPrimaryButtonEnabled = false
}
data class Error(val error: GetFeeError) : FeeSelectorUM() {
override val isPrimaryButtonEnabled = false
}
data class Content(
override val isPrimaryButtonEnabled: Boolean,
val fees: TransactionFee,
val feeItems: ImmutableList<FeeItem>,
val selectedFeeItem: FeeItem,

View file

@ -1,10 +1,12 @@
package com.tangem.features.send.v2.feeselector.model.transformers
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents
import com.tangem.utils.extensions.isZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toImmutableList
@ -30,7 +32,16 @@ internal class FeeSelectorCustomValueChangedTransformer(
fee = customFeeConverter.convertBack(updatedCustomValues),
customValues = updatedCustomValues,
)
val customFeeValue = updatedCustomValues.firstOrNull()
val isNotEmptyCustom = if (customFeeValue != null) {
!customFeeValue.value.parseToBigDecimal(customFeeValue.decimals).isZero()
} else {
false
}
return state.copy(
isPrimaryButtonEnabled = isNotEmptyCustom,
feeItems = state.feeItems.map { if (it is FeeItem.Custom) newCustomFee else it }.toImmutableList(),
selectedFeeItem = newCustomFee,
)

View file

@ -49,6 +49,7 @@ internal class FeeSelectorLoadedTransformer(
val nonce = ((prevState as? FeeSelectorUM.Content)?.feeNonce as? FeeNonce.Nonce)?.nonce
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = fees,
feeItems = feeItems,
selectedFeeItem = selectedFee,

View file

@ -203,6 +203,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
override val values: Sequence<FeeSelectorUM> = sequenceOf(
FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
feeItems = persistentListOf(lowFeeItem),
selectedFeeItem = lowFeeItem,
feeExtraInfo = FeeExtraInfo(
@ -218,6 +219,7 @@ private class FeeSelectorUMProvider : PreviewParameterProvider<FeeSelectorUM> {
fees = TransactionFee.Single(lowFeeItem.fee),
),
FeeSelectorUM.Content(
isPrimaryButtonEnabled = false,
feeItems = persistentListOf(maxFeeItem),
selectedFeeItem = maxFeeItem,
feeExtraInfo = FeeExtraInfo(

View file

@ -87,6 +87,7 @@ internal fun FeeSelectorModalBottomSheet(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
enabled = state.isPrimaryButtonEnabled,
text = stringResourceSafe(R.string.common_done),
onClick = feeSelectorIntents::onDoneClick,
)
@ -484,6 +485,7 @@ private fun FeeSelectorBS_Preview(
private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<FeeSelectorUM.Content>(
collection = listOf(
FeeSelectorUM.Content(
isPrimaryButtonEnabled = false,
feeItems = persistentListOf(
FeeItem.Suggested(
title = stringReference("Suggested by Tangem"),
@ -494,9 +496,6 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider<
FeeItem.Fast(fee = Fee.Common(Amount(value = BigDecimal("0.03"), blockchain = Blockchain.Ethereum))),
customFeeItem,
),
// selectedFeeItem = FeeItem.Market(
// amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum),
// ),
selectedFeeItem = customFeeItem,
feeExtraInfo = FeeExtraInfo(
isFeeApproximate = true,

View file

@ -19,6 +19,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS
import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty
import com.tangem.utils.extensions.isZero
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -91,7 +92,7 @@ internal class EthereumEIPCustomFeeConverter(
val mutableCustomValues = customValues.toMutableList()
return mutableCustomValues.apply {
when (index) {
FEE_AMOUNT -> setOnAmountChange(value, index)
FEE_AMOUNT -> setOnAmountChange(feeValue, value, index)
MAX_FEE -> setOnMaxFeeChange(value, index)
GAS_LIMIT -> setOnGasLimitChange(value, index)
else -> set(index, this[index].copy(value = value))
@ -99,8 +100,12 @@ internal class EthereumEIPCustomFeeConverter(
}.toImmutableList()
}
private fun MutableList<CustomFeeFieldUM>.setOnAmountChange(value: String, index: Int) {
val gasLimit = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
private fun MutableList<CustomFeeFieldUM>.setOnAmountChange(
feeValue: Fee.Ethereum.EIP1559,
value: String,
index: Int,
) {
val gasLimitRaw = this[GAS_LIMIT].value.parseToBigDecimal(this[GAS_LIMIT].decimals)
if (value.isBlank()) {
setEmpty(FEE_AMOUNT)
setEmpty(MAX_FEE)
@ -108,6 +113,27 @@ internal class EthereumEIPCustomFeeConverter(
val newFeeAmountDecimal = value.parseToBigDecimal(this[FEE_AMOUNT].decimals)
val newFeeAmount = newFeeAmountDecimal.movePointRight(GIGA_DECIMALS) // from ETH to GWEI
val gasLimit = if (gasLimitRaw.isZero()) {
val gasLimitTemp = feeValue.gasLimit.toBigDecimal()
set(
index = GAS_LIMIT,
element = this[GAS_LIMIT].copy(value = gasLimitTemp.parseBigDecimal(GIGA_DECIMALS)),
)
gasLimitTemp
} else {
gasLimitRaw
}
if (this[PRIORITY_FEE].value.isBlank()) {
set(
index = PRIORITY_FEE,
element = this[PRIORITY_FEE].copy(
value = feeValue.priorityFee.toBigDecimal().movePointLeft(GIGA_DECIMALS)
.parseBigDecimal(GIGA_DECIMALS),
),
)
}
val newMaxFee = newFeeAmount.divide(gasLimit, this[MAX_FEE].decimals, RoundingMode.HALF_UP)
set(

View file

@ -242,6 +242,7 @@ class SendConfirmationNotificationsTransformerV2Test {
)
val transactionFee = TransactionFee.Single(fee)
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(FeeItem.Market(fee)),
selectedFeeItem = FeeItem.Market(fee),
@ -282,6 +283,7 @@ class SendConfirmationNotificationsTransformerV2Test {
priority = priorityFee,
)
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
@ -364,6 +366,7 @@ class SendConfirmationNotificationsTransformerV2Test {
priority = fee,
)
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
@ -434,6 +437,7 @@ class SendConfirmationNotificationsTransformerV2Test {
priority = priorityFee,
)
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(

View file

@ -72,6 +72,7 @@ class TransformersComparisonTest {
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled)
assertThat(contentV1.notifications.size).isEqualTo(contentV2.notifications.size)
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled)
@ -117,6 +118,8 @@ class TransformersComparisonTest {
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.isPrimaryButtonEnabled).isTrue()
assertThat(contentV2.isPrimaryButtonEnabled).isTrue()
assertThat(contentV1.notifications).hasSize(1)
assertThat(contentV2.notifications).hasSize(1)
assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
@ -165,6 +168,8 @@ class TransformersComparisonTest {
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.isPrimaryButtonEnabled).isTrue()
assertThat(contentV2.isPrimaryButtonEnabled).isTrue()
assertThat(contentV1.notifications).hasSize(1)
assertThat(contentV2.notifications).hasSize(1)
assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
@ -215,6 +220,8 @@ class TransformersComparisonTest {
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.isPrimaryButtonEnabled).isTrue()
assertThat(contentV2.isPrimaryButtonEnabled).isTrue()
assertThat(contentV1.notifications).hasSize(2)
assertThat(contentV2.notifications).hasSize(2)
@ -268,6 +275,7 @@ class TransformersComparisonTest {
assertThat(contentV1.notifications).isEmpty()
assertThat(contentV2.notifications).isEmpty()
assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled)
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
}
@ -377,6 +385,7 @@ class TransformersComparisonTest {
)
val transactionFee = TransactionFee.Single(fee)
return FeeSelectorUMV2.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Market(fee),
@ -470,6 +479,7 @@ class TransformersComparisonTest {
priority = fee,
)
return FeeSelectorUMV2.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
@ -605,6 +615,7 @@ class TransformersComparisonTest {
priority = priorityFee,
)
return FeeSelectorUMV2.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
@ -740,6 +751,7 @@ class TransformersComparisonTest {
priority = priorityFee,
)
return FeeSelectorUMV2.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
@ -839,6 +851,7 @@ class TransformersComparisonTest {
)
val transactionFee = TransactionFee.Single(fee)
return FeeSelectorUMV2.Content(
isPrimaryButtonEnabled = true,
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Market(fee),

View file

@ -379,6 +379,7 @@ private fun SendWithSwapSuccessContent_Preview() {
),
feeFiatRateUM = null,
feeNonce = FeeNonce.None,
isPrimaryButtonEnabled = false,
),
confirmUM = ConfirmUM.Success(
isPrimaryButtonEnabled = true,

View file

@ -41,6 +41,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.transaction.models)
/** Data */
implementation(projects.data.common)

View file

@ -26,10 +26,11 @@ import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
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.transaction.models.AssetRequirementsCondition
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.feature.swap.converters.*
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.feature.swap.domain.models.ExpressDataError
@ -134,7 +135,12 @@ internal class DefaultSwapRepository(
contractAddress = initialCurrency.contractAddress,
network = initialCurrency.network,
)
val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) }
val currenciesList = currencyList
.filter {
val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, it)
requirements !is AssetRequirementsCondition.RequiredTrustline
}
.map { leastTokenInfoConverter.convert(it) }
val pairsDeferred = async {
getPairsInternal(

View file

@ -31,6 +31,7 @@ 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
@ -79,6 +80,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private val getEthSpecificFeeUseCase: GetEthSpecificFeeUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase,
private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase,
private val amountFormatter: AmountFormatter,
@Assisted private val userWalletId: UserWalletId,
) : SwapInteractor {
@ -139,7 +141,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
private fun getToCurrenciesGroup(
private suspend fun getToCurrenciesGroup(
currency: CryptoCurrency,
leastPairs: List<SwapPairLeast>,
cryptoCurrenciesList: List<CryptoCurrencyStatus>,
@ -171,15 +173,18 @@ internal class SwapInteractorImpl @AssistedInject constructor(
)
}
private fun findProvidersForPair(
private suspend fun findProvidersForPair(
cryptoCurrencyStatuses: CryptoCurrencyStatus,
swapPairsLeastList: List<SwapPairLeast>,
tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo,
): List<SwapProvider>? {
val requirements = getAssetRequirementsUseCase.invoke(userWalletId, cryptoCurrencyStatuses.currency).getOrNull()
return swapPairsLeastList.firstNotNullOfOrNull {
val listTokenInfo = tokenInfoForAvailable(it)
if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network &&
cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress
cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress &&
requirements !is AssetRequirementsCondition.RequiredTrustline
) {
it.providers
} else {

View file

@ -32,12 +32,13 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.network.Network
@ -145,8 +146,11 @@ private fun MissingRequiredBlock(missingNetworks: ImmutableList<WcNetworkInfoIte
val missingNetworksName = missingNetworks.joinToString { it.name }
val notificationUM = remember(missingNetworks) {
NotificationUM.Info(
title = stringReference("The wallet has no required networks"),
subtitle = stringReference("Add the $missingNetworksName network to your portfolio for this wallet."),
title = resourceReference(R.string.wc_missing_required_network_title),
subtitle = resourceReference(
R.string.wc_missing_required_network_description,
wrappedList(missingNetworksName),
),
)
}
Column(modifier = modifier) {
@ -288,7 +292,7 @@ private fun NetworkNameAndSymbol(name: String, symbol: String, modifier: Modifie
}
}
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WcSelectNetworksContent_Preview(
@ -296,29 +300,7 @@ private fun WcSelectNetworksContent_Preview(
state: WcSelectNetworksUM,
) {
TangemThemePreview {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
containerColor = TangemTheme.colors.background.tertiary,
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContent.Empty,
),
title = {
TangemModalBottomSheetTitle(
title = stringReference("Choose wallet"),
onEndClick = {},
endIconRes = R.drawable.ic_close_24,
)
},
content = {
WcSelectNetworksContent(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(horizontal = TangemTheme.dimens.spacing16),
state = state,
)
},
)
WcSelectNetworksBS(state = state, onBack = {}, onDismiss = {})
}
}
@ -328,7 +310,7 @@ private class WcSelectNetworksProvider : CollectionPreviewParameterProvider<WcSe
missing = persistentListOf(
WcNetworkInfoItem.Required(
id = "ethereum",
name = "EthereumEthereumEthereumEthereumEthereumEthereumEthereumEthereum",
name = "Ethereum",
symbol = "ETH",
icon = R.drawable.ic_solana_16,
),
@ -380,6 +362,7 @@ private class WcSelectNetworksProvider : CollectionPreviewParameterProvider<WcSe
),
),
onDone = {},
doneButtonEnabled = false,
),
WcSelectNetworksUM(
missing = persistentListOf(),
@ -424,6 +407,7 @@ private class WcSelectNetworksProvider : CollectionPreviewParameterProvider<WcSe
),
),
onDone = {},
doneButtonEnabled = true,
),
),
)

View file

@ -57,6 +57,7 @@ sealed class WcAppInfoSecurityNotification(
@Immutable
internal sealed class WcNetworksInfo {
data object NoneNetworksAdded : WcNetworksInfo()
data class MissingRequiredNetworkInfo(val networks: String) : WcNetworksInfo()
data class ContainsAllRequiredNetworks(val items: ImmutableList<WcNetworkInfoItem>) : WcNetworksInfo()
}

View file

@ -8,5 +8,5 @@ internal data class WcSelectNetworksUM(
val available: ImmutableList<WcNetworkInfoItem.Checkable>,
val notAdded: ImmutableList<WcNetworkInfoItem.ReadOnly>,
val onDone: () -> Unit,
val doneButtonEnabled: Boolean = true,
val doneButtonEnabled: Boolean,
)

View file

@ -1,7 +1,9 @@
package com.tangem.features.walletconnect.connections.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
@ -10,18 +12,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.ui.extensions.stringReference
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.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcPairError
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
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.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.*
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.walletconnect.connections.components.WcPairComponent
import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent
@ -91,7 +91,9 @@ internal class WcPairModel @Inject constructor(
appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = false),
)
pairState.result.onLeft(::processError)
pairState.result
.onLeft(::processError)
.onRight(::processSuccessfullyConnected)
router.pop()
}
is WcPairState.Error -> {
@ -196,6 +198,13 @@ internal class WcPairModel @Inject constructor(
stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.Verified(appName)))
}
private fun processSuccessfullyConnected(session: WcSession) {
// TODO: [REDACTED_JIRA] localization
messageSender.send(
SnackbarMessage(message = stringReference("Connected to ${session.sdkModel.appMetaData.name}")),
)
}
private fun processError(error: WcPairError) {
val alert = when (error) {
is WcPairError.UnsupportedDApp -> {
@ -236,6 +245,8 @@ internal class WcPairModel @Inject constructor(
WcNetworksSelectedTransformer(
missingNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
notAddedNetworks = proposalNetwork.notAdded,
additionallyEnabledNetworks = additionallyEnabledNetworks,
),
)

View file

@ -60,6 +60,9 @@ internal class WcSelectNetworksModel @Inject constructor(
private fun getInitialState(): WcSelectNetworksUM {
val editEnabled = params.missingRequiredNetworks.isEmpty()
val doneButtonEnabled = params.missingRequiredNetworks.isEmpty() &&
(params.requiredNetworks.isNotEmpty() || params.availableNetworks.isNotEmpty())
return WcSelectNetworksUM(
missing = params.missingRequiredNetworks.map { network ->
WcNetworkInfoItem.Required(
@ -98,7 +101,7 @@ internal class WcSelectNetworksModel @Inject constructor(
)
}.toImmutableList(),
onDone = ::onDone,
doneButtonEnabled = editEnabled,
doneButtonEnabled = doneButtonEnabled,
)
}
}

View file

@ -2,8 +2,8 @@ package com.tangem.features.walletconnect.connections.model.transformers
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig
@ -37,13 +37,22 @@ internal class WcAppInfoTransformer(
value = WcNetworksInfoConverter.Input(
missingNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
notAddedNetworks = proposalNetwork.notAdded,
additionallyEnabledNetworks = additionallyEnabledNetworks,
),
),
onNetworksClick = onNetworksClick,
connectButtonConfig = WcPrimaryButtonConfig(
showProgress = false,
enabled = proposalNetwork.missingRequired.isEmpty(),
enabled = WcConnectButtonAvailabilityConverter.convert(
WcConnectButtonAvailabilityConverter.Input(
missingNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
selectedNetworks = additionallyEnabledNetworks,
),
),
onClick = { onConnect(dAppSession.securityStatus) },
),
onDismiss = onDismiss,

View file

@ -1,8 +1,8 @@
package com.tangem.features.walletconnect.connections.model.transformers
import com.tangem.domain.models.network.Network
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
import com.tangem.utils.transformer.Transformer
@ -19,11 +19,20 @@ internal class WcAppInfoWalletChangedTransformer(
WcNetworksInfoConverter.Input(
missingNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
notAddedNetworks = proposalNetwork.notAdded,
additionallyEnabledNetworks = additionallyEnabledNetworks,
),
),
connectButtonConfig = prevState.connectButtonConfig.copy(
enabled = proposalNetwork.missingRequired.isEmpty(),
enabled = WcConnectButtonAvailabilityConverter.convert(
WcConnectButtonAvailabilityConverter.Input(
missingNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
selectedNetworks = additionallyEnabledNetworks,
),
),
),
)
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.walletconnect.connections.model.transformers
import com.tangem.domain.models.network.Network
import com.tangem.utils.converter.Converter
@Suppress("DestructuringDeclarationWithTooManyEntries")
internal object WcConnectButtonAvailabilityConverter : Converter<WcConnectButtonAvailabilityConverter.Input, Boolean> {
override fun convert(value: Input): Boolean {
val missing = value.missingNetworks
val required = value.requiredNetworks
val available = value.availableNetworks
val selected = value.selectedNetworks
return missing.isEmpty() && (required.isNotEmpty() || available.isNotEmpty() || selected.isNotEmpty())
}
data class Input(
val missingNetworks: Set<Network>,
val requiredNetworks: Set<Network>,
val availableNetworks: Set<Network>,
val selectedNetworks: Set<Network>,
)
}

View file

@ -7,31 +7,38 @@ import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@Suppress("DestructuringDeclarationWithTooManyEntries")
internal object WcNetworksInfoConverter : Converter<WcNetworksInfoConverter.Input, WcNetworksInfo> {
override fun convert(value: Input): WcNetworksInfo {
return if (value.missingNetworks.isNotEmpty()) {
WcNetworksInfo.MissingRequiredNetworkInfo(
networks = value.missingNetworks.joinToString { it.name },
)
} else {
WcNetworksInfo.ContainsAllRequiredNetworks(
items = (value.requiredNetworks + value.additionallyEnabledNetworks)
.map {
val missing = value.missingNetworks
val required = value.requiredNetworks
val available = value.availableNetworks
val notAdded = value.notAddedNetworks
val additionallyEnabled = value.additionallyEnabledNetworks
return when {
missing.isNotEmpty() -> WcNetworksInfo.MissingRequiredNetworkInfo(missing.joinToString { it.name })
required.isEmpty() && available.isEmpty() && notAdded.isNotEmpty() -> WcNetworksInfo.NoneNetworksAdded
else -> {
val combinedNetworks = required + additionallyEnabled
WcNetworksInfo.ContainsAllRequiredNetworks(
items = combinedNetworks.map { network ->
WcNetworkInfoItem.Required(
id = it.rawId,
icon = it.iconResId,
name = it.name,
symbol = it.currencySymbol,
id = network.rawId,
icon = network.iconResId,
name = network.name,
symbol = network.currencySymbol,
)
}
.toImmutableList(),
)
}.toImmutableList(),
)
}
}
}
data class Input(
val missingNetworks: Set<Network>,
val requiredNetworks: Set<Network>,
val availableNetworks: Set<Network>,
val notAddedNetworks: Set<Network>,
val additionallyEnabledNetworks: Set<Network>,
)
}

View file

@ -7,19 +7,29 @@ import com.tangem.utils.transformer.Transformer
internal class WcNetworksSelectedTransformer(
private val missingNetworks: Set<Network>,
private val requiredNetworks: Set<Network>,
private val availableNetworks: Set<Network>,
private val notAddedNetworks: Set<Network>,
private val additionallyEnabledNetworks: Set<Network>,
) : Transformer<WcAppInfoUM> {
override fun transform(prevState: WcAppInfoUM): WcAppInfoUM {
val contentState = prevState as? WcAppInfoUM.Content ?: return prevState
return contentState.copy(
connectButtonConfig = contentState.connectButtonConfig.copy(
enabled = missingNetworks.isEmpty() &&
(requiredNetworks.isNotEmpty() || additionallyEnabledNetworks.isNotEmpty()),
enabled = WcConnectButtonAvailabilityConverter.convert(
WcConnectButtonAvailabilityConverter.Input(
missingNetworks = missingNetworks,
requiredNetworks = requiredNetworks,
availableNetworks = availableNetworks,
selectedNetworks = additionallyEnabledNetworks,
),
),
),
networksInfo = WcNetworksInfoConverter.convert(
value = WcNetworksInfoConverter.Input(
missingNetworks = missingNetworks,
requiredNetworks = requiredNetworks,
availableNetworks = availableNetworks,
notAddedNetworks = notAddedNetworks,
additionallyEnabledNetworks = additionallyEnabledNetworks,
),
),

View file

@ -62,6 +62,8 @@ internal fun WcAppInfoItem(
text = title,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (verifiedDAppState is VerifiedDAppState.Verified) {
Icon(

View file

@ -35,10 +35,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.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -284,6 +281,19 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier
)
when (state.networksInfo) {
is WcNetworksInfo.ContainsAllRequiredNetworks -> Unit
is WcNetworksInfo.NoneNetworksAdded -> {
HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary)
Notification(
config = NotificationConfig(
iconResId = R.drawable.ic_alert_circle_24,
// TODO: [REDACTED_JIRA] localization
title = stringReference("Specify selected networks"),
subtitle = stringReference("At least one network is required for dApp connection"),
),
iconTint = TangemTheme.colors.icon.attention,
containerColor = TangemTheme.colors.background.action,
)
}
is WcNetworksInfo.MissingRequiredNetworkInfo -> {
HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary)
Notification(
@ -370,6 +380,7 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier
when (networksInfo) {
is WcNetworksInfo.ContainsAllRequiredNetworks -> NetworkIcons(items = networksInfo.items)
is WcNetworksInfo.MissingRequiredNetworkInfo -> Unit
is WcNetworksInfo.NoneNetworksAdded -> Unit
}
Icon(
modifier = Modifier.size(width = 18.dp, height = 24.dp),

View file

@ -193,6 +193,7 @@ private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Mod
private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AsyncImage(
@ -213,9 +214,12 @@ private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifi
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f, fill = false),
text = appInfo.name,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (appInfo.verifiedState is VerifiedDAppState.Verified) {
Icon(

View file

@ -41,7 +41,7 @@ internal object WcConnectionsPreviewData {
walletName = "Bitcoin",
connectedApps = persistentListOf(
WcConnectedAppInfo(
name = "React app",
name = "React app React app React app React app React app",
iconUrl = "$BASE_URL/LiteCoin.png",
subtitle = "https://react-app.walletconnect.com/",
verifiedState = VerifiedDAppState.Verified {},

View file

@ -3,12 +3,14 @@ package com.tangem.features.walletconnect.transaction.model
import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pushNew
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.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase
import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams
@ -24,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@Stable
@ -34,6 +37,7 @@ internal class WcAddNetworkModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val clipboardManager: ClipboardManager,
private val analytics: AnalyticsEventHandler,
private val useCaseFactory: WcRequestUseCaseFactory,
private val wcAddEthereumChainUMConverter: WcAddEthereumChainUMConverter,
) : Model(), WcCommonTransactionModel {
@ -44,10 +48,11 @@ internal class WcAddNetworkModel @Inject constructor(
val stackNavigation = StackNavigation<WcTransactionRoutes>()
private val params = paramsContainer.require<WcTransactionModelParams>()
private var useCase by Delegates.notNull<WcAddNetworkUseCase>()
init {
modelScope.launch {
val useCase: WcAddNetworkUseCase = useCaseFactory.createUseCase<WcAddNetworkUseCase>(params.rawRequest)
useCase = useCaseFactory.createUseCase<WcAddNetworkUseCase>(params.rawRequest)
.onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) }
.getOrNull() ?: return@launch
_uiState.emit(
@ -75,6 +80,12 @@ internal class WcAddNetworkModel @Inject constructor(
}
fun showTransactionRequest() {
analytics.send(
WcAnalyticEvents.TransactionDetailsOpened(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
),
)
stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo)
}

View file

@ -5,6 +5,7 @@ import arrow.core.Either
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.ValidationResult
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
@ -27,6 +28,7 @@ import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcAnalyticEvents.SignatureRequestReceived.EmulationStatus
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcRequestError
import com.tangem.domain.walletconnect.model.WcRequestError.Companion.message
@ -88,6 +90,7 @@ internal class WcSendTransactionModel @Inject constructor(
private var wcApproval: WcApproval? = null
private var sign: () -> Unit = {}
private val feeReloadState = MutableStateFlow(false)
private val signatureReceivedAnalyticsSendState = MutableStateFlow(false)
init {
@Suppress("UnusedPrivateMember")
@ -121,6 +124,8 @@ internal class WcSendTransactionModel @Inject constructor(
.collectLatest { (signState, securityCheck) ->
if (signingIsDone(signState, useCase)) return@collectLatest
sendSignatureReceivedAnalytics(useCase, securityCheck)
this@WcSendTransactionModel.signState = signState
val isSecurityCheckContent = securityCheck is Lce.Content
val isApprovalMethod = isSecurityCheckContent &&
@ -257,6 +262,12 @@ internal class WcSendTransactionModel @Inject constructor(
}
fun showTransactionRequest() {
analytics.send(
WcAnalyticEvents.TransactionDetailsOpened(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
),
)
stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo)
}
@ -358,4 +369,30 @@ internal class WcSendTransactionModel @Inject constructor(
private fun copyData(text: String) {
clipboardManager.setText(text = text, isSensitive = true)
}
private fun sendSignatureReceivedAnalytics(
useCase: WcSignUseCase<*>,
securityCheck: Lce<Throwable, BlockAidTransactionCheck.Result>,
) {
if (signatureReceivedAnalyticsSendState.value) return
val emulationStatus = when (securityCheck) {
is Lce.Content -> when (securityCheck.content.result.simulation) {
SimulationResult.FailedToSimulate -> EmulationStatus.CanNotEmulate
is SimulationResult.Success -> EmulationStatus.Emulated
}
is Lce.Error -> EmulationStatus.Error
is Lce.Loading -> return
}
analytics.send(
WcAnalyticEvents.SignatureRequestReceived(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
emulationStatus = emulationStatus,
),
)
signatureReceivedAnalyticsSendState.value = true
}
}

View file

@ -4,12 +4,14 @@ import androidx.compose.runtime.Stable
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
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.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod
@ -32,6 +34,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
@Suppress("LongParameterList")
@Stable
@ -42,6 +45,7 @@ internal class WcSignTransactionModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val clipboardManager: ClipboardManager,
private val analytics: AnalyticsEventHandler,
private val useCaseFactory: WcRequestUseCaseFactory,
private val signTypedDataUMConverter: WcSignTypedDataUMConverter,
private val signTransactionUMConverter: WcSignTransactionUMConverter,
@ -54,9 +58,11 @@ internal class WcSignTransactionModel @Inject constructor(
val stackNavigation = StackNavigation<WcTransactionRoutes>()
private var useCase by Delegates.notNull<WcMessageSignUseCase>()
init {
modelScope.launch {
val useCase = useCaseFactory.createUseCase<WcMessageSignUseCase>(params.rawRequest)
useCase = useCaseFactory.createUseCase<WcMessageSignUseCase>(params.rawRequest)
.onLeft { router.push(WcHandleMethodErrorConverter.convert(it)) }
.getOrNull() ?: return@launch
useCase.invoke()
@ -109,6 +115,12 @@ internal class WcSignTransactionModel @Inject constructor(
}
fun showTransactionRequest() {
analytics.send(
WcAnalyticEvents.TransactionDetailsOpened(
rawRequest = useCase.rawSdkRequest,
network = useCase.network,
),
)
stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo)
}

View file

@ -185,9 +185,9 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
onDismiss = {},
onSend = {},
appInfo = WcTransactionAppInfoContentUM(
appName = "React App",
appName = "React App React App ReactApp ReactApp ReactApp",
appIcon = "",
verifiedState = VerifiedDAppState.Verified {},
verifiedState = VerifiedDAppState.Unknown,
appSubtitle = "react-app.walletconnect.com",
),
estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM(
@ -225,7 +225,7 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide
onDismiss = {},
onSend = {},
appInfo = WcTransactionAppInfoContentUM(
appName = "React App",
appName = "React App React App React App React App React App",
appIcon = "",
verifiedState = VerifiedDAppState.Verified {},
appSubtitle = "react-app.walletconnect.com",