Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-27 18:37:48 +03:00
parent 1e19ce06b8
commit 5ce498df75
15 changed files with 222 additions and 237 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.states.Item
import com.tangem.core.ui.components.states.SelectableItemsState
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.swap.domain.models.ui.TxFee
import com.tangem.feature.swap.models.states.FeeItemState
import com.tangem.feature.swap.models.states.ProviderState
@ -44,6 +45,7 @@ sealed class SwapCardState {
data class SwapCardData(
val type: TransactionCardType,
val amountEquivalent: String?,
val token: CryptoCurrencyStatus?,
val coinId: String?,
val amountTextFieldValue: TextFieldValue?,
val tokenIconUrl: String?,

View file

@ -1,7 +1,20 @@
package com.tangem.feature.swap.models
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.feature.swap.domain.models.domain.SwapProvider
data class SwapSuccessStateHolder(
val fromTokenAmount: String,
val toTokenAmount: String,
val timestamp: Long,
val txUrl: String,
val fee: TextReference,
val rate: TextReference,
val selectedProvider: SwapProvider,
val fromTokenAmount: TextReference,
val toTokenAmount: TextReference,
val fromTokenFiatAmount: TextReference,
val toTokenFiatAmount: TextReference,
val fromTokenIconState: TokenIconState?,
val toTokenIconState: TokenIconState?,
val onSecondaryButtonClick: () -> Unit,
)

View file

@ -4,6 +4,7 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
@ -22,6 +23,7 @@ import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.*
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.viewmodels.SwapProcessDataState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@ -37,6 +39,8 @@ internal class StateBuilder(
private val appCurrencyProvider: Provider<AppCurrency>,
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val tokensDataConverter = TokensDataConverter(
onSearchEntered = actions.onSearchEntered,
onTokenSelected = actions.onTokenSelected,
@ -52,6 +56,7 @@ internal class StateBuilder(
type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected),
amountEquivalent = null,
amountTextFieldValue = null,
token = null,
tokenIconUrl = initialCurrency.iconUrl,
tokenCurrency = initialCurrency.symbol,
coinId = initialCurrency.network.backendId,
@ -65,6 +70,7 @@ internal class StateBuilder(
amountEquivalent = null,
tokenIconUrl = "",
tokenCurrency = "",
token = null,
amountTextFieldValue = null,
canSelectAnotherToken = false,
balance = "",
@ -98,6 +104,7 @@ internal class StateBuilder(
text = "0",
),
amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
token = fromToken,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = uiStateHolder.sendCardData.coinId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
@ -148,6 +155,7 @@ internal class StateBuilder(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = null,
token = uiStateHolder.sendCardData.token,
tokenIconUrl = fromToken.iconUrl,
tokenCurrency = fromToken.symbol,
coinId = fromToken.network.backendId,
@ -160,6 +168,7 @@ internal class StateBuilder(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = null,
amountEquivalent = null,
token = uiStateHolder.receiveCardData.token,
tokenIconUrl = toToken.iconUrl,
tokenCurrency = toToken.symbol,
coinId = toToken.network.backendId,
@ -225,6 +234,7 @@ internal class StateBuilder(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat),
token = fromCurrencyStatus,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = fromCurrencyStatus.currency.network.backendId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
@ -237,6 +247,7 @@ internal class StateBuilder(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()),
amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
token = toCurrencyStatus,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = toCurrencyStatus.currency.network.backendId,
isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken,
@ -282,6 +293,7 @@ internal class StateBuilder(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard),
amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue,
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
token = uiStateHolder.sendCardData.token,
tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl,
coinId = uiStateHolder.sendCardData.coinId,
isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken,
@ -294,6 +306,7 @@ internal class StateBuilder(
type = TransactionCardType.ReceiveCard(),
amountTextFieldValue = TextFieldValue("0"),
amountEquivalent = emptyAmountState.zeroAmountEquivalent,
token = uiStateHolder.receiveCardData.token,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = uiStateHolder.receiveCardData.coinId,
isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken,
@ -434,12 +447,38 @@ internal class StateBuilder(
fun createSuccessState(
uiState: SwapStateHolder,
txState: TxState.TxSent,
dataState: SwapProcessDataState,
txUrl: String,
onSecondaryBtnClick: () -> Unit,
): SwapStateHolder {
val fromToken = requireNotNull(uiState.sendCardData as? SwapCardState.SwapCardData)
val toToken = requireNotNull(uiState.receiveCardData as? SwapCardState.SwapCardData)
val fromTokenIconState = fromToken.token?.let(iconStateConverter::convert)
val toTokenIconState = toToken.token?.let(iconStateConverter::convert)
val fee = uiState.fee as? FeeItemState.Content ?: return uiState
val fromCryptoCurrencyStatus = requireNotNull(fromToken.token)
val toCryptoCurrencyStatus = requireNotNull(toToken.token)
val rate = txState.toAmount?.toBigDecimal()?.divide(
txState.fromAmount?.toBigDecimal(),
toCryptoCurrencyStatus.currency.decimals,
RoundingMode.HALF_UP,
)
val fromCurrencySymbol = fromCryptoCurrencyStatus.currency.symbol
val toCurrencySymbol = toCryptoCurrencyStatus.currency.symbol
return uiState.copy(
successState = SwapSuccessStateHolder(
fromTokenAmount = txState.fromAmount ?: "",
toTokenAmount = txState.toAmount ?: "",
timestamp = System.currentTimeMillis(),
txUrl = txUrl,
selectedProvider = requireNotNull(dataState.selectedProvider),
fee = TextReference.Str("${fee.amountCrypto} ${fee.symbolCrypto} (${fee.amountFiatFormatted})"),
rate = TextReference.Str("1 $fromCurrencySymbol$rate $toCurrencySymbol"),
fromTokenAmount = TextReference.Str("${txState.fromAmount.orEmpty()} ${fromToken.tokenCurrency}}"),
toTokenAmount = TextReference.Str("${txState.toAmount.orEmpty()} ${toToken.tokenCurrency}}"),
fromTokenFiatAmount = TextReference.Str(fromToken.amountEquivalent.orEmpty()),
toTokenFiatAmount = TextReference.Str(toToken.amountEquivalent.orEmpty()),
fromTokenIconState = fromTokenIconState,
toTokenIconState = toTokenIconState,
onSecondaryButtonClick = onSecondaryBtnClick,
),
)

View file

@ -272,7 +272,7 @@ private fun DropdownSelector(
@Composable
private fun FeeItem(fee: String) {
InformationItem(
subtitle = stringResource(id = R.string.send_fee_label),
subtitle = stringResource(id = R.string.common_fee_label),
value = fee,
)
}

View file

@ -336,6 +336,7 @@ private val sendCard = SwapCardState.SwapCardData(
canSelectAnotherToken = false,
balance = "123",
coinId = "",
token = null,
isBalanceHidden = false,
)
@ -349,6 +350,7 @@ private val receiveCard = SwapCardState.SwapCardData(
canSelectAnotherToken = true,
balance = "33333",
coinId = "",
token = null,
isBalanceHidden = false,
)

View file

@ -1,18 +1,29 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.ResultScreenContent
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImage
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.shareText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.presentation.R
@ -21,41 +32,117 @@ fun SwapSuccessScreen(state: SwapSuccessStateHolder, onBack: () -> Unit) {
Scaffold(
modifier = Modifier.systemBarsPadding(),
content = { padding ->
ResultScreenContent(
resultMessage = makeSuccessMessage(
fromTokenAmount = state.fromTokenAmount,
toTokenAmount = state.toTokenAmount,
),
resultColor = TangemTheme.colors.icon.attention,
onButtonClick = onBack,
icon = R.drawable.ic_clock_24,
secondaryButtonIcon = R.drawable.ic_arrow_top_right_24,
onSecondaryButtonClick = state.onSecondaryButtonClick,
secondaryButtonText = R.string.swapping_success_view_explorer_button_title,
title = R.string.swapping_success_view_title,
modifier = Modifier.padding(padding),
)
SwapSuccessScreenContent(padding = padding, state = state)
},
topBar = {
AppBarWithBackButton(
text = stringResource(R.string.common_swap),
onBackClick = onBack,
iconRes = R.drawable.ic_close_24,
)
},
bottomBar = {
SwapSuccessScreenButtons(
textRes = R.string.common_close,
txUrl = state.txUrl,
onExploreClick = state.onSecondaryButtonClick,
onDoneClick = onBack,
)
},
)
}
@Composable
private fun makeSuccessMessage(fromTokenAmount: String, toTokenAmount: String): AnnotatedString {
val swapToString = stringResource(id = R.string.swapping_swap_of_to, fromTokenAmount)
val message = "$swapToString\n$toTokenAmount"
return buildAnnotatedString {
append(message)
addStyle(
style = SpanStyle(color = TangemTheme.colors.text.accent),
start = message.indexOf(toTokenAmount),
end = message.length,
private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: PaddingValues) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = 0L)
SpacerH16()
InputRowImage(
title = TextReference.Res(R.string.swapping_success_from_title),
subtitle = state.fromTokenAmount,
caption = state.fromTokenFiatAmount,
tokenIconState = state.fromTokenIconState ?: TokenIconState.Loading,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
SpacerH16()
InputRowImage(
title = TextReference.Res(R.string.swapping_success_to_title),
subtitle = state.toTokenAmount,
caption = state.toTokenFiatAmount,
tokenIconState = state.toTokenIconState ?: TokenIconState.Loading,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
SpacerH16()
InputRowBestRate(
imageUrl = state.selectedProvider.imageLarge,
title = TextReference.Str(state.selectedProvider.name),
titleExtra = TextReference.Str(state.selectedProvider.type.name),
subtitle = state.rate,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
SpacerH16()
InputRowDefault(
title = TextReference.Res(R.string.common_fee_label),
text = state.fee,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
}
}
@Composable
private fun SwapSuccessScreenButtons(
@StringRes textRes: Int,
txUrl: String,
onExploreClick: () -> Unit,
onDoneClick: () -> Unit,
) {
val hapticFeedback = LocalHapticFeedback.current
val context = LocalContext.current
Column(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(TangemTheme.dimens.spacing16),
) {
if (txUrl.isNotBlank()) {
Row {
SecondaryButtonIconStart(
text = stringResource(id = com.tangem.core.ui.R.string.common_explore),
iconResId = com.tangem.core.ui.R.drawable.ic_web_24,
onClick = onExploreClick,
modifier = Modifier.weight(1f),
)
SpacerW12()
SecondaryButtonIconStart(
text = stringResource(id = com.tangem.core.ui.R.string.common_share),
iconResId = com.tangem.core.ui.R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(txUrl)
},
modifier = Modifier.weight(1f),
)
}
SpacerH12()
}
PrimaryButton(
text = stringResource(id = textRes),
enabled = true,
onClick = onDoneClick,
modifier = Modifier.fillMaxWidth(),
)
}
}
@ -63,9 +150,25 @@ private fun makeSuccessMessage(fromTokenAmount: String, toTokenAmount: String):
// region preview
private val state = SwapSuccessStateHolder(
fromTokenAmount = "1 000 DAI",
toTokenAmount = "1 131,46 MATIC",
) {}
timestamp = 0L,
txUrl = "https://www.google.com/#q=nam",
fee = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
selectedProvider = SwapProvider(
providerId = "1inch",
rateTypes = listOf(),
name = "1inch",
type = ExchangeProviderType.DEX,
imageLarge = "",
),
fromTokenAmount = TextReference.Str("1 000 DAI"),
toTokenAmount = TextReference.Str("1 000 MATIC"),
fromTokenFiatAmount = TextReference.Str("1 000 $"),
toTokenFiatAmount = TextReference.Str("1 000 $"),
fromTokenIconState = TokenIconState.Loading,
toTokenIconState = TokenIconState.Loading,
rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"),
onSecondaryButtonClick = {},
)
@Preview(showBackground = true)
@Composable

View file

@ -351,17 +351,22 @@ internal class SwapViewModel @Inject constructor(
.onSuccess {
when (it) {
is TxState.TxSent -> {
uiState = stateBuilder.createSuccessState(uiState, it) {
val txHash = it.txAddress
if (txHash.isNotEmpty()) {
swapRouter.openUrl(
blockchainInteractor.getExplorerTransactionLink(
networkId = dataState.networkId,
txAddress = it.txAddress,
),
)
}
}
val url = blockchainInteractor.getExplorerTransactionLink(
networkId = dataState.networkId,
txAddress = it.txAddress,
)
uiState = stateBuilder.createSuccessState(
uiState = uiState,
txState = it,
dataState = dataState,
txUrl = url,
onSecondaryBtnClick = {
val txHash = it.txAddress
if (txHash.isNotEmpty()) {
swapRouter.openUrl(url)
}
},
)
analyticsEventHandler.send(SwapEvents.SwapInProgressScreen)
swapRouter.openScreen(SwapNavScreen.Success)
}