Updated on 2026-08-14
This commit is contained in:
parent
5e12c2dc29
commit
af8544e6d7
12 changed files with 1048 additions and 65 deletions
315
common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt
Normal file
315
common/ui/src/main/java/com/tangem/common/ui/earn/EarnBlock.kt
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
package com.tangem.common.ui.earn
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.innerShadow
|
||||
import androidx.compose.ui.graphics.shadow.Shadow
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.ds.button.TangemButton
|
||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
||||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.row.TangemRowContainer
|
||||
import com.tangem.core.ui.ds.row.TangemRowLayoutId
|
||||
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
private const val TINTED_BACKGROUND_ALPHA = 0.1f
|
||||
private const val TINTED_BORDER_ALPHA = 0.1f
|
||||
private const val TINTED_INNER_SHADOW_ALPHA = 0.3f
|
||||
private val BorderWidth = 1.dp
|
||||
private val InnerShadowBlur = 20.dp
|
||||
private val ShimmerSubtitleWidth = 78.dp
|
||||
|
||||
@Composable
|
||||
fun EarnBlock(state: EarnBlockUM, modifier: Modifier = Modifier) {
|
||||
when (state) {
|
||||
is EarnBlockUM.Loading -> EarnBlockLoading(modifier)
|
||||
is EarnBlockUM.Content -> EarnBlockContent(state, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockLoading(modifier: Modifier = Modifier) {
|
||||
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
|
||||
TangemRowContainer(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.background(TangemTheme.colors2.surface.level3)
|
||||
.border(width = BorderWidth, color = TangemTheme.colors2.border.neutral.primary, shape = shape),
|
||||
contentPadding = PaddingValues(all = TangemTheme.dimens2.x3),
|
||||
content = {
|
||||
CircleShimmer(
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.size(TangemTheme.dimens2.x10),
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.size(width = TangemTheme.dimens2.x16, height = TangemTheme.dimens2.x5),
|
||||
radius = TangemTheme.dimens2.x2,
|
||||
)
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.size(width = ShimmerSubtitleWidth, height = TangemTheme.dimens2.x4),
|
||||
radius = TangemTheme.dimens2.x2,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Modifier) {
|
||||
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
|
||||
|
||||
val backgroundModifier = resolveBackgroundModifier(state.backgroundUM, shape)
|
||||
|
||||
val clickModifier = when (val trailing = state.trailingUM) {
|
||||
is EarnBlockUM.TrailingUM.Balance -> Modifier.clickable(onClick = trailing.onClick)
|
||||
else -> Modifier
|
||||
}
|
||||
|
||||
TangemRowContainer(
|
||||
modifier = modifier
|
||||
.clip(shape)
|
||||
.then(backgroundModifier)
|
||||
.then(clickModifier),
|
||||
contentPadding = PaddingValues(all = TangemTheme.dimens2.x3),
|
||||
content = {
|
||||
// Icon (HEAD)
|
||||
EarnBlockIcon(
|
||||
iconUM = state.iconUM,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3),
|
||||
)
|
||||
|
||||
// Title (START_TOP)
|
||||
Text(
|
||||
text = state.titleUM.text.resolveReference(),
|
||||
style = when (state.titleUM.style) {
|
||||
EarnBlockUM.TitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
|
||||
EarnBlockUM.TitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
|
||||
},
|
||||
color = state.titleUM.color(),
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.padding(end = TangemTheme.dimens2.x2),
|
||||
)
|
||||
|
||||
// Subtitle (START_BOTTOM)
|
||||
val subtitle = state.subtitleUM
|
||||
if (subtitle is EarnBlockUM.SubtitleUM.Text) {
|
||||
Text(
|
||||
text = subtitle.text.resolveReference(),
|
||||
style = when (subtitle.style) {
|
||||
EarnBlockUM.SubtitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
|
||||
EarnBlockUM.SubtitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
|
||||
},
|
||||
color = subtitle.color(),
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(end = TangemTheme.dimens2.x2),
|
||||
)
|
||||
}
|
||||
|
||||
// Trailing
|
||||
EarnBlockTrailing(trailingUM = state.trailingUM)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun resolveBackgroundModifier(backgroundUM: EarnBlockUM.BackgroundUM, shape: RoundedCornerShape): Modifier {
|
||||
return when (backgroundUM) {
|
||||
is EarnBlockUM.BackgroundUM.Surface -> Modifier
|
||||
.background(TangemTheme.colors2.surface.level3)
|
||||
.border(width = BorderWidth, color = TangemTheme.colors2.border.neutral.primary, shape = shape)
|
||||
is EarnBlockUM.BackgroundUM.Tinted -> {
|
||||
val tintColor = backgroundUM.color()
|
||||
Modifier
|
||||
.background(tintColor.copy(alpha = TINTED_BACKGROUND_ALPHA))
|
||||
.border(width = BorderWidth, color = tintColor.copy(alpha = TINTED_BORDER_ALPHA), shape = shape)
|
||||
.innerShadow(
|
||||
shape = shape,
|
||||
shadow = Shadow(
|
||||
radius = InnerShadowBlur,
|
||||
color = tintColor.copy(alpha = TINTED_INNER_SHADOW_ALPHA),
|
||||
offset = DpOffset.Zero,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockTrailing(trailingUM: EarnBlockUM.TrailingUM?) {
|
||||
when (trailingUM) {
|
||||
is EarnBlockUM.TrailingUM.Button -> {
|
||||
TangemButton(
|
||||
buttonUM = trailingUM.buttonUM,
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.TAIL),
|
||||
)
|
||||
}
|
||||
is EarnBlockUM.TrailingUM.Balance -> {
|
||||
if (!trailingUM.isBalanceHidden) {
|
||||
Text(
|
||||
text = trailingUM.fiatValue.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography2.bodySemibold16,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
|
||||
)
|
||||
Text(
|
||||
text = trailingUM.cryptoValue.resolveReference(),
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
|
||||
)
|
||||
}
|
||||
}
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EarnBlockIcon(iconUM: EarnBlockUM.IconUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.size(TangemTheme.dimens2.x10),
|
||||
) {
|
||||
if (iconUM is EarnBlockUM.IconUM.Glowing) {
|
||||
val glowShape = RoundedCornerShape(percent = 50)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens2.x6)
|
||||
.blur(radius = TangemTheme.dimens2.x4, edgeTreatment = BlurredEdgeTreatment.Unbounded)
|
||||
.background(color = iconUM.glowColor().copy(alpha = 0.7f), shape = glowShape),
|
||||
)
|
||||
}
|
||||
val iconRes = when (iconUM) {
|
||||
is EarnBlockUM.IconUM.Glowing -> iconUM.iconRes
|
||||
is EarnBlockUM.IconUM.Plain -> iconUM.iconRes
|
||||
}
|
||||
TangemIcon(
|
||||
tangemIconUM = TangemIconUM.Image(imageRes = iconRes),
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x10),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun EarnBlock_Preview(@PreviewParameter(EarnBlockPreviewProvider::class) state: EarnBlockUM) {
|
||||
TangemThemePreviewRedesign {
|
||||
EarnBlock(
|
||||
state = state,
|
||||
modifier = Modifier.padding(TangemTheme.dimens2.x4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
|
||||
collection = listOf(
|
||||
EarnBlockUM.Loading,
|
||||
EarnBlockUM.Content(
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.staking_native),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
color = { TangemTheme.colors2.text.neutral.tertiary },
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
color = { TangemTheme.colors2.text.neutral.tertiary },
|
||||
),
|
||||
trailingUM = null,
|
||||
),
|
||||
EarnBlockUM.Content(
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Tinted { TangemTheme.colors2.border.status.accent },
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(
|
||||
iconRes = R.drawable.ic_staking_40,
|
||||
glowColor = { TangemTheme.colors2.border.status.accent },
|
||||
),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.token_details_staking_block_title),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
color = { TangemTheme.colors2.text.status.accent },
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stringReference("Average APR 5.24%"),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
color = { TangemTheme.colors2.text.neutral.primary },
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
buttonUM = TangemButtonUM(
|
||||
text = stringReference("Stake"),
|
||||
type = TangemButtonType.Accent,
|
||||
size = TangemButtonSize.X9,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
EarnBlockUM.Content(
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(
|
||||
iconRes = R.drawable.ic_staking_40,
|
||||
glowColor = { TangemTheme.colors2.text.status.accent },
|
||||
),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = stringReference("Native staking"),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
color = { TangemTheme.colors2.text.neutral.primary },
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stringReference("$ 12.34 rewards"),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
color = { TangemTheme.colors2.text.status.accent },
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Balance(
|
||||
fiatValue = stringReference("$ 500.17"),
|
||||
cryptoValue = stringReference("500.00 SOL"),
|
||||
isBalanceHidden = false,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.common.ui.earn
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.extensions.ColorReference2
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
@Immutable
|
||||
sealed interface EarnBlockUM {
|
||||
|
||||
data object Loading : EarnBlockUM
|
||||
|
||||
data class Content(
|
||||
val backgroundUM: BackgroundUM,
|
||||
val iconUM: IconUM,
|
||||
val titleUM: TitleUM,
|
||||
val subtitleUM: SubtitleUM?,
|
||||
val trailingUM: TrailingUM?,
|
||||
) : EarnBlockUM
|
||||
|
||||
@Immutable
|
||||
sealed interface BackgroundUM {
|
||||
data object Surface : BackgroundUM
|
||||
data class Tinted(val color: ColorReference2) : BackgroundUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface IconUM {
|
||||
data class Glowing(
|
||||
@DrawableRes val iconRes: Int,
|
||||
val glowColor: ColorReference2,
|
||||
) : IconUM
|
||||
|
||||
data class Plain(
|
||||
@DrawableRes val iconRes: Int,
|
||||
) : IconUM
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class TitleUM(
|
||||
val text: TextReference,
|
||||
val style: Style,
|
||||
val color: ColorReference2,
|
||||
) {
|
||||
enum class Style { Large, Small }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface SubtitleUM {
|
||||
data class Text(
|
||||
val text: TextReference,
|
||||
val style: Style,
|
||||
val color: ColorReference2,
|
||||
) : SubtitleUM
|
||||
|
||||
enum class Style { Large, Small }
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface TrailingUM {
|
||||
data class Button(val buttonUM: TangemButtonUM) : TrailingUM
|
||||
|
||||
data class Balance(
|
||||
val fiatValue: TextReference,
|
||||
val cryptoValue: TextReference,
|
||||
val isBalanceHidden: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
) : TrailingUM
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ fun TangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
|
|||
iconPosition = buttonUM.iconPosition,
|
||||
isEnabled = buttonUM.isEnabled,
|
||||
isLoading = buttonUM.isLoading,
|
||||
type = TangemButtonType.Positive,
|
||||
type = TangemButtonType.Accent,
|
||||
size = buttonUM.size,
|
||||
shape = buttonUM.shape,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,4 +70,20 @@ internal class TokenDetailsDialogFactory @Inject constructor(
|
|||
fun showError(text: TextReference) {
|
||||
uiMessageSender.send(DialogMessage(message = text))
|
||||
}
|
||||
|
||||
fun showConfirmHideExpressStatus(onConfirm: () -> Unit) {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.express_status_hide_dialog_title),
|
||||
message = resourceReference(R.string.express_status_hide_dialog_text),
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_hide),
|
||||
onClick = onConfirm,
|
||||
)
|
||||
},
|
||||
secondActionBuilder = { cancelAction() },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,8 +42,6 @@ import com.tangem.core.ui.extensions.stringReference
|
|||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
|
||||
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
|
||||
|
|
@ -110,6 +108,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.transform
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetTopBarTitleTransformer
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.ToggleBalanceTypeTransformer
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateStakingNotificationTransformer
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateNotificationsTransformer
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTopBarMenuTransformer
|
||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
|
|
@ -128,6 +128,7 @@ import kotlinx.coroutines.launch
|
|||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration")
|
||||
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class TokenDetailsModel @Inject constructor(
|
||||
|
|
@ -204,6 +205,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val yieldSupplyBalanceJobHolder = JobHolder()
|
||||
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
|
||||
private val redesignBalanceJobHolder = JobHolder()
|
||||
private val redesignEarnJobHolder = JobHolder()
|
||||
|
||||
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
|
||||
private var account: Account.CryptoPortfolio? = null
|
||||
|
|
@ -386,6 +388,13 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
val updatedState = stateFactory.getStateWithNotifications(warnings)
|
||||
notificationsAnalyticsSender.send(uiState.value, updatedState.notifications)
|
||||
uiState.value = updatedState
|
||||
|
||||
redesignStateController.update(
|
||||
UpdateNotificationsTransformer(
|
||||
warnings = warnings,
|
||||
clickIntents = this@TokenDetailsModel,
|
||||
),
|
||||
)
|
||||
}
|
||||
.launchIn(modelScope)
|
||||
.saveIn(warningsJobHolder)
|
||||
|
|
@ -797,9 +806,9 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
)
|
||||
|
||||
if (canHide) {
|
||||
showConfirmHideTokenDialog(cryptoCurrency)
|
||||
dialogFactory.showConfirmHideToken(currency = cryptoCurrency, onConfirm = ::onHideConfirmed)
|
||||
} else {
|
||||
showLinkedTokensDialog(cryptoCurrency)
|
||||
dialogFactory.showLinkedTokens(currency = cryptoCurrency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1021,7 +1030,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
if (message != null) {
|
||||
showErrorDialog(stringReference(message))
|
||||
dialogFactory.showError(text = stringReference(message))
|
||||
TangemLogger.e(message)
|
||||
}
|
||||
},
|
||||
|
|
@ -1065,7 +1074,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
if (message != null) {
|
||||
showErrorDialog(message)
|
||||
dialogFactory.showError(text = message)
|
||||
}
|
||||
},
|
||||
ifRight = { uiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() },
|
||||
|
|
@ -1080,7 +1089,9 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
blockchain = cryptoCurrency.network.name,
|
||||
),
|
||||
)
|
||||
showDismissIncompleteTransactionConfirmDialog()
|
||||
dialogFactory.showDismissIncompleteTransactionConfirm(
|
||||
onConfirm = ::onConfirmDismissIncompleteTransactionClick,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onConfirmDismissIncompleteTransactionClick() {
|
||||
|
|
@ -1090,7 +1101,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
currency = cryptoCurrency,
|
||||
).fold(
|
||||
ifLeft = { e ->
|
||||
showErrorDialog(stringReference(e.message.orEmpty()))
|
||||
dialogFactory.showError(text = stringReference(e.message.orEmpty()))
|
||||
TangemLogger.e("Error: $e")
|
||||
},
|
||||
ifRight = {
|
||||
|
|
@ -1115,7 +1126,8 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
ifLeft = { e ->
|
||||
when (e) {
|
||||
is AssociateAssetError.NotEnoughBalance -> {
|
||||
showErrorDialog(
|
||||
dialogFactory.showError(
|
||||
text =
|
||||
resourceReference(
|
||||
id = R.string.warning_hedera_token_association_not_enough_hbar_message,
|
||||
formatArgs = wrappedList(e.feeCurrency.symbol),
|
||||
|
|
@ -1123,7 +1135,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
is AssociateAssetError.DataError -> {
|
||||
showErrorDialog(stringReference(e.message.orEmpty()))
|
||||
dialogFactory.showError(text = stringReference(e.message.orEmpty()))
|
||||
TangemLogger.e("Error: $e")
|
||||
}
|
||||
}
|
||||
|
|
@ -1138,7 +1150,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onConfirmDisposeExpressStatus() {
|
||||
showConfirmHideExpressStatusDialog()
|
||||
dialogFactory.showConfirmHideExpressStatus(onConfirm = ::onDisposeExpressStatus)
|
||||
}
|
||||
|
||||
override fun onDisposeExpressStatus() {
|
||||
|
|
@ -1172,7 +1184,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
|
||||
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false
|
||||
|
||||
showErrorDialog(unavailabilityReason.getUnavailabilityReasonText())
|
||||
dialogFactory.showError(text = unavailabilityReason.getUnavailabilityReasonText())
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -1201,42 +1213,6 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title)))
|
||||
}
|
||||
|
||||
private fun showConfirmHideTokenDialog(currency: CryptoCurrency) {
|
||||
dialogFactory.showConfirmHideToken(currency = currency, onConfirm = ::onHideConfirmed)
|
||||
}
|
||||
|
||||
private fun showLinkedTokensDialog(currency: CryptoCurrency) {
|
||||
dialogFactory.showLinkedTokens(currency = currency)
|
||||
}
|
||||
|
||||
private fun showDismissIncompleteTransactionConfirmDialog() {
|
||||
dialogFactory.showDismissIncompleteTransactionConfirm(
|
||||
onConfirm = ::onConfirmDismissIncompleteTransactionClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showConfirmHideExpressStatusDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.express_status_hide_dialog_title),
|
||||
message = resourceReference(R.string.express_status_hide_dialog_text),
|
||||
firstActionBuilder = {
|
||||
EventMessageAction(
|
||||
title = resourceReference(R.string.common_hide),
|
||||
onClick = {
|
||||
onDisposeExpressStatus()
|
||||
},
|
||||
)
|
||||
},
|
||||
secondActionBuilder = { cancelAction() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showErrorDialog(text: TextReference) {
|
||||
dialogFactory.showError(text = text)
|
||||
}
|
||||
|
||||
private fun checkForActionUpdates() {
|
||||
combine(
|
||||
tokenDetailsDeepLinkActionListener.tokenDetailsActionFlow,
|
||||
|
|
@ -1424,6 +1400,50 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
observeRedesignBalance()
|
||||
updateRedesignTopBarMenu()
|
||||
observeRedesignTopBarTitle()
|
||||
observeRedesignStakingNotification()
|
||||
}
|
||||
|
||||
private fun observeRedesignStakingNotification() {
|
||||
val statusFlow = getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
|
||||
.map { it.status }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val availabilityFlow = getStakingAvailabilityUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
)
|
||||
.map { it.getOrElse { StakingAvailability.Unavailable } }
|
||||
.distinctUntilChanged()
|
||||
|
||||
val entryInfoFlow = availabilityFlow.mapLatest { availability ->
|
||||
(availability as? StakingAvailability.Available)?.let { available ->
|
||||
getStakingEntryInfoUseCase(
|
||||
cryptoCurrencyId = cryptoCurrency.id,
|
||||
symbol = cryptoCurrency.symbol,
|
||||
stakingOption = available.option,
|
||||
).getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
combine(
|
||||
flow = statusFlow,
|
||||
flow2 = availabilityFlow,
|
||||
flow3 = entryInfoFlow,
|
||||
flow4 = selectedAppCurrencyFlow,
|
||||
) { status, availability, entryInfo, appCurrency ->
|
||||
redesignStateController.update(
|
||||
UpdateStakingNotificationTransformer(
|
||||
cryptoCurrencyStatus = status,
|
||||
stakingAvailability = availability,
|
||||
stakingEntryInfo = entryInfo,
|
||||
appCurrency = appCurrency,
|
||||
clickIntents = this,
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
.saveIn(redesignEarnJobHolder)
|
||||
}
|
||||
|
||||
private fun initRedesignState() {
|
||||
|
|
|
|||
|
|
@ -61,8 +61,9 @@ internal class TokenDetailsStateController @Inject constructor() {
|
|||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = CurrencyIconState.Loading,
|
||||
),
|
||||
notifications = persistentListOf(),
|
||||
earnBlockState = null,
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = ""),
|
||||
stakingBlocksState = null,
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
onRefresh = {},
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import com.tangem.common.ui.account.AccountIconUM
|
|||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.common.ui.earn.EarnBlockUM
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.ds.message.TangemMessageUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -13,8 +15,9 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
internal data class TokenDetailsUM(
|
||||
val topAppBarUM: TokenDetailsTopAppBarUM,
|
||||
val balanceBlockUM: TokenDetailsBalanceBlockUM,
|
||||
val notifications: ImmutableList<TangemMessageUM>,
|
||||
val earnBlockState: EarnBlockUM?,
|
||||
val marketPriceBlockState: MarketPriceBlockState,
|
||||
val stakingBlocksState: StakingBlockUM?,
|
||||
val pullToRefreshConfig: PullToRefreshConfig,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isMarketPriceAvailable: Boolean,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
|
||||
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds.message.TangemMessageButtonUM
|
||||
import com.tangem.core.ui.ds.message.TangemMessageEffect
|
||||
import com.tangem.core.ui.ds.message.TangemMessageUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.domain.tokens.model.warnings.HederaWarnings
|
||||
import com.tangem.domain.tokens.model.warnings.KaspaWarnings
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
internal class UpdateNotificationsTransformer(
|
||||
private val warnings: Set<CryptoCurrencyWarning>,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
|
||||
val notifications = warnings.mapNotNull(::mapWarning).toImmutableList()
|
||||
return prevState.copy(notifications = notifications)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun mapWarning(warning: CryptoCurrencyWarning): TangemMessageUM? {
|
||||
return when (warning) {
|
||||
is CryptoCurrencyWarning.SomeNetworksUnreachable -> TangemMessageUM(
|
||||
id = "networks_unreachable",
|
||||
title = resourceReference(CoreResR.string.warning_network_unreachable_title),
|
||||
subtitle = resourceReference(CoreResR.string.warning_network_unreachable_message),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
)
|
||||
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> createFeeWarning(
|
||||
FeeWarningParams(
|
||||
id = "balance_not_enough_for_fee",
|
||||
currency = warning.tokenCurrency,
|
||||
networkName = warning.coinCurrency.network.name,
|
||||
feeCurrencyName = warning.coinCurrency.name,
|
||||
feeCurrencySymbol = warning.coinCurrency.symbol,
|
||||
buyCurrency = warning.coinCurrency,
|
||||
),
|
||||
)
|
||||
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> createFeeWarning(
|
||||
FeeWarningParams(
|
||||
id = "custom_token_not_enough_for_fee",
|
||||
currency = warning.currency,
|
||||
networkName = warning.feeCurrency?.network?.name ?: warning.networkName,
|
||||
feeCurrencyName = warning.feeCurrencyName,
|
||||
feeCurrencySymbol = warning.feeCurrencySymbol,
|
||||
buyCurrency = warning.feeCurrency,
|
||||
),
|
||||
)
|
||||
is CryptoCurrencyWarning.BeaconChainShutdown -> TangemMessageUM(
|
||||
id = "beacon_chain_shutdown",
|
||||
title = resourceReference(CoreResR.string.warning_beacon_chain_retirement_title),
|
||||
subtitle = resourceReference(CoreResR.string.warning_beacon_chain_retirement_content),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
)
|
||||
is HederaWarnings.AssociateWarning -> TangemMessageUM(
|
||||
id = "hedera_associate",
|
||||
title = resourceReference(CoreResR.string.warning_hedera_missing_token_association_title),
|
||||
subtitle = resourceReference(CoreResR.string.warning_hedera_missing_token_association_message_brief),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(CoreResR.string.warning_hedera_missing_token_association_button_title),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = clickIntents::onAssociateClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
is HederaWarnings.AssociateWarningWithFee -> TangemMessageUM(
|
||||
id = "hedera_associate_fee",
|
||||
title = resourceReference(CoreResR.string.warning_hedera_missing_token_association_title),
|
||||
subtitle = resourceReference(
|
||||
CoreResR.string.warning_hedera_missing_token_association_message,
|
||||
wrappedList(
|
||||
warning.fee.format { crypto(symbol = "", decimals = warning.feeCurrencyDecimals) },
|
||||
warning.feeCurrencySymbol,
|
||||
),
|
||||
),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(CoreResR.string.warning_hedera_missing_token_association_button_title),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = clickIntents::onAssociateClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
is CryptoCurrencyWarning.RequiredTrustline -> TangemMessageUM(
|
||||
id = "required_trustline",
|
||||
title = resourceReference(CoreResR.string.warning_token_trustline_title),
|
||||
subtitle = resourceReference(
|
||||
CoreResR.string.warning_token_trustline_subtitle,
|
||||
wrappedList(
|
||||
warning.requiredAmount.format { crypto(symbol = "", warning.currencyDecimals) }.trim(),
|
||||
warning.currencySymbol,
|
||||
),
|
||||
),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(CoreResR.string.warning_token_trustline_button_title),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = clickIntents::onOpenTrustlineClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
is KaspaWarnings.IncompleteTransaction -> TangemMessageUM(
|
||||
id = "kaspa_incomplete",
|
||||
title = resourceReference(CoreResR.string.warning_kaspa_unfinished_token_transaction_title),
|
||||
subtitle = resourceReference(
|
||||
CoreResR.string.warning_kaspa_unfinished_token_transaction_message,
|
||||
wrappedList(
|
||||
warning.amount.format { crypto(symbol = "", decimals = warning.currencyDecimals) },
|
||||
warning.currencySymbol,
|
||||
),
|
||||
),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(CoreResR.string.alert_button_try_again),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = clickIntents::onRetryIncompleteTransactionClick,
|
||||
),
|
||||
),
|
||||
onCloseClick = clickIntents::onDismissIncompleteTransactionClick,
|
||||
)
|
||||
is CryptoCurrencyWarning.MigrationMaticToPol -> TangemMessageUM(
|
||||
id = "migration_matic_pol",
|
||||
title = resourceReference(CoreResR.string.warning_matic_migration_title),
|
||||
subtitle = resourceReference(CoreResR.string.warning_matic_migration_message),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
)
|
||||
is CryptoCurrencyWarning.MigrationClore -> TangemMessageUM(
|
||||
id = "migration_clore",
|
||||
title = resourceReference(CoreResR.string.warning_clore_migration_title),
|
||||
subtitle = resourceReference(CoreResR.string.warning_clore_migration_description),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(CoreResR.string.warning_clore_migration_button),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = clickIntents::onCloreMigrationClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
// Non-warning types — skip for redesign
|
||||
is CryptoCurrencyWarning.ExistentialDeposit,
|
||||
is CryptoCurrencyWarning.Rent,
|
||||
is CryptoCurrencyWarning.SomeNetworksNoAccount,
|
||||
is CryptoCurrencyWarning.TopUpWithoutReserve,
|
||||
is CryptoCurrencyWarning.SwapPromo,
|
||||
is CryptoCurrencyWarning.FeeResourceInfo,
|
||||
is CryptoCurrencyWarning.UsedOutdatedDataWarning,
|
||||
-> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFeeWarning(params: FeeWarningParams): TangemMessageUM {
|
||||
val buttons = if (params.buyCurrency != null) {
|
||||
persistentListOf(
|
||||
TangemMessageButtonUM(
|
||||
text = resourceReference(
|
||||
CoreResR.string.common_buy_currency,
|
||||
wrappedList(params.feeCurrencySymbol),
|
||||
),
|
||||
type = TangemButtonType.Primary,
|
||||
onClick = { clickIntents.onBuyCoinClick(params.buyCurrency) },
|
||||
),
|
||||
)
|
||||
} else {
|
||||
persistentListOf()
|
||||
}
|
||||
|
||||
return TangemMessageUM(
|
||||
id = params.id,
|
||||
title = resourceReference(
|
||||
CoreResR.string.warning_send_blocked_funds_for_fee_title,
|
||||
wrappedList(params.feeCurrencyName),
|
||||
),
|
||||
subtitle = resourceReference(
|
||||
CoreResR.string.warning_send_blocked_funds_for_fee_message,
|
||||
wrappedList(
|
||||
params.currency.name,
|
||||
params.networkName,
|
||||
params.currency.name,
|
||||
params.feeCurrencyName,
|
||||
params.feeCurrencySymbol,
|
||||
),
|
||||
),
|
||||
messageEffect = TangemMessageEffect.None,
|
||||
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
|
||||
buttonsUM = buttons,
|
||||
)
|
||||
}
|
||||
|
||||
private data class FeeWarningParams(
|
||||
val id: String,
|
||||
val currency: CryptoCurrency,
|
||||
val networkName: String,
|
||||
val feeCurrencyName: String,
|
||||
val feeCurrencySymbol: String,
|
||||
val buyCurrency: CryptoCurrency?,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
|
||||
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import com.tangem.common.getRewardStakingBalance
|
||||
import com.tangem.common.getTotalStakingBalance
|
||||
import com.tangem.common.ui.earn.EarnBlockUM
|
||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
||||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
||||
import com.tangem.core.ui.ds.button.TangemButtonType
|
||||
import com.tangem.core.ui.ds.button.TangemButtonUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.defaultAmount
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.formatStyled
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.staking.RewardBlockType
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.StakingOption
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
|
||||
import com.tangem.features.tokendetails.impl.R
|
||||
import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable
|
||||
import com.tangem.utils.isNullOrZero
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import java.math.BigDecimal
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
import com.tangem.core.ui.R as CoreUiR
|
||||
|
||||
internal class UpdateStakingNotificationTransformer(
|
||||
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val stakingAvailability: StakingAvailability,
|
||||
private val stakingEntryInfo: StakingEntryInfo?,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val clickIntents: TokenDetailsClickIntents,
|
||||
) : Transformer<TokenDetailsUM> {
|
||||
|
||||
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
|
||||
return prevState.copy(earnBlockState = buildEarnBlock(prevState.isBalanceHidden))
|
||||
}
|
||||
|
||||
private fun buildEarnBlock(isBalanceHidden: Boolean): EarnBlockUM? {
|
||||
return when (val availability = stakingAvailability) {
|
||||
StakingAvailability.TemporaryUnavailable -> buildTemporaryUnavailable()
|
||||
StakingAvailability.Unavailable -> null
|
||||
is StakingAvailability.Available -> getStakingInfoBlock(availability, isBalanceHidden)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildTemporaryUnavailable(): EarnBlockUM.Content {
|
||||
return EarnBlockUM.Content(
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.staking_native),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
color = { TangemTheme.colors2.text.neutral.tertiary },
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
color = { TangemTheme.colors2.text.neutral.tertiary },
|
||||
),
|
||||
trailingUM = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStakingInfoBlock(
|
||||
availability: StakingAvailability.Available,
|
||||
isBalanceHidden: Boolean,
|
||||
): EarnBlockUM? {
|
||||
val status = cryptoCurrencyStatus
|
||||
val stakingBalance = status.value.stakingBalance as? StakingBalance.Data
|
||||
val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId)
|
||||
|
||||
return when {
|
||||
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
|
||||
val hasPendingBalances = stakingBalance.hasPendingBalances()
|
||||
if (!hasPendingBalances) {
|
||||
buildStakeAvailable(
|
||||
availability = availability,
|
||||
isEnabled = isStakingButtonEnabled(status),
|
||||
)
|
||||
} else {
|
||||
buildActiveBlock(
|
||||
stakingAmount = stakingBalance.getPendingAmount(),
|
||||
rewardAmount = null,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> null
|
||||
else -> buildActiveBlock(
|
||||
stakingAmount = stakingCryptoAmount,
|
||||
rewardAmount = stakingBalance.getRewardAmount(),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
|
||||
return status.value is CryptoCurrencyStatus.Loaded ||
|
||||
status.value is CryptoCurrencyStatus.NoQuote ||
|
||||
status.value is CryptoCurrencyStatus.Custom
|
||||
}
|
||||
|
||||
private fun buildStakeAvailable(
|
||||
availability: StakingAvailability.Available,
|
||||
isEnabled: Boolean,
|
||||
): EarnBlockUM.Content {
|
||||
return EarnBlockUM.Content(
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Tinted { TangemTheme.colors2.markers.backgroundTintedBlue },
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(
|
||||
iconRes = CoreUiR.drawable.ic_staking_40,
|
||||
glowColor = { TangemTheme.colors2.border.status.accent },
|
||||
),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(id = R.string.token_details_staking_block_title),
|
||||
style = EarnBlockUM.TitleUM.Style.Small,
|
||||
color = { TangemTheme.colors2.text.status.accent },
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = stakeAvailableSubtitle(availability.option.displayApy),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Large,
|
||||
color = { TangemTheme.colors2.text.neutral.primary },
|
||||
),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Button(
|
||||
buttonUM = TangemButtonUM(
|
||||
text = resourceReference(R.string.common_stake),
|
||||
type = TangemButtonType.Accent,
|
||||
size = TangemButtonSize.X9,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
isEnabled = isEnabled,
|
||||
onClick = clickIntents::onStakeBannerClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun stakeAvailableSubtitle(apy: BigDecimal?): TextReference {
|
||||
return if (apy != null) {
|
||||
resourceReference(
|
||||
CoreResR.string.token_details_earn_staking_subtitle,
|
||||
wrappedList(apy.format { percent() }),
|
||||
)
|
||||
} else {
|
||||
resourceReference(CoreResR.string.staking_notification_earn_rewards_text)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildActiveBlock(
|
||||
stakingAmount: BigDecimal?,
|
||||
rewardAmount: BigDecimal?,
|
||||
isBalanceHidden: Boolean,
|
||||
): EarnBlockUM.Content {
|
||||
val status = cryptoCurrencyStatus
|
||||
val fiatRate = status.value.fiatRate
|
||||
val fiatAmount = stakingAmount?.let { fiatRate?.multiply(it) }
|
||||
val rewardFiatAmount = rewardAmount?.let { fiatRate?.multiply(it) }
|
||||
return EarnBlockUM.Content(
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Glowing(
|
||||
iconRes = CoreUiR.drawable.ic_staking_40,
|
||||
glowColor = { TangemTheme.colors2.border.status.accent },
|
||||
),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.staking_native),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
color = { TangemTheme.colors2.text.neutral.primary },
|
||||
),
|
||||
subtitleUM = getRewardSubtitle(status, rewardFiatAmount),
|
||||
trailingUM = EarnBlockUM.TrailingUM.Balance(
|
||||
fiatValue = fiatAmount.formatStyled {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
).defaultAmount(
|
||||
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
|
||||
)
|
||||
},
|
||||
cryptoValue = stringReference(
|
||||
stakingAmount.format {
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
},
|
||||
),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
onClick = clickIntents::onStakeBannerClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRewardSubtitle(
|
||||
status: CryptoCurrencyStatus,
|
||||
stakingRewardAmount: BigDecimal?,
|
||||
): EarnBlockUM.SubtitleUM? {
|
||||
val blockchainId = status.currency.network.rawId
|
||||
val isCoin = status.currency.id.isCoin
|
||||
val stakingBalance = status.value.stakingBalance
|
||||
|
||||
val rewardBlockType = when {
|
||||
stakingBalance is StakingBalance.Data.P2PEthPool -> {
|
||||
if (stakingBalance.totalRewards.isNullOrZero()) {
|
||||
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable
|
||||
} else {
|
||||
RewardBlockType.EthereumEarnedRewards
|
||||
}
|
||||
}
|
||||
isStakingRewardUnavailable(blockchainId, isCoin) -> {
|
||||
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable
|
||||
}
|
||||
stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards
|
||||
else -> RewardBlockType.Rewards
|
||||
}
|
||||
|
||||
val text = when (rewardBlockType) {
|
||||
RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim)
|
||||
RewardBlockType.CardanoNoRewards -> resourceReference(R.string.staking_cardano_details_rewards_info_text)
|
||||
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable,
|
||||
RewardBlockType.RewardUnavailable.SolanaRewardUnavailable,
|
||||
-> return null
|
||||
RewardBlockType.EthereumEarnedRewards -> {
|
||||
val cryptoRewardAmount = (stakingBalance as? StakingBalance.Data.P2PEthPool)?.totalRewards
|
||||
resourceReference(
|
||||
R.string.staking_details_autocompound_rewards_earned,
|
||||
wrappedList(
|
||||
cryptoRewardAmount.format {
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
RewardBlockType.RewardsRequirementsError,
|
||||
RewardBlockType.Rewards,
|
||||
-> resourceReference(
|
||||
R.string.staking_details_rewards_to_claim,
|
||||
wrappedList(
|
||||
stakingRewardAmount.format { fiat(appCurrency.code, appCurrency.symbol) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val isAccent = rewardBlockType == RewardBlockType.Rewards ||
|
||||
rewardBlockType == RewardBlockType.RewardsRequirementsError ||
|
||||
rewardBlockType == RewardBlockType.EthereumEarnedRewards
|
||||
|
||||
return EarnBlockUM.SubtitleUM.Text(
|
||||
text = text,
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
color = if (isAccent) {
|
||||
{ TangemTheme.colors2.text.status.accent }
|
||||
} else {
|
||||
{ TangemTheme.colors2.text.neutral.tertiary }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun StakingBalance.Data?.hasPendingBalances(): Boolean = when (this) {
|
||||
is StakingBalance.Data.StakeKit -> balance.items.isNotEmpty()
|
||||
is StakingBalance.Data.P2PEthPool -> !unstakingAmount.isNullOrZero()
|
||||
null -> false
|
||||
}
|
||||
|
||||
private fun StakingBalance.Data?.getPendingAmount(): BigDecimal = when (this) {
|
||||
is StakingBalance.Data.StakeKit -> balance.items.sumOf { it.amount }
|
||||
is StakingBalance.Data.P2PEthPool -> unstakingAmount
|
||||
null -> BigDecimal.ZERO
|
||||
}
|
||||
|
||||
private fun StakingBalance.Data?.getRewardAmount(): BigDecimal = when (this) {
|
||||
is StakingBalance.Data.StakeKit -> getRewardStakingBalance()
|
||||
is StakingBalance.Data.P2PEthPool -> totalRewards
|
||||
null -> BigDecimal.ZERO
|
||||
}
|
||||
|
||||
private val StakingOption.displayApy: BigDecimal?
|
||||
get() = when (this) {
|
||||
is StakingOption.StakeKit -> yield.preferredValidators
|
||||
.mapNotNull { it.rewardInfo?.rate }
|
||||
.maxOrNull()
|
||||
is StakingOption.P2PEthPool -> apy
|
||||
}
|
||||
|
|
@ -10,10 +10,13 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import com.tangem.common.ui.earn.EarnBlock
|
||||
import com.tangem.common.ui.notifications.notifications
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
|
|
@ -59,6 +62,11 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
|
|||
partialCollapsedHeight = partialCollapsedHeight,
|
||||
)
|
||||
|
||||
val rootBackground by LocalRootBackgroundColor.current
|
||||
val notificationModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens2.x4)
|
||||
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
|
|
@ -79,18 +87,18 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
|
|||
)
|
||||
},
|
||||
body = {
|
||||
LazyColumn(
|
||||
TokenDetailsBody(
|
||||
tokenDetailsUM = tokenDetailsUM,
|
||||
rootBackground = rootBackground,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(behavior.nestedScrollConnection),
|
||||
) {
|
||||
// TODO [REDACTED_TASK_KEY] Token Details Make Transaction History
|
||||
}
|
||||
itemModifier = notificationModifier,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val rootBackground by LocalRootBackgroundColor.current
|
||||
val hazeIntensity by animateFloatAsState(
|
||||
targetValue = (behavior.state.collapsedFraction * 2f).coerceIn(0f, 1f),
|
||||
label = "TopBarHazeIntensity",
|
||||
|
|
@ -110,6 +118,31 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenDetailsBody(
|
||||
tokenDetailsUM: TokenDetailsUM,
|
||||
rootBackground: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
itemModifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
notifications(
|
||||
notifications = tokenDetailsUM.notifications,
|
||||
contentColor = rootBackground,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
tokenDetailsUM.earnBlockState?.let { earnBlock ->
|
||||
item(key = "staking_block") {
|
||||
EarnBlock(
|
||||
state = earnBlock,
|
||||
modifier = itemModifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO [REDACTED_TASK_KEY] Token Details Make Transaction History
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
|
|
@ -137,13 +170,14 @@ private fun TokenDetailsScreen_Preview() {
|
|||
),
|
||||
),
|
||||
),
|
||||
notifications = persistentListOf(),
|
||||
earnBlockState = null,
|
||||
balanceBlockUM = TokenDetailsBalanceBlockUM.Loading(
|
||||
actionButtons = persistentListOf(),
|
||||
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
|
||||
currencyIconState = CurrencyIconState.Loading,
|
||||
),
|
||||
marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = "USDT"),
|
||||
stakingBlocksState = null,
|
||||
pullToRefreshConfig = PullToRefreshConfig(
|
||||
isRefreshing = false,
|
||||
onRefresh = {},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlockLegacy
|
||||
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
|
||||
import com.tangem.features.txhistory.component.TxHistoryComponent
|
||||
import com.tangem.features.txhistory.entity.TxHistoryUM
|
||||
|
|
@ -133,7 +133,7 @@ internal fun TokenDetailsScreenLegacy(
|
|||
key = StakingBlockUM::class.java,
|
||||
contentType = StakingBlockUM::class.java,
|
||||
content = {
|
||||
TokenStakingBlock(
|
||||
TokenStakingBlockLegacy(
|
||||
state = state.stakingBlocksState,
|
||||
isBalanceHidden = state.isBalanceHidden,
|
||||
modifier = itemModifier,
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import com.tangem.features.tokendetails.impl.R
|
|||
* @param modifier modifier
|
||||
*/
|
||||
@Composable
|
||||
internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
internal fun TokenStakingBlockLegacy(state: StakingBlockUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
|
|
@ -60,16 +60,16 @@ internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean,
|
|||
targetState = state,
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
label = "Staking block animation",
|
||||
) {
|
||||
when (it) {
|
||||
) { stakingBlock ->
|
||||
when (stakingBlock) {
|
||||
is StakingBlockUM.TemporaryUnavailable -> StakingTemporaryUnavailableBlock()
|
||||
is StakingBlockUM.Loading -> StakingLoading()
|
||||
is StakingBlockUM.Staked -> StakingBalanceBlock(
|
||||
state = it,
|
||||
state = stakingBlock,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
is StakingBlockUM.StakeAvailable -> StakingAvailableContent(
|
||||
state = it,
|
||||
state = stakingBlock,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -165,7 +165,7 @@ private fun Preview_TokenStakingBlock(
|
|||
state: StakingBlockUM,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokenStakingBlock(
|
||||
TokenStakingBlockLegacy(
|
||||
state = state,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue