Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-24 18:28:09 +03:00
parent 464f8f8a80
commit d10cf0ac2e
23 changed files with 1117 additions and 10 deletions

View file

@ -0,0 +1,20 @@
package com.tangem.feature.swap.domain.models.domain
data class ExchangeStatusModel(
val providerId: Int,
val status: ExchangeStatus? = null,
val txId: String? = null,
val txUrl: String? = null,
)
enum class ExchangeStatus {
New,
Waiting,
Confirming,
Verifying,
Exchanging,
Failed,
Sending,
Finished,
Refunded,
}

View file

@ -28,6 +28,7 @@ dependencies {
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.compose.constraintLayout)
implementation(deps.arrow.core)
implementation(deps.jodatime)
@ -68,4 +69,5 @@ dependencies {
/** Feature Apis */
implementation(projects.features.tokendetails.api)
implementation(projects.features.send.api)
implementation(projects.features.swap.domain)
}

View file

@ -0,0 +1,31 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
internal data class SwapTransactionsState(
val txId: String,
val providerId: Int,
val txUrl: String? = null,
val rate: BigDecimal,
val timestamp: Long,
val status: PersistentList<ExchangeStatusState>,
val activeStatus: ExchangeStatus?,
val toCryptoAmount: String,
val toFiatAmount: String,
val toCurrencyIcon: TokenIconState,
val fromCryptoAmount: String,
val fromFiatAmount: String,
val fromCurrencyIcon: TokenIconState,
val onClick: () -> Unit,
val onGoToProviderClick: () -> Unit,
)
internal class ExchangeStatusState(
val status: ExchangeStatus,
val text: String,
val isActive: Boolean,
val isDone: Boolean,
)

View file

@ -39,7 +39,7 @@ internal fun TokenDetailsBalanceBlock(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
),
text = stringResource(id = R.string.onboarding_balance_title),
text = stringResource(id = R.string.common_balance_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
maxLines = 1,

View file

@ -0,0 +1,67 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.components.inputrow.InputRowApprox
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tokendetails.impl.R
@Suppress("LongParameterList")
@Composable
internal fun ExchangeEstimate(
timestamp: TextReference,
fromTokenIconState: TokenIconState,
toTokenIconState: TokenIconState,
fromCryptoAmount: TextReference,
toCryptoAmount: TextReference,
fromFiatAmount: TextReference,
toFiatAmount: TextReference,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing14,
bottom = TangemTheme.dimens.spacing2,
),
) {
Text(
text = stringResource(id = R.string.express_estimated_amount),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Text(
text = timestamp.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
InputRowApprox(
leftIcon = fromTokenIconState,
leftTitle = fromCryptoAmount,
leftSubtitle = fromFiatAmount,
rightIcon = toTokenIconState,
rightTitle = toCryptoAmount,
rightSubtitle = toFiatAmount,
)
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.InputRowBestRate
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ExchangeProvider(providerName: TextReference, providerType: TextReference, imageUrl: String) {
Column(
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
Text(
text = stringResource(id = R.string.express_provider),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
)
InputRowBestRate(
imageUrl = imageUrl,
title = providerName,
titleExtra = providerType,
subtitle = TextReference.Res(R.string.express_floating_rate),
)
}
}

View file

@ -0,0 +1,209 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
@Composable
internal fun ExchangeStatusBlock(
status: PersistentList<ExchangeStatusState>,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(
vertical = TangemTheme.dimens.spacing14,
horizontal = TangemTheme.dimens.spacing12,
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing16),
) {
Text(
text = stringResource(id = R.string.common_balance_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
Row(
modifier = Modifier.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.spacing16)
.padding(end = TangemTheme.dimens.spacing2),
)
Text(
text = stringResource(id = R.string.express_go_to_provider),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
status.forEachIndexed { index, item ->
ExchangeStatusStep(
stepStatus = item,
isLast = index == status.lastIndex,
)
}
}
}
@Composable
private fun ExchangeStatusStep(
stepStatus: ExchangeStatusState,
modifier: Modifier = Modifier,
isLast: Boolean = false,
) {
Row(modifier = modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
AnimatedContent(
targetState = stepStatus,
label = "Exchange Step Change Success",
modifier = Modifier
.size(TangemTheme.dimens.size20),
) {
when {
it.status == ExchangeStatus.Failed && !it.isDone -> ExchangeStepWaringOrError(
iconRes = R.drawable.ic_close_24,
color = TangemTheme.colors.icon.warning,
)
it.status == ExchangeStatus.Verifying && !it.isDone -> ExchangeStepWaringOrError(
iconRes = R.drawable.ic_exclamation_24,
color = TangemTheme.colors.icon.attention,
)
it.isDone -> ExchangeStepSuccess()
it.isActive -> ExchangeStepInProgress()
else -> ExchangeStepDefault()
}
}
if (!isLast) {
ExchangeStepSeparator()
}
}
ExchangeStatusStepText(stepStatus)
}
}
@Composable
private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) {
val textColor = when {
stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning
stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention
stepStatus.isDone -> TangemTheme.colors.text.primary1
!stepStatus.isActive -> TangemTheme.colors.text.disabled
else -> TangemTheme.colors.text.primary1
}
Text(
text = stepStatus.text,
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
)
}
@Composable
private fun ExchangeStepDefault() {
Box(
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepSuccess() {
Icon(
painter = painterResource(id = R.drawable.ic_check_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = TangemTheme.colors.field.focused,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepWaringOrError(color: Color, @DrawableRes iconRes: Int) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = color,
modifier = Modifier
.border(
width = TangemTheme.dimens.size1_5,
color = color,
shape = CircleShape,
)
.padding(TangemTheme.dimens.spacing2),
)
}
@Composable
private fun ExchangeStepInProgress() {
CircularProgressIndicator(
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
modifier = Modifier
.padding(TangemTheme.dimens.spacing2)
.size(TangemTheme.dimens.size14),
)
}
@Composable
private fun ExchangeStepSeparator() {
Box(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.size(
width = TangemTheme.dimens.size1_5,
height = TangemTheme.dimens.size10,
)
.background(
color = TangemTheme.colors.field.focused,
shape = CircleShape,
),
)
}

View file

@ -0,0 +1,84 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.toDateFormat
import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
@Composable
internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
color = TangemTheme.colors.background.tertiary,
) { content: ExchangeStatusBottomSheetConfig ->
ExchangeStatusBottomSheetContent(content = content)
}
}
@Composable
private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) {
val config = content.value
Column(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.align(CenterHorizontally),
)
SpacerH10()
Text(
text = stringResource(id = R.string.express_exchange_status_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
.align(CenterHorizontally),
)
SpacerH16()
val timestamp = config.timestamp
ExchangeEstimate(
timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"),
fromTokenIconState = config.fromCurrencyIcon,
toTokenIconState = config.toCurrencyIcon,
fromCryptoAmount = TextReference.Str(config.fromCryptoAmount),
toCryptoAmount = TextReference.Str(config.toCryptoAmount),
fromFiatAmount = TextReference.Str(config.fromFiatAmount),
toFiatAmount = TextReference.Str(config.toFiatAmount),
)
SpacerH12()
// todo replace with real provider data
ExchangeProvider(
providerName = TextReference.Str(config.providerId.toString()),
providerType = TextReference.Str("CEX"),
imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/changenow_512.png",
)
SpacerH12()
ExchangeStatusBlock(
status = config.status,
onClick = config.onGoToProviderClick,
)
SpacerH24()
}
}
internal data class ExchangeStatusBottomSheetConfig(
val value: SwapTransactionsState,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,180 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Visibility
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.domain.models.domain.ExchangeStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.PersistentList
@OptIn(ExperimentalFoundationApi::class)
internal fun LazyListScope.swapTransactionsItems(
swapTxs: PersistentList<SwapTransactionsState>,
modifier: Modifier = Modifier,
) {
if (swapTxs.isNotEmpty()) {
items(
count = swapTxs.size,
key = { swapTxs[it].txId },
contentType = { swapTxs[it]::class.java },
) {
val item = swapTxs[it]
val (iconRes, tint) = when (item.activeStatus) {
ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention
ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning
else -> null to null
}
ExchangeStatusItem(
providerName = item.providerId.toString(),
fromTokenIconState = item.fromCurrencyIcon,
toTokenIconState = item.toCurrencyIcon,
fromAmount = item.fromCryptoAmount,
toAmount = item.toCryptoAmount,
onClick = item.onClick,
infoIconRes = iconRes,
infoIconTint = tint,
modifier = modifier.animateItemPlacement(),
)
}
}
}
@Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList")
@Composable
private fun ExchangeStatusItem(
providerName: String,
fromTokenIconState: TokenIconState,
toTokenIconState: TokenIconState,
fromAmount: String,
toAmount: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
@DrawableRes infoIconRes: Int? = null,
infoIconTint: Color? = null,
) {
ConstraintLayout(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable { onClick() }
.padding(TangemTheme.dimens.spacing12),
) {
val (titleRef, iconRef, infoIconRef, swapIconRef, fromRef, toRef, fromIconRef, toIconRef) = createRefs()
val padding6 = TangemTheme.dimens.spacing6
Text(
text = stringResource(id = R.string.express_exchange_by, providerName),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.constrainAs(titleRef) {
start.linkTo(parent.start)
top.linkTo(parent.top)
},
)
TokenIcon(
state = fromTokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(fromIconRef) {
start.linkTo(parent.start)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Text(
text = fromAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.constrainAs(fromRef) {
start.linkTo(fromIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Icon(
painter = painterResource(id = R.drawable.ic_forward_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size12)
.constrainAs(swapIconRef) {
start.linkTo(fromRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
TokenIcon(
state = toTokenIconState,
shouldDisplayNetwork = false,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(toIconRef) {
start.linkTo(swapIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Text(
text = toAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.constrainAs(toRef) {
start.linkTo(toIconRef.end, padding6)
top.linkTo(titleRef.bottom, padding6)
bottom.linkTo(parent.bottom)
},
)
Icon(
painter = painterResource(id = infoIconRes ?: R.drawable.ic_alert_triangle_20),
contentDescription = null,
tint = infoIconTint ?: TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size20)
.constrainAs(infoIconRef) {
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(iconRef.start)
visibility = if (infoIconRes == null) {
Visibility.Gone
} else {
Visibility.Visible
}
},
)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier
.size(TangemTheme.dimens.size24)
.constrainAs(iconRef) {
end.linkTo(parent.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
},
)
}
}

View file

@ -475,4 +475,35 @@ internal class TokenDetailsViewModel @Inject constructor(
override fun onCloseRentInfoNotification() {
uiState = stateFactory.getStateWithRemovedRentNotification()
}
}
}
//
// val text = when (stepStatus.status) {
// ExchangeStatus.Failed -> stringResource(id = R.string.express_exchange_status_failed)
// ExchangeStatus.Verifying -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_verified)
// } else {
// stringResource(id = R.string.express_exchange_status_verifying)
// }
// ExchangeStatus.New, ExchangeStatus.Waiting -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_received)
// } else {
// stringResource(id = R.string.express_exchange_status_receiving)
// }
// ExchangeStatus.Confirming -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_confirmed)
// } else {
// stringResource(id = R.string.express_exchange_status_confirming)
// }
// ExchangeStatus.Exchanging -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_exchanged)
// } else {
// stringResource(id = R.string.express_exchange_status_exchanging)
// }
// ExchangeStatus.Sending -> if (stepStatus.isDone) {
// stringResource(id = R.string.express_exchange_status_sent)
// } else {
// stringResource(id = R.string.express_exchange_status_sending)
// }
// else -> ""
// }