Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-02 14:11:30 +03:00
commit 45156f05b0
1184 changed files with 47887 additions and 11982 deletions

View file

@ -19,7 +19,6 @@ import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.quote.PriceChange
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
class AccountCryptoPortfolioItemStateConverter(
@ -41,14 +40,11 @@ class AccountCryptoPortfolioItemStateConverter(
private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content {
val subtitle2State = when (fiatBalance.amount.isZero()) {
true -> null
false -> priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
}
val subtitle2State = priceChangeLce?.fold(
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
ifError = { null },
ifContent = { priceChange -> priceChange.toSubtitle2State() },
)
return TokenItemState.Content(
id = account.accountId.toItemId(),
iconState = AccountIconItemStateConverter.convert(this),
@ -120,7 +116,7 @@ class AccountCryptoPortfolioItemStateConverter(
)
}
private fun AccountId.toItemId() = this.userWalletId.stringValue + this.value
private fun AccountId.toItemId() = this.value
private fun BigDecimal.getPriceChangeType(): PriceChangeType = PriceChangeConverter.fromBigDecimal(value = this)

View file

@ -6,6 +6,7 @@ import com.tangem.common.ui.R
import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.account.PaymentAccountIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.domain.models.account.CryptoPortfolioIcon
@ -16,7 +17,7 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
* - a single character (derived from the [name]) using [AccountCharIcon], or
* - a predefined vector resource (from [icon]) using [AccountResIcon].
*
* The background color is taken from [CryptoPortfolioIconUM.color],
* The background color is taken from [AccountIconUM.CryptoPortfolio.color],
* and the icon size, text style, and container shape are adapted based on the given [size].
*
* @param name A [TextReference] used to resolve and display the first letter
@ -31,29 +32,43 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
@Composable
fun AccountIcon(
name: TextReference,
icon: CryptoPortfolioIconUM,
icon: AccountIconUM.CryptoPortfolio,
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val letter = name.resolveReference().firstOrNull()
val iconColor = icon.color.getUiColor()
when {
icon.value == CryptoPortfolioIcon.Icon.Letter && letter == null -> {
AccountResIcon(
resId = R.drawable.ic_tangem_24,
color = icon.color.getUiColor(),
color = iconColor,
size = size,
modifier = modifier,
)
}
icon.value == CryptoPortfolioIcon.Icon.Letter -> AccountCharIcon(
char = letter ?: 'N',
color = icon.color.getUiColor(),
color = iconColor,
size = size,
modifier = modifier,
)
else -> AccountResIcon(
resId = icon.value.getResId(),
color = icon.color.getUiColor(),
color = iconColor,
size = size,
modifier = modifier,
)
}
}
@Composable
fun AccountIcon(name: TextReference, icon: AccountIconUM, size: AccountIconSize, modifier: Modifier = Modifier) {
when (icon) {
is AccountIconUM.Payment -> PaymentAccountIcon(size = size, modifier = modifier)
is AccountIconUM.CryptoPortfolio -> AccountIcon(
name = name,
icon = icon,
size = size,
modifier = modifier,
)
@ -62,7 +77,7 @@ fun AccountIcon(
object AccountIconPreviewData {
fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM(
fun randomAccountIcon(letter: Boolean = false) = AccountIconUM.CryptoPortfolio(
value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(),
color = Color.entries.random(),
)

View file

@ -20,5 +20,6 @@ object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.Cryp
isGrayscale = false,
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
sealed class AccountIconUM {
data class CryptoPortfolio(val value: Icon, val color: Color) : AccountIconUM()
data object Payment : AccountIconUM()
}

View file

@ -27,7 +27,7 @@ import com.tangem.core.ui.res.TangemTheme
@Composable
fun AccountLabel(
name: TextReference,
icon: CryptoPortfolioIconUM,
icon: AccountIconUM,
iconSize: AccountIconSize,
modifier: Modifier = Modifier,
nameStyle: TextStyle = TangemTheme.typography.subtitle2,

View file

@ -22,21 +22,24 @@ class AccountPortfolioItemUMConverter(
) : Converter<Account, UserWalletItemUM> {
override fun convert(value: Account): UserWalletItemUM {
return with(value) {
UserWalletItemUM(
id = accountId.value,
name = accountName.toUM().value,
information = getInfo(account = this),
balance = getBalanceInfo(),
isEnabled = isEnabled,
endIcon = endIcon,
onClick = onClick,
imageState = getImageState(account = this),
)
return when (value) {
is Account.CryptoPortfolio -> with(value) {
UserWalletItemUM(
id = accountId.value,
name = accountName.toUM().value,
information = getInfo(account = this),
balance = getBalanceInfo(),
isEnabled = isEnabled,
endIcon = endIcon,
onClick = onClick,
imageState = getImageState(account = this),
)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
}
private fun getInfo(account: Account): UserWalletItemUM.Information.Loaded = when (account) {
private fun getInfo(account: Account.CryptoPortfolio): UserWalletItemUM.Information.Loaded = when (account) {
is Account.CryptoPortfolio -> {
val text = pluralReference(
R.plurals.common_tokens_count,
@ -45,13 +48,15 @@ class AccountPortfolioItemUMConverter(
)
UserWalletItemUM.Information.Loaded(text)
}
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getImageState(account: Account) = when (account) {
private fun getImageState(account: Account.CryptoPortfolio) = when (account) {
is Account.CryptoPortfolio -> UserWalletItemUM.ImageState.Account(
name = account.accountName.toUM().value,
icon = account.icon.toUM(),
icon = CryptoPortfolioIconConverter.convert(account.icon),
)
is Account.Payment -> TODO("[REDACTED_JIRA]")
}
private fun getBalanceInfo(): UserWalletItemUM.Balance {

View file

@ -41,7 +41,7 @@ import com.tangem.core.ui.res.TangemThemePreview
fun AccountRow(
title: TextReference,
subtitle: TextReference,
icon: CryptoPortfolioIconUM,
icon: AccountIconUM.CryptoPortfolio,
modifier: Modifier = Modifier,
isReverse: Boolean = false,
) {

View file

@ -1,7 +1,9 @@
package com.tangem.common.ui.account
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -9,11 +11,16 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendScreenTestTags
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.StringsSigns
/**
* A composable function that displays an account label (icon + name) with an optional prefix.
@ -61,4 +68,28 @@ fun AccountTitle(
)
}
}
}
@Preview
@Composable
private fun PreviewAccountTitle() {
TangemThemePreview {
Column {
AccountTitle(
accountTitleUM = AccountTitleUM.Account(
prefixText = stringReference(StringsSigns.DOT),
name = stringReference("Main Wallet"),
icon = AccountIconUM.CryptoPortfolio(
value = CryptoPortfolioIcon.Icon.Bookmark,
color = CryptoPortfolioIcon.Color.Pattypan,
),
),
modifier = Modifier.padding(4.dp),
)
AccountTitle(
accountTitleUM = AccountTitleUM.Account.payment(prefixText = stringReference(StringsSigns.DOT)),
modifier = Modifier.padding(4.dp),
)
}
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.common.ui.account
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
/**
* A sealed interface representing the title of an account, which can be either a simple text
@ -19,6 +21,16 @@ sealed interface AccountTitleUM {
data class Account(
val prefixText: TextReference,
val name: TextReference,
val icon: CryptoPortfolioIconUM,
) : AccountTitleUM
val icon: AccountIconUM,
) : AccountTitleUM {
companion object {
fun payment(prefixText: TextReference = TextReference.EMPTY): Account {
return Account(
prefixText = prefixText,
name = resourceReference(R.string.tangempay_payment_account),
icon = AccountIconUM.Payment,
)
}
}
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.converter.TwoWayConverter
object CryptoPortfolioIconConverter : TwoWayConverter<CryptoPortfolioIcon, AccountIconUM.CryptoPortfolio> {
override fun convert(value: CryptoPortfolioIcon): AccountIconUM.CryptoPortfolio {
return AccountIconUM.CryptoPortfolio(value = value.value, color = value.color)
}
override fun convertBack(value: AccountIconUM.CryptoPortfolio): CryptoPortfolioIcon {
return CryptoPortfolioIcon.ofCustomAccount(value = value.value, color = value.color)
}
}

View file

@ -47,7 +47,4 @@ fun CryptoPortfolioIcon.Icon.getResId(): Int {
CryptoPortfolioIcon.Icon.Package -> R.drawable.ic_package_24
CryptoPortfolioIcon.Icon.Gift -> R.drawable.ic_gift_24
}
}
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(domainModel = this)
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(value = this.value, color = this.color)
}

View file

@ -1,15 +0,0 @@
package com.tangem.common.ui.account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
data class CryptoPortfolioIconUM(
val value: Icon,
val color: Color,
) {
constructor(domainModel: CryptoPortfolioIcon) : this(
value = domainModel.value,
color = domainModel.color,
)
}

View file

@ -82,7 +82,7 @@ fun PortfolioSelectRow(
@Immutable
data class PortfolioSelectUM(
val icon: CryptoPortfolioIconUM?,
val icon: AccountIconUM.CryptoPortfolio?,
val name: TextReference,
val isAccountMode: Boolean,
val isMultiChoice: Boolean,

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
@ -15,7 +16,7 @@ class AmountAccountConverter(
return if (value != null && isAccountsMode) {
AccountTitleUM.Account(
name = value.accountName.toUM().value,
icon = value.icon.toUM(),
icon = CryptoPortfolioIconConverter.convert(value.icon),
prefixText = prefixText,
)
} else {

View file

@ -5,7 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import com.tangem.common.ui.R
import com.tangem.common.ui.account.AccountNameUM
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
@ -88,7 +88,7 @@ object AmountStatePreviewData {
val amountStateV2Accounts = amountState.copy(
accountTitleUM = AccountTitleUM.Account(
name = AccountNameUM.DefaultMain.value,
icon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM(),
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
prefixText = resourceReference(R.string.common_from),
),
availableBalanceCrypto = stringReference("2 130,81231238 USDT"),

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -19,7 +20,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.format.bigdecimal.crypto
@ -66,11 +66,14 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
state = amountState.tokenIconState,
iconSize = 40.dp,
)
ResizableText(
Text(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
autoSize = TextAutoSize.StepBased(
maxFontSize = TangemTheme.typography.h2.fontSize,
),
maxLines = 1,
modifier = Modifier
.fillMaxWidth()

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -18,7 +19,6 @@ import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
@ -128,21 +128,23 @@ private fun AmountBlockV2(
.padding(top = 8.dp)
.weight(1f),
) {
ResizableText(
Text(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.h2.fontSize),
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
ResizableText(
Text(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.body2.fontSize),
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
)
extraContent()

View file

@ -1,15 +1,23 @@
package com.tangem.common.ui.bottomsheet.chooseaddress
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SimpleSettingsRow
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
@Composable
fun ChooseAddressBottomSheet(config: TangemBottomSheetConfig) {
@ -31,4 +39,40 @@ private fun ChooseAddressBottomSheetContent(content: ChooseAddressBottomSheetCon
)
}
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun ChooseAddressBottomSheet_Preview(
@PreviewParameter(ChooseAddressBottomSheetPreviewProvider::class) params: ChooseAddressBottomSheetConfig,
) {
TangemThemePreview {
ChooseAddressBottomSheetContent(params)
}
}
private class ChooseAddressBottomSheetPreviewProvider : PreviewParameterProvider<ChooseAddressBottomSheetConfig> {
override val values: Sequence<ChooseAddressBottomSheetConfig>
get() = sequenceOf(
ChooseAddressBottomSheetConfig(
addressModels = persistentListOf(
AddressModel(
displayName = stringReference("Main address"),
fullName = stringReference("Bitcoin"),
value = "0x1234...abcd",
type = AddressModel.Type.Default,
),
AddressModel(
displayName = stringReference("Legacy address"),
fullName = stringReference("Bitcoin"),
value = "0x1234...abcd",
type = AddressModel.Type.Default,
),
),
onClick = {},
),
)
}
// endregion

View file

@ -1,10 +1,9 @@
package com.tangem.common.ui.bottomsheet.chooseaddress
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -14,13 +13,12 @@ class ChooseAddressBottomSheetConfig(
val onClick: (AddressModel) -> Unit,
) : TangemBottomSheetConfigContent {
constructor(
asset: TokenReceiveBottomSheetConfig.Asset,
network: Network,
currency: CryptoCurrency,
networkAddress: NetworkAddress,
onClick: (AddressModel) -> Unit,
) : this(
addressModels = networkAddress.availableAddresses
.mapToAddressModels(asset, network)
.mapToAddressModels(cryptoCurrency = currency)
.toImmutableList(),
onClick = onClick,
)

View file

@ -1,9 +1,7 @@
package com.tangem.common.ui.bottomsheet.receive
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -20,20 +18,6 @@ fun Set<NetworkAddress.Address>.mapToAddressModels(cryptoCurrency: CryptoCurrenc
cryptoCurrency.network,
)
fun Set<NetworkAddress.Address>.mapToAddressModels(
asset: TokenReceiveBottomSheetConfig.Asset,
network: Network,
): List<AddressModel> = when (asset) {
is TokenReceiveBottomSheetConfig.Asset.Currency -> mapToAddressModels(
stringReference("${asset.name} (${asset.symbol})"),
network,
)
is TokenReceiveBottomSheetConfig.Asset.NFT -> mapToAddressModels(
resourceReference(R.string.common_nft),
network,
)
}
private fun Set<NetworkAddress.Address>.mapToAddressModels(name: TextReference, network: Network): List<AddressModel> =
this
.sortedBy { it.type.ordinal }

View file

@ -1,330 +0,0 @@
package com.tangem.common.ui.bottomsheet.receive
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.components.rememberQrPainters
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbarHost
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
@Composable
fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(config) { content: TokenReceiveBottomSheetConfig ->
TokenReceiveBottomSheetContent(content = content)
}
}
@Composable
private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) {
var selectedAddress by remember { mutableStateOf(content.addresses.first()) }
val snackbarHostState = remember(::SnackbarHostState)
ContainerWithSnackbarHost(snackbarHostState = snackbarHostState) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(top = 24.dp, bottom = 16.dp)
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 24.dp),
) {
QrCodeContent(content = content, onAddressChange = { selectedAddress = it })
Info(
showMemoDisclaimer = content.showMemoDisclaimer,
notifications = content.notifications,
)
Buttons(
snackbarHostState = snackbarHostState,
onShareClick = { content.onShareClick(selectedAddress.value) },
onCopyClick = { content.onCopyClick(selectedAddress.value) },
)
}
}
}
@Composable
private fun Info(
showMemoDisclaimer: Boolean,
notifications: ImmutableList<NotificationConfig>,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
if (showMemoDisclaimer) {
Text(
modifier = Modifier
.padding(horizontal = 18.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.receive_bottom_sheet_no_memo_required_message),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
notifications.fastForEach { notificationConfig ->
key(notificationConfig.hashCode()) {
Notification(config = notificationConfig)
}
}
}
}
@Composable
private fun ContainerWithSnackbarHost(snackbarHostState: SnackbarHostState, content: @Composable () -> Unit) {
Box {
content()
CopiedTextSnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
)
}
}
@Composable
private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) {
val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value))
val pagerState = rememberPagerState(
initialPage = 0,
initialPageOffsetFraction = 0f,
pageCount = content.addresses::count,
)
LaunchedEffect(key1 = pagerState.currentPage) {
onAddressChange.invoke(content.addresses[pagerState.currentPage])
}
HorizontalPager(state = pagerState) { currentPage ->
QrCodePage(
content = content,
qrCodePainter = qrCodes[currentPage],
currentIndex = currentPage,
)
}
if (pagerState.pageCount > 1) {
val indicatorState = rememberLazyListState()
val selectedColor = TangemTheme.colors.icon.primary1
val unselectedColor = TangemTheme.colors.icon.informative
LazyRow(
modifier = Modifier.height(20.dp),
state = indicatorState,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
repeat(pagerState.pageCount) { iteration ->
item(key = iteration) {
val color by animateColorAsState(
targetValue = if (pagerState.currentPage == iteration) selectedColor else unselectedColor,
label = "",
)
Box(
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 6.dp)
.background(color = color, shape = CircleShape)
.size(7.dp),
)
}
}
}
}
}
@Composable
private fun QrCodePage(content: TokenReceiveBottomSheetConfig, qrCodePainter: Painter, currentIndex: Int) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
text = stringResourceSafe(
R.string.receive_bottom_sheet_warning_message_compact,
content.addresses[currentIndex].fullName.resolveReference(),
content.network,
),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h3,
)
Image(
painter = qrCodePainter,
contentDescription = null,
contentScale = ContentScale.Fit,
modifier = Modifier.size(248.dp),
)
Text(
text = content.addresses[currentIndex].value,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.subtitle1,
)
}
}
@Composable
private fun Buttons(
snackbarHostState: SnackbarHostState,
onShareClick: () -> Unit,
onCopyClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val hapticFeedback = LocalHapticFeedback.current
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
val resources = context.resources
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResourceSafe(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
onCopyClick()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getStringSafe(R.string.wallet_notification_address_copied),
)
}
},
)
SecondaryButtonIconStart(
modifier = Modifier.weight(1f),
text = stringResourceSafe(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onShareClick()
},
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_TokenReceiveBottomSheet(
@PreviewParameter(TokenReceiveBottomSheetConfigPreviewProvider::class) params: TokenReceiveBottomSheetConfig,
) {
TangemThemePreview {
val config = TangemBottomSheetConfig(
isShown = true,
content = params,
onDismissRequest = {},
)
TokenReceiveBottomSheet(config)
}
}
private class TokenReceiveBottomSheetConfigPreviewProvider : PreviewParameterProvider<TokenReceiveBottomSheetConfig> {
val address = AddressModel(
displayName = stringReference("Address 1"),
fullName = combinedReference(
stringReference("Address 1"),
stringReference(" Stellar"),
),
value = "0xe5178c7d4d0e861ed2e9414e045b501226b0de8d",
type = AddressModel.Type.Default,
)
private val baseConfig = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = "Stellar",
symbol = "XLM",
),
network = "Ethereum",
notifications = persistentListOf(
NotificationConfig(
title = stringReference("Send only XLM on the Ethereum network"),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconResId = R.drawable.ic_alert_circle_24,
),
),
addresses = persistentListOf(address),
showMemoDisclaimer = false,
onCopyClick = {},
onShareClick = {},
)
override val values: Sequence<TokenReceiveBottomSheetConfig>
get() = sequenceOf(
baseConfig,
baseConfig.copy(
addresses = persistentListOf(
address,
address.copy(displayName = stringReference("Address 2")),
),
),
baseConfig.copy(
showMemoDisclaimer = true,
),
baseConfig.copy(
showMemoDisclaimer = true,
addresses = persistentListOf(
address,
address.copy(displayName = stringReference("Address 2")),
),
),
)
}
// endregion Preview

View file

@ -1,72 +0,0 @@
package com.tangem.common.ui.bottomsheet.receive
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.notifications.NotificationConfig
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.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
data class TokenReceiveBottomSheetConfig(
val asset: Asset,
val network: String,
val addresses: ImmutableList<AddressModel>,
val showMemoDisclaimer: Boolean,
val notifications: ImmutableList<NotificationConfig>,
val onCopyClick: (String) -> Unit,
val onShareClick: (String) -> Unit,
) : TangemBottomSheetConfigContent {
constructor(
asset: Asset,
network: Network,
networkAddress: NetworkAddress,
showMemoDisclaimer: Boolean,
customNotifications: ImmutableList<NotificationConfig> = persistentListOf(),
onCopyClick: (String) -> Unit,
onShareClick: (String) -> Unit,
) : this(
asset = asset,
network = network.name,
addresses = networkAddress.availableAddresses
.mapToAddressModels(asset, network)
.toImmutableList(),
showMemoDisclaimer = showMemoDisclaimer,
notifications = persistentListOf(defaultAssetNotificationConfig(asset, network)).addAll(0, customNotifications),
onCopyClick = onCopyClick,
onShareClick = onShareClick,
)
companion object {
private fun defaultAssetNotificationConfig(asset: Asset, network: Network): NotificationConfig =
NotificationConfig(
title = resourceReference(
R.string.receive_bottom_sheet_warning_title,
wrappedList(asset.displaySymbol, network.name),
),
subtitle = resourceReference(R.string.receive_bottom_sheet_warning_message_description),
iconResId = R.drawable.ic_alert_circle_24,
iconTint = NotificationConfig.IconTint.Accent,
)
}
@Immutable
sealed class Asset {
abstract val displaySymbol: TextReference
data class Currency(val name: String, val symbol: String) : Asset() {
override val displaySymbol = stringReference(symbol)
}
data object NFT : Asset() {
override val displaySymbol = resourceReference(R.string.common_nft)
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.common.ui.expressStatus.state
import androidx.compose.runtime.Composable
import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.PersistentList
data class ExpressTransactionsBlockState(
val transactions: PersistentList<ExpressTransactionStateUM>,
val bottomSheetSlot: BottomSheetSlot?,
val dialogSlot: DialogSlot?,
)
data class BottomSheetSlot(
val config: TangemBottomSheetConfig,
val content: @Composable () -> Unit,
)
data class DialogSlot(
val config: TokenDetailsDialogConfig,
val content: @Composable () -> Unit,
)

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.navigationButtons.preview.NavigationButtonsPreview
import com.tangem.core.ui.components.HoldToConfirmButton
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.buttons.common.TangemButton
@ -98,34 +99,48 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier
modifier = modifier.fillMaxWidth(),
) { button ->
if (button != null && button.textReference != TextReference.EMPTY) {
val icon = if (button.iconRes != null && button.isIconVisible) {
TangemButtonIconPosition.End(iconResId = button.iconRes)
} else {
TangemButtonIconPosition.None
}
val color = if (button.isDimmed) {
TangemButtonsDefaults.secondaryButtonColors
.copy(contentColor = TangemTheme.colors.text.tertiary)
} else {
TangemButtonsDefaults.primaryButtonColors
}
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = {
GlobalMultipleClickPreventer.processEvent {
if (button.isHapticClick) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (button.isHoldToConfirm) {
HoldToConfirmButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
isLoading = button.shouldShowProgress,
onConfirm = {
GlobalMultipleClickPreventer.processEvent {
button.onClick()
}
button.onClick()
}
},
showProgress = button.shouldShowProgress,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
icon = icon,
modifier = Modifier.fillMaxWidth(),
)
},
modifier = Modifier.fillMaxWidth(),
)
} else {
val icon = if (button.iconRes != null && button.isIconVisible) {
TangemButtonIconPosition.End(iconResId = button.iconRes)
} else {
TangemButtonIconPosition.None
}
val color = if (button.isDimmed) {
TangemButtonsDefaults.secondaryButtonColors
.copy(contentColor = TangemTheme.colors.text.tertiary)
} else {
TangemButtonsDefaults.primaryButtonColors
}
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = {
GlobalMultipleClickPreventer.processEvent {
if (button.isHapticClick) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
button.onClick()
}
},
showProgress = button.shouldShowProgress,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
icon = icon,
modifier = Modifier.fillMaxWidth(),
)
}
} else {
Spacer(modifier = Modifier.fillMaxWidth())
}

View file

@ -34,5 +34,6 @@ data class NavigationButton(
val isEnabled: Boolean = true,
val isDimmed: Boolean = false,
val isHapticClick: Boolean = false,
val isHoldToConfirm: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -1,22 +1,24 @@
package com.tangem.common.ui.news
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CardColors
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
@ -39,23 +41,36 @@ fun ArticleCard(
modifier: Modifier = Modifier,
colors: CardColors = TangemBlockCardColors,
) {
BlockCard(
modifier = modifier,
onClick = onArticleClick,
colors = colors,
) {
if (articleConfigUM.isTrending) {
TrendingArticle(articleConfigUM = articleConfigUM)
} else {
DefaultArticle(articleConfigUM = articleConfigUM)
}
if (articleConfigUM.isTrending) {
TrendingArticle(
articleConfigUM = articleConfigUM,
modifier = modifier,
onArticleClick = onArticleClick,
colors = colors,
)
} else {
DefaultArticle(
articleConfigUM = articleConfigUM,
modifier = modifier,
onArticleClick = onArticleClick,
colors = colors,
)
}
}
@Composable
private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
private fun TrendingArticle(
articleConfigUM: ArticleConfigUM,
onArticleClick: () -> Unit,
colors: CardColors,
modifier: Modifier = Modifier,
) {
Column(
modifier = Modifier.padding(vertical = 24.dp, horizontal = 16.dp),
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(colors.containerColor)
.clickable { onArticleClick() }
.padding(vertical = 24.dp, horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
@ -80,8 +95,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
TangemTheme.colors.text.primary1
},
style = TangemTheme.typography.h3,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
)
SpacerH(8.dp)
@ -101,8 +115,54 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
}
@Composable
private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
Column(modifier = Modifier.padding(12.dp)) {
fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
BlockCard(
modifier = modifier,
onClick = onClick,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxSize()
.padding(vertical = 31.dp, horizontal = 12.dp),
) {
Image(
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
contentDescription = stringResourceSafe(R.string.common_show_more),
)
SpacerH(16.dp)
Text(
text = stringResourceSafe(R.string.news_all_news),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringResourceSafe(R.string.news_stay_in_the_loop),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
@Composable
private fun DefaultArticle(
articleConfigUM: ArticleConfigUM,
onArticleClick: () -> Unit,
colors: CardColors,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(colors.containerColor)
.clickable { onArticleClick() }
.padding(12.dp),
) {
ArticleInfo(
score = articleConfigUM.score,
createdAt = articleConfigUM.createdAt.resolveReference(),
@ -122,33 +182,46 @@ private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
overflow = TextOverflow.Ellipsis,
)
SpacerH(16.dp)
SpacerHMax(modifier = Modifier.padding(bottom = 16.dp))
Tags(tags = articleConfigUM.tags.toImmutableList())
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun Tags(tags: ImmutableList<LabelUM>, modifier: Modifier = Modifier) {
val expandIndicator = remember {
ContextualFlowRowOverflow.expandIndicator {
val remainingItems = tags.size - shownItemCount
Label(state = LabelUM(TextReference.Str("${StringsSigns.PLUS}$remainingItems")))
}
}
ContextualFlowRow(
itemCount = tags.size,
FlowRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
maxItemsInEachRow = MAX_TAGS_COUNT_TO_SHOW + 1,
maxLines = 1,
overflow = expandIndicator,
) { index ->
Label(state = tags[index])
itemVerticalAlignment = Alignment.Bottom,
) {
val displayedTags = if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
tags.take(MAX_TAGS_COUNT_TO_SHOW)
} else {
tags
}
displayedTags.forEach { tag ->
Label(state = tag)
}
if (tags.size > MAX_TAGS_COUNT_TO_SHOW) {
val remainingItems = tags.size - MAX_TAGS_COUNT_TO_SHOW
Label(
state = LabelUM(
text = TextReference.Str("${StringsSigns.PLUS}$remainingItems"),
maxLines = 1,
),
)
}
}
}
private const val MAX_TAGS_COUNT_TO_SHOW = 3
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -1,7 +1,10 @@
package com.tangem.common.ui.news
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -9,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@ -49,6 +53,8 @@ internal fun ArticleInfo(score: Float, createdAt: String, modifier: Modifier = M
Text(
text = createdAt,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors.text.secondary,
)
}

View file

@ -41,8 +41,9 @@ fun TrendingLoadingArticle(modifier: Modifier = Modifier) {
}
@Composable
fun DefaultLoadingArticle() {
fun DefaultLoadingArticle(modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(modifier = Modifier.padding(12.dp)) {

View file

@ -21,7 +21,7 @@ import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import java.math.BigDecimal
@Suppress("LargeClass")
@Suppress("LargeClass", "CanBeNonNullable")
object NotificationsFactory {
fun MutableList<NotificationUM>.addFeeUnreachableNotification(
@ -314,12 +314,14 @@ object NotificationsFactory {
}
}
@Suppress("LongParameterList")
fun MutableList<NotificationUM>.addExceedsBalanceNotification(
cryptoCurrencyWarning: CryptoCurrencyWarning?,
cryptoCurrencyStatus: CryptoCurrencyStatus,
shouldMergeFeeNetworkName: Boolean,
onClick: (CryptoCurrency) -> Unit,
onAnalyticsEvent: (CryptoCurrency) -> Unit,
onResetAnalyticsEvent: (CryptoCurrency) -> Unit,
) {
when (cryptoCurrencyWarning) {
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> {
@ -357,7 +359,7 @@ object NotificationsFactory {
)
onAnalyticsEvent(cryptoCurrencyWarning.currency)
}
else -> Unit
else -> onResetAnalyticsEvent(cryptoCurrencyStatus.currency)
}
}

View file

@ -0,0 +1,155 @@
package com.tangem.common.ui.tokendetails
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
/**
* Wallet bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
data class TokenDetailsDialogConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: DialogContentConfig,
) {
sealed class DialogContentConfig {
abstract val title: TextReference?
abstract val message: TextReference
abstract val confirmButtonConfig: ButtonConfig
abstract val cancelButtonConfig: ButtonConfig?
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val hasWarning: Boolean = false,
)
data class ConfirmHideConfig(
val currencyTitle: String,
val onConfirmClick: () -> Unit,
val onCancelClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = TextReference.Res(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currencyTitle),
)
override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message)
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_cancel),
onClick = onCancelClick,
)
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.token_details_hide_alert_hide),
onClick = onConfirmClick,
hasWarning = true,
)
}
data class HasLinkedTokensConfig(
val currencyName: String,
val currencySymbol: String,
val networkName: String,
val onConfirmClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = TextReference.Res(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currencySymbol),
)
override val message: TextReference = TextReference.Res(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(currencyName, currencySymbol, networkName),
)
override val cancelButtonConfig: ButtonConfig?
get() = null
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_ok),
onClick = onConfirmClick,
)
}
data class DisabledButtonReasonDialogConfig(
val text: TextReference,
val onConfirmClick: () -> Unit,
) : DialogContentConfig() {
override val title = null
override val message: TextReference = text
override val cancelButtonConfig = null
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_ok),
onClick = onConfirmClick,
)
}
data class RemoveIncompleteTransactionConfirmDialogConfig(
val onConfirmClick: () -> Unit,
val onCancelClick: () -> Unit,
) : DialogContentConfig() {
override val title = null
override val message: TextReference = TextReference.Res(
id = R.string.warning_kaspa_unfinished_token_transaction_discard_message,
)
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_cancel),
onClick = onCancelClick,
)
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_yes),
onClick = onConfirmClick,
)
}
data class ErrorDialogConfig(
val text: TextReference,
val onConfirmClick: () -> Unit,
) : DialogContentConfig() {
override val title = null
override val message: TextReference = text
override val cancelButtonConfig = null
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_ok),
onClick = onConfirmClick,
)
}
data class ConfirmExpressStatusHideDialogConfig(
val onConfirmClick: () -> Unit,
val onCancelClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = resourceReference(R.string.express_status_hide_dialog_title)
override val message: TextReference = resourceReference(R.string.express_status_hide_dialog_text)
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = resourceReference(R.string.common_hide),
onClick = onConfirmClick,
)
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
text = resourceReference(R.string.common_cancel),
onClick = onCancelClick,
)
}
}
}

View file

@ -23,7 +23,8 @@ import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -45,7 +46,7 @@ class TokenItemStateConverter(
private val appCurrency: AppCurrency,
private val yieldModuleApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingApyMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
private val yieldSupplyPromoBannerKey: String? = null,
private val promoCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = {
CryptoCurrencyToIconStateConverter().convert(it)
},
@ -74,7 +75,7 @@ class TokenItemStateConverter(
createPromoBannerState(
status = status,
yieldModuleApyMap = yieldModuleApyMap,
yieldSupplyPromoBannerKey = yieldSupplyPromoBannerKey,
promoCryptoCurrencyStatus = promoCryptoCurrencyStatus,
onApyLabelClick = onApyLabelClick,
onYieldPromoCloseClick = onYieldPromoCloseClick,
onYieldPromoShown = onYieldPromoShown,
@ -251,9 +252,9 @@ class TokenItemStateConverter(
stakingApyMap = stakingApyMap,
)
val rewardTypeRes = when (stakingInfo.rewardType) {
Yield.RewardType.APR -> R.string.staking_apr_earn_badge
Yield.RewardType.UNKNOWN,
Yield.RewardType.APY,
RewardType.APR -> R.string.staking_apr_earn_badge
RewardType.UNKNOWN,
RewardType.APY,
null,
-> R.string.yield_module_earn_badge
}
@ -283,12 +284,14 @@ class TokenItemStateConverter(
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
is StakingOption.P2P -> {
// P2P or no balance: use preferred validators
// TODO add p2p logic
null
is StakingOption.P2PEthPool -> {
RewardInfo(
rate = stakingOptions.apy,
type = RewardType.APY,
)
}
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
@ -314,7 +317,7 @@ class TokenItemStateConverter(
return StakingLocalInfo(
rate = rateInfo?.rate,
isActive = stakeKitBalance != null, // todo add p2p check
isActive = stakeKitBalance != null || p2pEthPoolBalance != null,
rewardType = rateInfo?.type,
)
}
@ -401,7 +404,7 @@ class TokenItemStateConverter(
private fun createPromoBannerState(
status: CryptoCurrencyStatus,
yieldModuleApyMap: Map<String, BigDecimal>,
yieldSupplyPromoBannerKey: String?,
promoCryptoCurrencyStatus: CryptoCurrencyStatus?,
onApyLabelClick: ((CryptoCurrencyStatus, ApySource, String) -> Unit)?,
onYieldPromoCloseClick: (() -> Unit)?,
onYieldPromoShown: ((cryptoCurrency: CryptoCurrency) -> Unit)?,
@ -411,13 +414,15 @@ class TokenItemStateConverter(
if (status.value !is CryptoCurrencyStatus.Loaded) {
return TokenItemState.PromoBannerState.Empty
}
if (yieldSupplyPromoBannerKey == null || yieldSupplyPromoBannerKey != token.yieldSupplyKey() ||
yieldModuleApyMap[token.yieldSupplyKey()] == null
) {
if (promoCryptoCurrencyStatus == null || status.currency.id != promoCryptoCurrencyStatus.currency.id) {
return TokenItemState.PromoBannerState.Empty
}
val yieldSupplyApy =
yieldModuleApyMap[token.yieldSupplyKey()] ?: return TokenItemState.PromoBannerState.Empty
val yieldSupplyApy = yieldModuleApyMap.entries.firstOrNull { (key, _) ->
key.equals(
other = token.yieldSupplyKey(),
ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId),
)
}?.value ?: return TokenItemState.PromoBannerState.Empty
return TokenItemState.PromoBannerState.Content(
title = resourceReference(
@ -469,7 +474,7 @@ class TokenItemStateConverter(
private data class StakingLocalInfo(
val rate: BigDecimal?,
val isActive: Boolean,
val rewardType: Yield.RewardType?,
val rewardType: RewardType?,
)
private data class EarnApyInfo(

View file

@ -35,6 +35,7 @@ class UserWalletItemUMConverter(
private val isClickableIfLocked: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
artwork: UserWalletItemUM.ImageState? = null,
private val mode: InfoField = InfoField.Devices,
) : Converter<UserWallet, UserWalletItemUM> {
private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading
@ -61,17 +62,29 @@ class UserWalletItemUMConverter(
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) {
is UserWallet.Cold -> {
val cardCount = userWallet.getCardsCount() ?: 1
TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
when (mode) {
is InfoField.Devices -> {
val cardCount = userWallet.getCardsCount() ?: 1
TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
}
is InfoField.Tokens -> {
TextReference.PluralRes(
id = R.plurals.common_tokens_count,
count = mode.tokensCount,
formatArgs = wrappedList(mode.tokensCount),
)
}
}
}
is UserWallet.Hot -> {
TextReference.Res(R.string.hw_mobile_wallet)
}
}
return UserWalletItemUM.Information.Loaded(text)
}
@ -103,4 +116,9 @@ class UserWalletItemUMConverter(
}
}
}
sealed interface InfoField {
data object Devices : InfoField
data class Tokens(val tokensCount: Int) : InfoField
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import javax.annotation.concurrent.Immutable
@ -64,7 +64,7 @@ data class UserWalletItemUM(
data class Account(
val name: TextReference,
val icon: CryptoPortfolioIconUM,
val icon: AccountIconUM.CryptoPortfolio,
) : ImageState()
data class Image(