Updated on 2026-08-14

This commit is contained in:
Tangem 2025-06-17 12:17:29 +04:00
parent 717b8e2df2
commit 422e406d15
21 changed files with 258 additions and 112 deletions

View file

@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass
data class ValidationResponse( data class ValidationResponse(
@Json(name = "status") val status: String, @Json(name = "status") val status: String,
@Json(name = "result_type") val resultType: String, @Json(name = "result_type") val resultType: String,
@Json(name = "description") val description: String?,
) )

View file

@ -39,6 +39,7 @@ internal object BlockAidMapper {
} else { } else {
mapSimulationSuccessResult(from.simulation.accountSummary) mapSimulationSuccessResult(from.simulation.accountSummary)
}, },
description = from.validation.description,
) )
} }

View file

@ -46,7 +46,7 @@ class BlockAidMapperTest {
spenders = mapOf("spender" to spenderDetails), spenders = mapOf("spender" to spenderDetails),
) )
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign"), validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""),
simulation = SimulationResponse( simulation = SimulationResponse(
status = "Success", status = "Success",
accountSummary = AccountSummaryResponse( accountSummary = AccountSummaryResponse(
@ -79,7 +79,7 @@ class BlockAidMapperTest {
outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")), outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")),
) )
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign"), validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""),
simulation = SimulationResponse( simulation = SimulationResponse(
status = "Success", status = "Success",
accountSummary = AccountSummaryResponse( accountSummary = AccountSummaryResponse(
@ -105,7 +105,7 @@ class BlockAidMapperTest {
@Test @Test
fun `when response error validation rhen returns failed to validate`() { fun `when response error validation rhen returns failed to validate`() {
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Error", resultType = "Benign"), validation = ValidationResponse(status = "Error", resultType = "Benign", description = ""),
simulation = SimulationResponse( simulation = SimulationResponse(
status = "Success", status = "Success",
accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null),
@ -120,7 +120,7 @@ class BlockAidMapperTest {
@Test @Test
fun `when response not benign then returns validation unsafe`() { fun `when response not benign then returns validation unsafe`() {
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Phishing"), validation = ValidationResponse(status = "Success", resultType = "Phishing", description = ""),
simulation = SimulationResponse( simulation = SimulationResponse(
status = "Success", status = "Success",
accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null),
@ -134,7 +134,7 @@ class BlockAidMapperTest {
@Test @Test
fun `when response simulation not success then returns simulation failed ro simulate`() { fun `when response simulation not success then returns simulation failed ro simulate`() {
val response = TransactionScanResponse( val response = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign"), validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""),
simulation = SimulationResponse( simulation = SimulationResponse(
status = "Error", status = "Error",
accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null),
@ -148,7 +148,7 @@ class BlockAidMapperTest {
@Test @Test
fun `when response simulation is empty then returns failed to simulate`() { fun `when response simulation is empty then returns failed to simulate`() {
val txResponse = TransactionScanResponse( val txResponse = TransactionScanResponse(
validation = ValidationResponse(status = "Success", resultType = "Benign"), validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""),
simulation = SimulationResponse( simulation = SimulationResponse(
status = "Success", status = "Success",
accountSummary = AccountSummaryResponse( accountSummary = AccountSummaryResponse(

View file

@ -119,7 +119,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor(
emitAll(delegate.invoke(transactionData)) emitAll(delegate.invoke(transactionData))
} }
override suspend fun getAmount(): Amount? { override fun getAmount(): Amount? {
return approvalAmount return approvalAmount
} }

View file

@ -127,7 +127,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor(
return dAppFee return dAppFee
} }
override suspend fun getAmount(): Amount? { override fun getAmount(): Amount? {
return approvalAmount return approvalAmount
} }

View file

@ -8,5 +8,6 @@ package com.domain.blockaid.models.transaction
*/ */
data class CheckTransactionResult( data class CheckTransactionResult(
val validation: ValidationResult, val validation: ValidationResult,
val description: String? = null,
val simulation: SimulationResult, val simulation: SimulationResult,
) )

View file

@ -44,6 +44,6 @@ interface WcMutableFee {
* [updateAmount] triggered a new [TransactionData] emit * [updateAmount] triggered a new [TransactionData] emit
*/ */
interface WcApproval { interface WcApproval {
suspend fun getAmount(): Amount? fun getAmount(): Amount?
fun updateAmount(amount: Amount?) fun updateAmount(amount: Amount?)
} }

View file

@ -60,6 +60,7 @@ internal abstract class WcCommonTransactionComponentDelegate(
is WcTransactionRoutes.Transaction, is WcTransactionRoutes.Transaction,
is WcTransactionRoutes.TransactionRequestInfo, is WcTransactionRoutes.TransactionRequestInfo,
is WcTransactionRoutes.Alert, is WcTransactionRoutes.Alert,
is WcTransactionRoutes.CustomAllowance,
-> stackNavigation?.pop() -> stackNavigation?.pop()
else -> Unit else -> Unit
} }

View file

@ -4,7 +4,9 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 import com.tangem.features.walletconnect.connections.components.AlertsComponentV2
import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory.createCommonTransactionAppInfoAlertUM import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory.createCommonTransactionAppInfoAlertUM
import com.tangem.features.walletconnect.transaction.components.send.WcCustomAllowanceComponent
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel
import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel
import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes
internal fun getWcCommonScreen( internal fun getWcCommonScreen(
@ -23,5 +25,12 @@ internal fun getWcCommonScreen(
appComponentContext = appComponentContext, appComponentContext = appComponentContext,
model = model, model = model,
) )
is WcTransactionRoutes.CustomAllowance -> WcCustomAllowanceComponent(
appComponentContext = appComponentContext,
model = model,
onClickDoneCustomAllowance = { value, isUnlimited ->
(model as? WcSendTransactionModel)?.onClickDoneCustomAllowance(value, isUnlimited)
},
)
} }
} }

View file

@ -0,0 +1,39 @@
package com.tangem.features.walletconnect.transaction.components.send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel
import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM
import com.tangem.features.walletconnect.transaction.ui.approve.WcCustomAllowanceContent
import java.math.BigDecimal
internal class WcCustomAllowanceComponent(
private val appComponentContext: AppComponentContext,
private val model: WcCommonTransactionModel,
private val onClickDoneCustomAllowance: (BigDecimal, Boolean) -> Unit,
) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent {
override fun dismiss() {
model.dismiss()
}
@Composable
override fun BottomSheet() {
val content by model.uiState.collectAsStateWithLifecycle()
val spendAllowance = (content as? WcSendTransactionUM)?.spendAllowance
if (spendAllowance != null) {
WcCustomAllowanceContent(
state = spendAllowance,
onClickDone = { value, isUnlimited ->
router.pop()
onClickDoneCustomAllowance(value, isUnlimited)
},
onBack = router::pop,
onDismiss = ::dismiss,
)
}
}
}

View file

@ -27,6 +27,7 @@ internal class WcSendTransactionComponent(
onClickTransactionRequest = model::showTransactionRequest, onClickTransactionRequest = model::showTransactionRequest,
onBack = router::pop, onBack = router::pop,
onDismiss = ::dismiss, onDismiss = ::dismiss,
onClickAllowToSpend = model::onClickAllowToSpend,
) )
} }
} }

View file

@ -1,9 +1,14 @@
package com.tangem.features.walletconnect.transaction.entity.approve package com.tangem.features.walletconnect.transaction.entity.approve
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import java.math.BigDecimal
internal data class WcSpendAllowanceUM( internal data class WcSpendAllowanceUM(
val amountValue: BigDecimal,
val isUnlimited: Boolean,
val amountText: TextReference, val amountText: TextReference,
val tokenSymbol: String, val tokenSymbol: String,
val tokenImageUrl: String?, val tokenImageUrl: String?,
) val networkIconRes: Int?,
) : TangemBottomSheetConfigContent

View file

@ -1,6 +1,7 @@
package com.tangem.features.walletconnect.transaction.entity.send package com.tangem.features.walletconnect.transaction.entity.send
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM
import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM
import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM
import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM
@ -10,6 +11,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction
internal data class WcSendTransactionUM( internal data class WcSendTransactionUM(
val transaction: WcSendTransactionItemUM, val transaction: WcSendTransactionItemUM,
override val transactionRequestInfo: WcTransactionRequestInfoUM, override val transactionRequestInfo: WcTransactionRequestInfoUM,
val spendAllowance: WcSpendAllowanceUM? = null,
) : WcCommonTransactionUM ) : WcCommonTransactionUM
internal data class WcSendTransactionItemUM( internal data class WcSendTransactionItemUM(

View file

@ -24,6 +24,7 @@ import com.tangem.features.walletconnect.transaction.ui.blockaid.WcSendAndReceiv
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal
import javax.inject.Inject import javax.inject.Inject
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -46,6 +47,8 @@ internal class WcSendTransactionModel @Inject constructor(
val stackNavigation = StackNavigation<WcTransactionRoutes>() val stackNavigation = StackNavigation<WcTransactionRoutes>()
private var wcApproval: WcApproval? = null
init { init {
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
modelScope.launch { modelScope.launch {
@ -77,6 +80,7 @@ internal class WcSendTransactionModel @Inject constructor(
val isSecurityCheckContent = securityCheck is Lce.Content val isSecurityCheckContent = securityCheck is Lce.Content
var isApprovalMethod = isSecurityCheckContent && var isApprovalMethod = isSecurityCheckContent &&
securityCheck.content is BlockAidTransactionCheck.Result.Approval securityCheck.content is BlockAidTransactionCheck.Result.Approval
wcApproval = useCase as? WcApproval
buildUiState(securityCheck, useCase, signState) buildUiState(securityCheck, useCase, signState)
} }
else -> unknownMethodRunnable() else -> unknownMethodRunnable()
@ -90,7 +94,9 @@ internal class WcSendTransactionModel @Inject constructor(
signState: WcSignState<*>, signState: WcSignState<*>,
) { ) {
val blockAidState = when (securityCheck) { val blockAidState = when (securityCheck) {
is Lce.Content -> blockAidUiConverter.convert(securityCheck.content.result) is Lce.Content -> blockAidUiConverter.convert(
WcSendAndReceiveBlockAidUiConverter.Input(securityCheck.content.result, wcApproval?.getAmount()),
)
is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false) is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false)
is Lce.Loading -> WcSendReceiveTransactionCheckResultsUM(isLoading = true) is Lce.Loading -> WcSendReceiveTransactionCheckResultsUM(isLoading = true)
} }
@ -108,6 +114,7 @@ internal class WcSendTransactionModel @Inject constructor(
) as? WcSendTransactionUM ) as? WcSendTransactionUM
transactionUM = transactionUM?.copy( transactionUM = transactionUM?.copy(
transaction = transactionUM.transaction.copy(estimatedWalletChanges = blockAidState), transaction = transactionUM.transaction.copy(estimatedWalletChanges = blockAidState),
spendAllowance = blockAidState.spendAllowance,
) )
_uiState.emit(transactionUM) _uiState.emit(transactionUM)
} }
@ -120,6 +127,17 @@ internal class WcSendTransactionModel @Inject constructor(
stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo)
} }
fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) {
val maxValue = if (isUnlimited) Double.MAX_VALUE.toBigDecimal() else value
wcApproval?.getAmount()?.let { currentAmount ->
wcApproval?.updateAmount(currentAmount.copy(maxValue = maxValue))
}
}
fun onClickAllowToSpend() {
wcApproval?.let { stackNavigation.pushNew(WcTransactionRoutes.CustomAllowance) }
}
private fun showVerifiedAlert(appName: String) { private fun showVerifiedAlert(appName: String) {
stackNavigation.pushNew(WcTransactionRoutes.Alert(WcTransactionRoutes.Alert.Type.Verified(appName))) stackNavigation.pushNew(WcTransactionRoutes.Alert(WcTransactionRoutes.Alert.Type.Verified(appName)))
} }

View file

@ -14,6 +14,9 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout
@Serializable @Serializable
data object TransactionRequestInfo : WcTransactionRoutes() data object TransactionRequestInfo : WcTransactionRoutes()
@Serializable
data object CustomAllowance : WcTransactionRoutes()
@Serializable @Serializable
data class Alert(val type: Type) : WcTransactionRoutes() { data class Alert(val type: Type) : WcTransactionRoutes() {
@Serializable @Serializable

View file

@ -12,7 +12,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -23,91 +23,126 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig 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.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.approve.WcCustomAllowanceUM import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM
import java.math.BigDecimal
@Composable @Composable
internal fun WcCustomAllowanceContent( internal fun WcCustomAllowanceContent(
allowance: WcCustomAllowanceUM, state: WcSpendAllowanceUM,
onAmountChange: (String) -> Unit, onClickDone: (BigDecimal, Boolean) -> Unit,
onToggleChange: (Boolean) -> Unit, onBack: () -> Unit,
onClickDone: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
Column( var amountText by remember { mutableStateOf(state.amountValue.toPlainString()) }
modifier = modifier var isUnlimited by remember { mutableStateOf(state.isUnlimited) }
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth()
.padding(TangemTheme.dimens.size16),
) {
AmountTextField(
allowance = allowance,
onAmountChange = onAmountChange,
)
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) val amount: BigDecimal by remember(amountText) {
mutableStateOf(amountText.toBigDecimalOrNull() ?: state.amountValue)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.action)
.padding(TangemTheme.dimens.spacing16)
.fillMaxWidth(),
) {
Text(
text = stringResourceSafe(R.string.wc_unlimited_amount),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.weight(1f),
)
TangemSwitch(
checked = allowance.isUnlimited,
onCheckedChange = onToggleChange,
)
}
Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing60))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_done),
onClick = onClickDone,
)
} }
TangemModalBottomSheetWithFooter<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
containerColor = TangemTheme.colors.background.tertiary,
onBack = onBack,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.wc_custom_allowance_title),
startIconRes = R.drawable.ic_back_24,
onStartClick = onBack,
)
},
content = {
Column(
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.fillMaxWidth()
.padding(16.dp),
) {
AmountTextField(
amountText = amountText,
onAmountChange = { amountText = it },
isEnabled = !isUnlimited,
tokenImageUrl = state.tokenImageUrl,
networkIconRes = state.networkIconRes,
)
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(14.dp))
.background(TangemTheme.colors.background.action)
.padding(16.dp)
.fillMaxWidth(),
) {
Text(
text = stringResourceSafe(R.string.wc_unlimited_amount),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.weight(1f),
)
TangemSwitch(
checked = isUnlimited,
onCheckedChange = { isUnlimited = it },
)
}
Spacer(modifier = Modifier.height(60.dp))
}
},
footer = {
PrimaryButton(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.common_done),
onClick = { onClickDone(amount, isUnlimited) },
)
},
)
} }
@Composable @Composable
private fun AmountTextField( private fun AmountTextField(
allowance: WcCustomAllowanceUM, amountText: String,
onAmountChange: (String) -> Unit, onAmountChange: (String) -> Unit,
isEnabled: Boolean,
tokenImageUrl: String?,
@DrawableRes networkIconRes: Int?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14)) .clip(RoundedCornerShape(14.dp))
.background(color = TangemTheme.colors.background.action) .background(TangemTheme.colors.background.action)
.padding( .padding(end = 8.dp, top = 4.dp, bottom = 4.dp)
end = TangemTheme.dimens.size8,
top = TangemTheme.dimens.size4,
bottom = TangemTheme.dimens.size4,
)
.fillMaxWidth(), .fillMaxWidth(),
) { ) {
TextField( TextField(
value = allowance.amountText, value = amountText,
onValueChange = onAmountChange, onValueChange = onAmountChange,
enabled = isEnabled,
label = { label = {
Text( Text(
text = stringResourceSafe(R.string.send_amount_label), text = stringResourceSafe(R.string.send_amount_label),
@ -127,6 +162,9 @@ private fun AmountTextField(
unfocusedTextColor = TangemTheme.colors.text.primary1, unfocusedTextColor = TangemTheme.colors.text.primary1,
focusedPlaceholderColor = TangemTheme.colors.text.disabled, focusedPlaceholderColor = TangemTheme.colors.text.disabled,
unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, unfocusedPlaceholderColor = TangemTheme.colors.text.disabled,
disabledContainerColor = Color.Transparent,
disabledTextColor = TangemTheme.colors.text.disabled,
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent,
cursorColor = TangemTheme.colors.icon.primary1, cursorColor = TangemTheme.colors.icon.primary1,
@ -134,40 +172,44 @@ private fun AmountTextField(
) )
TokenWithNetworkIcon( TokenWithNetworkIcon(
tokenIconUrl = allowance.tokenIconUrl, tokenIconUrl = tokenImageUrl,
networkIconRes = allowance.networkIconRes, networkIconRes = networkIconRes,
) )
} }
} }
@Composable @Composable
private fun TokenWithNetworkIcon( private fun TokenWithNetworkIcon(
tokenIconUrl: String, tokenIconUrl: String?,
@DrawableRes networkIconRes: Int, @DrawableRes networkIconRes: Int?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Box( Box(
modifier = modifier modifier = modifier.size(48.dp),
.size(TangemTheme.dimens.size48),
) { ) {
AsyncImage( AsyncImage(
model = tokenIconUrl,
contentDescription = null,
modifier = Modifier modifier = Modifier
.aspectRatio(1f) .aspectRatio(1f)
.clip(CircleShape) .clip(CircleShape)
.align(Alignment.Center) .align(Alignment.Center)
.padding(TangemTheme.dimens.spacing8), .padding(8.dp),
model = tokenIconUrl, placeholder = painterResource(R.drawable.ic_nft_placeholder_20),
contentDescription = null, error = painterResource(R.drawable.ic_nft_placeholder_20),
)
Image(
modifier = Modifier
.align(Alignment.TopEnd)
.size(TangemTheme.dimens.size18)
.border(TangemTheme.dimens.size2, TangemTheme.colors.background.action, CircleShape)
.padding(TangemTheme.dimens.spacing2),
painter = painterResource(id = networkIconRes),
contentDescription = null,
) )
if (networkIconRes != null) {
Image(
modifier = Modifier
.align(Alignment.TopEnd)
.size(18.dp)
.border(2.dp, TangemTheme.colors.background.action, CircleShape)
.padding(2.dp),
painter = painterResource(networkIconRes),
contentDescription = null,
)
}
} }
} }
@ -175,10 +217,10 @@ private fun TokenWithNetworkIcon(
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
internal fun PreviewWcCustomAllowanceContent( internal fun PreviewWcCustomAllowanceContent(
@PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcCustomAllowanceUM, @PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcSpendAllowanceUM,
) { ) {
TangemThemePreview { TangemThemePreview {
TangemModalBottomSheet<WcCustomAllowanceUM>( TangemModalBottomSheet<WcSpendAllowanceUM>(
config = TangemBottomSheetConfig( config = TangemBottomSheetConfig(
isShown = true, isShown = true,
onDismissRequest = {}, onDismissRequest = {},
@ -196,22 +238,24 @@ internal fun PreviewWcCustomAllowanceContent(
}, },
content = { content = {
WcCustomAllowanceContent( WcCustomAllowanceContent(
allowance = state, state = state,
onAmountChange = {}, onClickDone = { _, _ -> },
onToggleChange = {}, onBack = {},
onClickDone = {}, onDismiss = {},
) )
}, },
) )
} }
} }
private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider<WcCustomAllowanceUM>( private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider<WcSpendAllowanceUM>(
listOf( listOf(
WcCustomAllowanceUM( WcSpendAllowanceUM(
networkIconRes = R.drawable.img_eth_22, networkIconRes = R.drawable.img_eth_22,
tokenIconUrl = "https://tangem.com", tokenImageUrl = "https://tangem.com",
amountText = "100", amountText = TextReference.Str("100"),
amountValue = BigDecimal("100"),
tokenSymbol = "ETH",
isUnlimited = false, isUnlimited = false,
), ),
), ),

View file

@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.ui.approve
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -22,13 +23,18 @@ import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllow
import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem
@Composable @Composable
internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: Modifier = Modifier) { internal fun WcSpendAllowanceItem(
spendAllowance: WcSpendAllowanceUM,
onClickAllowToSpend: () -> Unit,
modifier: Modifier = Modifier,
) {
Column( Column(
modifier = modifier modifier = modifier
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.background(TangemTheme.colors.background.action) .background(TangemTheme.colors.background.action)
.fillMaxWidth() .fillMaxWidth()
.padding(end = 12.dp, bottom = 12.dp), .padding(end = 12.dp, bottom = 12.dp)
.clickable { onClickAllowToSpend() },
) { ) {
WcSmallTitleItem(R.string.wc_allow_to_spend) WcSmallTitleItem(R.string.wc_allow_to_spend)

View file

@ -23,7 +23,11 @@ import com.tangem.features.walletconnect.transaction.ui.approve.WcSpendAllowance
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResultsUM, modifier: Modifier = Modifier) { internal fun TransactionCheckResultsItem(
item: WcSendReceiveTransactionCheckResultsUM,
onClickAllowToSpend: () -> Unit,
modifier: Modifier = Modifier,
) {
Column( Column(
modifier = modifier modifier = modifier
.fillMaxWidth(), .fillMaxWidth(),
@ -38,7 +42,7 @@ internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResu
if (item.estimatedWalletChanges != null) { if (item.estimatedWalletChanges != null) {
WcEstimatedWalletChangesItem(item.estimatedWalletChanges) WcEstimatedWalletChangesItem(item.estimatedWalletChanges)
} else if (item.spendAllowance != null) { } else if (item.spendAllowance != null) {
WcSpendAllowanceItem(item.spendAllowance) WcSpendAllowanceItem(item.spendAllowance, onClickAllowToSpend)
} else { } else {
WcEstimatedWalletChangesNotLoadedItem() WcEstimatedWalletChangesNotLoadedItem()
} }
@ -57,7 +61,7 @@ private fun TransactionCheckResultsItemPreview(
modifier = Modifier modifier = Modifier
.background(TangemTheme.colors.background.tertiary), .background(TangemTheme.colors.background.tertiary),
) { ) {
TransactionCheckResultsItem(item = item) TransactionCheckResultsItem(item = item, {})
} }
} }
} }

View file

@ -5,6 +5,7 @@ import com.domain.blockaid.models.transaction.SimulationResult
import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.ValidationResult
import com.domain.blockaid.models.transaction.simultation.AmountInfo import com.domain.blockaid.models.transaction.simultation.AmountInfo
import com.domain.blockaid.models.transaction.simultation.SimulationData import com.domain.blockaid.models.transaction.simultation.SimulationData
import com.tangem.blockchain.common.Amount
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
@ -21,21 +22,20 @@ import javax.inject.Inject
internal const val DECIMALS_AMOUNT = 2 internal const val DECIMALS_AMOUNT = 2
@Suppress("CyclomaticComplexMethod")
internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor(
private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter, private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter,
private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter, private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter,
) : ) : Converter<WcSendAndReceiveBlockAidUiConverter.Input, WcSendReceiveTransactionCheckResultsUM> {
Converter<CheckTransactionResult, WcSendReceiveTransactionCheckResultsUM> { override fun convert(value: Input): WcSendReceiveTransactionCheckResultsUM {
override fun convert(value: CheckTransactionResult): WcSendReceiveTransactionCheckResultsUM { val description = value.result.description
return WcSendReceiveTransactionCheckResultsUM( return WcSendReceiveTransactionCheckResultsUM(
isLoading = false, isLoading = false,
notificationText = when (value.validation) { notificationText = when (value.result.validation) {
ValidationResult.SAFE, ValidationResult.FAILED_TO_VALIDATE -> null ValidationResult.SAFE, ValidationResult.FAILED_TO_VALIDATE -> null
// TODO("Add text after approve with designer") ValidationResult.UNSAFE -> if (!description.isNullOrEmpty()) TextReference.Str(description) else null
ValidationResult.UNSAFE ->
TextReference.Str("The transaction approves tokens to a known malicious address")
}, },
estimatedWalletChanges = (value.simulation as? SimulationResult.Success)?.data?.let { data -> estimatedWalletChanges = (value.result.simulation as? SimulationResult.Success)?.data?.let { data ->
when (data) { when (data) {
is SimulationData.Approve -> null is SimulationData.Approve -> null
is SimulationData.SendAndReceive -> { is SimulationData.SendAndReceive -> {
@ -82,15 +82,21 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor(
} }
} }
}, },
spendAllowance = (value.simulation as? SimulationResult.Success)?.data?.let { data -> spendAllowance = (value.result.simulation as? SimulationResult.Success)?.data?.let { data ->
when (data) { when (data) {
is SimulationData.SendAndReceive -> null is SimulationData.SendAndReceive -> null
is SimulationData.Approve -> data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) } is SimulationData.Approve -> value.approvedAmount?.let {
.firstOrNull() data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) }.firstOrNull()
}
} }
}, },
) )
} }
data class Input(
val result: CheckTransactionResult,
val approvedAmount: Amount?,
)
} }
internal fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } internal fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) }

View file

@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.ui.blockaid
import com.domain.blockaid.models.transaction.simultation.ApprovedAmount import com.domain.blockaid.models.transaction.simultation.ApprovedAmount
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
@ -10,12 +11,15 @@ import javax.inject.Inject
internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter<ApprovedAmount, WcSpendAllowanceUM> { internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter<ApprovedAmount, WcSpendAllowanceUM> {
override fun convert(value: ApprovedAmount) = WcSpendAllowanceUM( override fun convert(value: ApprovedAmount) = WcSpendAllowanceUM(
amountValue = value.approvedAmount,
amountText = if (value.isUnlimited) { amountText = if (value.isUnlimited) {
TextReference.Res(R.string.wc_common_unlimited) TextReference.Res(R.string.wc_common_unlimited)
} else { } else {
TextReference.Str(value.approvedAmount.amountText()) TextReference.Str(value.approvedAmount.amountText())
}, },
isUnlimited = value.isUnlimited,
tokenSymbol = value.tokenInfo.symbol, tokenSymbol = value.tokenInfo.symbol,
tokenImageUrl = value.tokenInfo.logoUrl, tokenImageUrl = value.tokenInfo.logoUrl,
networkIconRes = value.tokenInfo.chainId?.toString()?.let { getActiveIconRes(it) },
) )
} }

View file

@ -46,6 +46,7 @@ internal fun WcSendTransactionModalBottomSheet(
onClickTransactionRequest: () -> Unit, onClickTransactionRequest: () -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
onClickAllowToSpend: () -> Unit,
) { ) {
TangemModalBottomSheetWithFooter<TangemBottomSheetConfigContent.Empty>( TangemModalBottomSheetWithFooter<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig( config = TangemBottomSheetConfig(
@ -92,7 +93,7 @@ internal fun WcSendTransactionModalBottomSheet(
} }
Column(modifier = Modifier.padding(top = 16.dp)) { Column(modifier = Modifier.padding(top = 16.dp)) {
if (state.estimatedWalletChanges != null) { if (state.estimatedWalletChanges != null) {
TransactionCheckResultsItem(state.estimatedWalletChanges) TransactionCheckResultsItem(state.estimatedWalletChanges, onClickAllowToSpend)
} }
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
WcSendTransactionItems( WcSendTransactionItems(
@ -140,7 +141,7 @@ private fun WcSendTransactionBottomSheetPreview(
) )
}, },
content = { content = {
WcSendTransactionModalBottomSheet(state, {}, {}, {}) WcSendTransactionModalBottomSheet(state, {}, {}, {}, {})
}, },
) )
} }