Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-01 12:06:10 +03:00
commit 036e031c1f
1212 changed files with 28739 additions and 8811 deletions

View file

@ -4,11 +4,13 @@ import android.os.Bundle
import com.tangem.common.routing.AppRoute.ManageTokens.Source
import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.InitScreenLaunchMode
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.feedback.models.CardInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWalletId
@ -23,10 +25,16 @@ sealed class AppRoute(val path: String) : Route {
data object Initial : AppRoute(path = "/initial")
@Serializable
data object Home : AppRoute(path = "/home")
data class Home(
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
) : AppRoute(path = "/home")
@Serializable
data class Welcome(
@Deprecated("No longer used, will be removed in future releases")
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
// we still have this param to be handled by WalletConnectLinkIntentHandler in WelcomeMiddleware
@Deprecated("No longer used, will be removed in future releases")
val intent: SerializableIntent? = null,
) : AppRoute(path = "/welcome"), RouteBundleParams {
@ -129,6 +137,8 @@ sealed class AppRoute(val path: String) : Route {
val initialCurrency: CryptoCurrency,
val selectedCurrency: CryptoCurrency?,
val source: Source,
val showSendViaSwapNotification: Boolean,
val analyticsCategoryName: String,
) : AppRoute(path = "/$source/choose_managed_tokens/$userWalletId/${initialCurrency.id.value}") {
enum class Source {
SendViaSwap,
@ -304,6 +314,21 @@ sealed class AppRoute(val path: String) : Route {
@Serializable
object AddExistingWallet : AppRoute(path = "/add_existing_wallet")
@Serializable
data class WalletActivation(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_activation/${userWalletId.stringValue}")
@Serializable
data class CreateWalletBackup(
val userWalletId: UserWalletId,
) : AppRoute(path = "/create_wallet_backup/${userWalletId.stringValue}")
@Serializable
data class UpdateAccessCode(
val userWalletId: UserWalletId,
) : AppRoute(path = "/update_access_code/${userWalletId.stringValue}")
@Serializable
data class SendEntryPoint(
val userWalletId: UserWalletId,
@ -317,4 +342,27 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
) : AppRoute(path = "/send_with_swap/${userWalletId.stringValue}/${currency.symbol}")
@Serializable
data class CreateAccount(
val userWalletId: UserWalletId,
) : AppRoute(path = "/create_account/${userWalletId.stringValue}")
@Serializable
data class EditAccount(
val account: Account,
) : AppRoute(path = "/edit_account/${account.accountId.value}")
@Serializable
data class AccountDetails(
val account: Account,
) : AppRoute(path = "/account_details/${account.accountId.value}")
@Serializable
data class ArchivedAccountList(
val userWalletId: UserWalletId,
) : AppRoute(path = "/archived_account/${userWalletId.stringValue}")
@Serializable
data object TangemPayDetails : AppRoute(path = "/tangem_pay_details")
}

View file

@ -0,0 +1,13 @@
package com.tangem.common.routing.entity
import kotlinx.serialization.Serializable
@Serializable
sealed class InitScreenLaunchMode {
@Serializable
data object Standard : InitScreenLaunchMode()
@Serializable
data object WithCardScan : InitScreenLaunchMode()
}

View file

@ -14,6 +14,7 @@ data class SerializableIntent(
val packageValue: String?,
val component: String?,
val flags: Int,
// CAUTION: works wrong with SerializableBundle constructor(bundle: Bundle), need to be removed
val extras: SerializableBundle?,
) {

View file

@ -5,7 +5,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.models.staking.StakingID
import java.math.BigDecimal
/**

View file

@ -4,7 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
import com.tangem.domain.staking.model.StakingID
import com.tangem.domain.models.staking.StakingID
import java.math.BigDecimal
/**

View file

@ -8,8 +8,8 @@ import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.test.domain.card.MockScanResponseFactory
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.card.DerivationStyleProvider
import com.tangem.domain.card.common.util.derivationStyleProvider
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.card.configs.GenericCardConfig
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
@ -25,6 +25,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
val cardano by lazy { createCoin(blockchain = Blockchain.Cardano) }
val chia by lazy { createCoin(Blockchain.Chia) }
val ethereum by lazy { createCoin(Blockchain.Ethereum) }
val stellar by lazy { createCoin(Blockchain.Stellar) }
val chiaAndEthereum by lazy {
listOf(
@ -64,6 +65,10 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
hasFiatFeeRate = blockchain.feePaidCurrency() !is FeePaidCurrency.FeeResource,
canHandleTokens = false,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = when (blockchain) {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.NameResolvingType.ENS
else -> Network.NameResolvingType.NONE
},
)
return factory.createCoin(network = network)
@ -94,6 +99,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul
hasFiatFeeRate = true,
canHandleTokens = true,
transactionExtrasType = Network.TransactionExtrasType.NONE,
nameResolvingType = Network.NameResolvingType.NONE,
),
name = "NEVER-MIND",
symbol = "NEVER-MIND",

View file

@ -9,7 +9,24 @@ fun <B> assertEither(actual: Either<Throwable, B>, expected: Either<Throwable, B
.onLeft {
val expectedError = expected.leftOrNull() ?: error("Actual is Either.Left: $it")
Truth.assertThat(it::class.java).isEqualTo(expectedError::class.java)
Truth.assertThat(it).isInstanceOf(expectedError::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(expectedError.message)
}
}
fun assertEitherRight(actual: Either<Throwable, Unit>) {
actual
.onRight { Truth.assertThat(actual).isEqualTo(Either.Right(Unit)) }
.onLeft {
error("Actual is Either.Left: $it")
}
}
fun <B> assertEitherLeft(actual: Either<Throwable, B>, expected: Throwable) {
actual
.onRight { error("Actual is Either.Right: $it") }
.onLeft {
Truth.assertThat(it::class.java).isEqualTo(expected::class.java)
Truth.assertThat(it).hasMessageThat().isEqualTo(expected.message)
}
}

View file

@ -12,7 +12,6 @@ dependencies {
/** Compose */
implementation(deps.compose.material3)
implementation(deps.compose.material)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)

View file

@ -0,0 +1,134 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.AccountIconPreviewData.randomAccountIcon
import com.tangem.core.ui.extensions.TextReference
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.domain.models.account.CryptoPortfolioIcon
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
enum class AccountIconSize {
Default, Large, Medium, Small, ExtraSmall
}
/**
* Displays an account icon that can either show a letter (derived from [name])
* or a predefined vector resource (from [icon]).
*
* The background color is determined by the icon's [CryptoPortfolioIconUM.color],
* and the icon size, text style, and box modifier are adapted based on the given [size].
*
* @param name The text reference used to resolve and display the first letter
* when [icon] is set to [CryptoPortfolioIcon.Icon.Letter].
* @param icon The account icon definition, which can be a letter or a drawable resource.
* @param size The size of the icon, defined by [AccountIconSize].
*/
@Composable
fun AccountIcon(
name: TextReference,
icon: CryptoPortfolioIconUM,
size: AccountIconSize,
modifier: Modifier = Modifier,
) {
val boxModifier = modifier.selectBoxModifier(size)
val iconSize = Modifier.selectIconSize(size)
val textStyle = when (size) {
AccountIconSize.Default -> TangemTheme.typography.h3
AccountIconSize.Large -> TangemTheme.typography.h1
AccountIconSize.Medium -> TangemTheme.typography.subtitle1
AccountIconSize.Small -> TangemTheme.typography.subtitle2
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
}
Box(
contentAlignment = Alignment.Center,
modifier = boxModifier.background(icon.color.getUiColor()),
) {
val icon = icon.value
val letter = name.resolveReference().firstOrNull()
when {
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
text = letter?.uppercase() ?: "",
style = textStyle,
color = TangemTheme.colors.text.constantWhite,
)
else -> Icon(
modifier = iconSize,
tint = TangemTheme.colors.text.constantWhite,
imageVector = ImageVector.vectorResource(id = icon.getResId()),
contentDescription = null,
)
}
}
}
private fun Modifier.selectIconSize(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> this.size(20.dp)
AccountIconSize.Large -> this.size(40.dp)
AccountIconSize.Medium -> this.size(16.dp)
AccountIconSize.Small -> this.size(12.dp)
AccountIconSize.ExtraSmall -> this.size(8.dp)
}
private fun Modifier.selectBoxModifier(size: AccountIconSize): Modifier = when (size) {
AccountIconSize.Default -> size(36.dp).clip(RoundedCornerShape(10.dp))
AccountIconSize.Large -> size(88.dp).clip(RoundedCornerShape(24.dp))
AccountIconSize.Medium -> size(28.dp).clip(RoundedCornerShape(8.dp))
AccountIconSize.Small -> size(20.dp).clip(RoundedCornerShape(6.dp))
AccountIconSize.ExtraSmall -> size(14.dp).clip(RoundedCornerShape(4.dp))
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AccountIcon() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Account Name")
Row(
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(), size = AccountIconSize.ExtraSmall)
}
Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Default)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Large)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Medium)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.Small)
AccountIcon(name = name, randomAccountIcon(letter = true), size = AccountIconSize.ExtraSmall)
}
}
}
object AccountIconPreviewData {
fun randomAccountIcon(letter: Boolean = false) = CryptoPortfolioIconUM(
value = if (letter) CryptoPortfolioIcon.Icon.Letter else CryptoPortfolioIcon.Icon.entries.random(),
color = Color.entries.random(),
)
}

View file

@ -0,0 +1,110 @@
package com.tangem.common.ui.account
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
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.TangemThemePreview
/**
* Displays a row representing an account with an icon, title, and subtitle.
*
* The row consists of:
* - An [AccountIcon] on the left.
* - A column with the [title] and [subtitle] texts, which can be displayed in normal
* or reversed order depending on [isReverse].
*
* The layout uses horizontal spacing between the icon and text, and vertical spacing
* between the title and subtitle.
*
* @param title The main text shown in the row, usually representing the account name.
* @param subtitle The secondary text, typically providing additional details about the account.
* @param icon The account icon definition, displayed using [AccountIcon].
* @param isReverse If `true`, the [subtitle] is displayed above the [title].
* Otherwise, the [title] is displayed above the [subtitle].
*/
@Composable
fun AccountRow(
title: TextReference,
subtitle: TextReference,
icon: CryptoPortfolioIconUM,
modifier: Modifier = Modifier,
isReverse: Boolean = false,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
AccountIcon(
name = title,
icon = icon,
size = AccountIconSize.Default,
)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
) {
if (isReverse) {
Subtitle(subtitle)
Title(title)
} else {
Title(title)
Subtitle(subtitle)
}
}
}
}
@Composable
private fun Title(title: TextReference) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
}
@Composable
private fun Subtitle(subtitle: TextReference) {
Text(
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.caption2,
text = subtitle.resolveReference(),
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
Sample()
}
}
@Composable
private fun Sample() {
val name = stringReference("Main account")
val info = stringReference("10 tokens in 2 networks")
val subtitle = resourceReference(R.string.account_form_name)
fun icon(letter: Boolean = false) = AccountIconPreviewData.randomAccountIcon(letter)
Column(
modifier = Modifier.background(TangemTheme.colors.background.primary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
AccountRow(title = name, subtitle = info, icon = icon())
AccountRow(title = name, subtitle = subtitle, icon = icon(), isReverse = true)
}
}

View file

@ -47,4 +47,7 @@ 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

@ -0,0 +1,15 @@
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

@ -4,7 +4,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer

View file

@ -14,7 +14,7 @@ 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.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -15,7 +15,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -9,15 +9,18 @@ import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.combinedReference
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.extensions.orMaskWithStars
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.persistentListOf
@ -58,6 +61,7 @@ class AmountStateConverter(
return AmountState.Data(
title = value.title,
availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
availableBalanceShort = stringReference(crypto),
tokenName = stringReference(status.currency.name),
tokenIconState = iconStateConverter.convert(status),
amountTextField = amountFieldConverter.convert(value.value),
@ -94,7 +98,9 @@ class AmountStateConverter(
* @property maxEnterAmount max enter amount data
* @property cryptoCurrencyStatus current cryptocurrency status
* @property iconStateConverter currency icon converter
* @property isBalanceHidden is balance hidden status
*/
@Suppress("LongParameterList")
class AmountStateConverterV2(
private val clickIntents: AmountScreenClickIntents,
private val appCurrency: AppCurrency,
@ -102,6 +108,7 @@ class AmountStateConverterV2(
private val maxEnterAmount: EnterAmountBoundary,
private val iconStateConverter: CryptoCurrencyToIconStateConverter,
private val isRedesignEnabled: Boolean,
private val isBalanceHidden: Boolean,
) : Converter<AmountParameters, AmountState> {
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
@ -117,13 +124,25 @@ class AmountStateConverterV2(
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) {
return AmountState.Empty(
isRedesignEnabled = isRedesignEnabled,
)
}
return AmountState.Data(
title = value.title,
availableBalance = if (isRedesignEnabled) {
resourceReference(R.string.common_balance, wrappedList(crypto))
combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
).orMaskWithStars(isBalanceHidden)
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
.orMaskWithStars(isBalanceHidden)
},
availableBalanceShort = stringReference(crypto).orMaskWithStars(isBalanceHidden),
tokenName = stringReference(cryptoCurrencyStatus.currency.name),
tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus.currency),
amountTextField = amountFieldConverter.convert(value.value),

View file

@ -1,7 +1,7 @@
package com.tangem.common.ui.amountScreen.converters
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter
/**

View file

@ -3,13 +3,17 @@ package com.tangem.common.ui.amountScreen.converters.field
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.core.ui.extensions.combinedReference
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.extensions.orMaskWithStars
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.transformer.Transformer
/**
@ -25,6 +29,7 @@ class AmountBoundaryUpdateTransformer(
private val maxEnterAmount: EnterAmountBoundary,
private val appCurrency: AppCurrency,
private val isRedesignEnabled: Boolean,
private val isBalanceHidden: Boolean,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
@ -34,13 +39,18 @@ class AmountBoundaryUpdateTransformer(
val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) }
val availableBalance = if (isRedesignEnabled) {
resourceReference(R.string.common_balance, wrappedList(crypto))
combinedReference(
stringReference(crypto),
stringReference(" $DOT "),
stringReference(fiat),
)
} else {
resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat))
}
return prevState.copy(
availableBalance = availableBalance,
availableBalance = availableBalance.orMaskWithStars(isBalanceHidden),
availableBalanceShort = stringReference(crypto).orMaskWithStars(isBalanceHidden),
)
}
}

View file

@ -16,7 +16,7 @@ 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.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
@ -39,7 +39,7 @@ class AmountFieldChangeTransformer(
val amountTextField = prevState.amountTextField
if (value.isEmpty()) return prevState.emptyState()
if (value.isEmpty()) return prevState.emptyState(maxEnterAmount.fiatRate)
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
@ -75,7 +75,7 @@ class AmountFieldChangeTransformer(
error = when {
isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance)
isLessThanMinimumIfProvided -> {
val minimumAmount = minimumTransactionAmount?.amount.format {
val minimumAmount = minimumTransactionAmount.amount.format {
crypto(cryptoCurrencyStatus.currency)
}
@ -96,7 +96,7 @@ class AmountFieldChangeTransformer(
)
}
private fun AmountState.Data.emptyState(): AmountState.Data {
private fun AmountState.Data.emptyState(fiatRate: BigDecimal?): AmountState.Data {
return copy(
isPrimaryButtonEnabled = false,
reduceAmountBy = BigDecimal.ZERO,
@ -104,7 +104,7 @@ class AmountFieldChangeTransformer(
value = "",
fiatValue = "",
cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO),
fiatAmount = amountTextField.fiatAmount.copy(value = if (fiatRate != null) BigDecimal.ZERO else null),
isError = false,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.None,

View file

@ -9,9 +9,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.Amount
import com.tangem.domain.tokens.model.AmountType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.convertToAmount
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
@ -100,8 +100,8 @@ class AmountFieldConverterV2(
val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency)
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val (fiatValue, fiatDecimal) = when {
value.isEmpty() -> "" to BigDecimal.ZERO
fiatRate.isNullOrZero() -> "" to null
value.isEmpty() -> "" to BigDecimal.ZERO
else -> {
val fiatDecimal = fiatRate?.multiply(cryptoDecimal)
val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty()

View file

@ -12,7 +12,7 @@ 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.core.ui.utils.parseBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.extensions.isZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal

View file

@ -17,7 +17,8 @@ sealed class AmountState {
/**
* @param isPrimaryButtonEnabled indicates if next state button enabled
* @param title title
* @param availableBalance user crypto currency balance
* @param availableBalance user crypto currency balance with fiat balance
* @param availableBalanceShort user crypto currency balance without fiat balance
* @param tokenIconState crypto currency icon state
* @param segmentedButtonConfig currency switcher config
* @param selectedButton selected currency index
@ -33,6 +34,7 @@ sealed class AmountState {
override val isRedesignEnabled: Boolean,
val title: TextReference,
val availableBalance: TextReference,
val availableBalanceShort: TextReference,
val tokenName: TextReference,
val tokenIconState: CurrencyIconState,
val segmentedButtonConfig: PersistentList<AmountSegmentedButtonsConfig>,

View file

@ -24,7 +24,8 @@ object AmountStatePreviewData {
val amountState = AmountState.Data(
isPrimaryButtonEnabled = false,
title = stringReference("Family Wallet"),
availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"),
availableBalance = stringReference("2 130,88 USDT • 2 129,92 \$)"),
availableBalanceShort = stringReference("2 130,88 USDT"),
tokenIconState = CurrencyIconState.Loading,
segmentedButtonConfig = persistentListOf(
AmountSegmentedButtonsConfig(
@ -92,6 +93,13 @@ object AmountStatePreviewData {
amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false),
)
val amountStateV2WithoutRates = amountState.copy(
amountTextField = amountState.amountTextField.copy(
fiatAmount = amountState.amountTextField.fiatAmount.copy(
value = null,
),
),
)
val amountErrorState = amountWithValueState.copy(
amountTextField = amountWithValueState.amountTextField.copy(
isError = true,

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -25,6 +26,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingSendDetailsScreenTestTags
import java.math.BigDecimal
@Composable
@ -63,7 +65,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing24),
.padding(top = TangemTheme.dimens.spacing24)
.testTag(StakingSendDetailsScreenTestTags.PRIMARY_AMOUNT),
)
Text(
text = secondAmount,
@ -72,7 +75,8 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
.padding(top = TangemTheme.dimens.spacing8)
.testTag(StakingSendDetailsScreenTestTags.SECONDARY_AMOUNT),
)
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
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
@ -24,6 +25,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -43,7 +45,7 @@ fun AmountBlockV2(
crypto(
symbol = "",
decimals = amount.cryptoAmount.decimals,
)
).uncapped()
}.orEmpty()
val fiatAmount = amount.fiatAmount.value.format {
@ -62,7 +64,7 @@ fun AmountBlockV2(
AmountBlockV2(
title = amountState.title,
balance = amountState.availableBalance,
balance = amountState.availableBalanceShort,
currencyTitle = currencyTitle,
currencyIconState = amountState.tokenIconState,
firstAmount = firstAmount,
@ -102,7 +104,7 @@ private fun AmountBlockV2(
Row {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.body2,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
SpacerWMax()
@ -121,18 +123,20 @@ private fun AmountBlockV2(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(top = 8.dp),
) {
Text(
ResizableText(
text = firstAmount,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
ResizableText(
text = secondAmount,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
)
extraContent()
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig
@ -22,6 +23,7 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendScreenTestTags
import kotlinx.collections.immutable.PersistentList
private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey"
@ -77,7 +79,8 @@ internal fun LazyListScope.buttons(
.padding(
vertical = TangemTheme.dimens.spacing10,
horizontal = TangemTheme.dimens.spacing34,
),
)
.testTag(StakingSendScreenTestTags.MAX_BUTTON),
)
}
}
@ -90,7 +93,8 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment
.fillMaxSize()
.padding(
horizontal = TangemTheme.dimens.spacing10,
),
)
.testTag(StakingSendScreenTestTags.CURRENCY_BUTTON),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
@ -102,13 +106,13 @@ private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegment
url = button.iconUrl,
size = TangemTheme.dimens.size18,
isGrayscale = !isSegmentedButtonsEnabled,
modifier = iconModifier,
modifier = iconModifier.testTag(StakingSendScreenTestTags.FIAT_ICON),
)
} else if (button.iconState != null) {
CurrencyIcon(
state = button.iconState,
shouldDisplayNetwork = false,
modifier = iconModifier,
modifier = iconModifier.testTag(StakingSendScreenTestTags.CURRENCY_ICON),
)
}
Text(

View file

@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
@ -28,6 +29,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendScreenTestTags
import com.tangem.core.ui.utils.rememberDecimalFormat
import kotlinx.coroutines.delay
@ -116,7 +118,8 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri
textAlign = TextAlign.Center,
modifier = Modifier
.align(TopCenter)
.padding(bottom = TangemTheme.dimens.spacing32),
.padding(bottom = TangemTheme.dimens.spacing32)
.testTag(StakingSendScreenTestTags.SECONDARY_AMOUNT),
)
AmountFieldError(
isError = amountField.isError,

View file

@ -15,6 +15,7 @@ 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.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.R
@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.StakingSendScreenTestTags
private const val AMOUNT_FIELD_KEY = "amountFieldKey"
@ -52,7 +54,8 @@ internal fun LazyListScope.amountField(
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing14),
.padding(top = TangemTheme.dimens.spacing14)
.testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TITLE),
)
val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference()
@ -66,7 +69,8 @@ internal fun LazyListScope.amountField(
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing2),
.padding(top = TangemTheme.dimens.spacing2)
.testTag(StakingSendScreenTestTags.AMOUNT_CONTAINER_TEXT),
)
}
CurrencyIcon(
@ -113,7 +117,7 @@ internal fun LazyListScope.amountFieldV2(
} else {
Text(
text = amountState.title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
@ -164,7 +168,7 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi
modifier = Modifier
.padding(end = 16.dp)
.clip(RoundedCornerShape(16.dp))
.background(TangemTheme.colors.background.secondary)
.background(TangemTheme.colors.button.secondary)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.BottomCenter
import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.graphicsLayer
@ -127,7 +128,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.padding(top = 8.dp),
.padding(top = 4.dp),
) {
if (amountUM is AmountState.Empty) {
TextShimmer(
@ -148,7 +149,7 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -
error = amountUM.amountTextField.error,
modifier = Modifier
.align(BottomCenter)
.padding(top = 20.dp),
.padding(top = 24.dp),
)
}
}
@ -156,17 +157,21 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -
@Composable
private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) {
val isFiatAvailable = amountUM.amountTextField.fiatAmount.value != null
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.align(TopCenter)
.padding(bottom = 20.dp)
.padding(bottom = 16.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onCurrencyChange(!amountUM.amountTextField.isFiatValue) },
),
)
.alpha(if (isFiatAvailable) 1f else 0f)
.padding(4.dp),
) {
val iconRotateState by animateFloatAsState(
targetValue = if (amountUM.amountTextField.isFiatValue) ROTATED_DEGREE else INITIAL_DEGREE,
@ -324,6 +329,8 @@ private class AmountFieldV2PreviewProvider : PreviewParameterProvider<AmountStat
get() = sequenceOf(
AmountStatePreviewData.emptyState,
AmountStatePreviewData.amountState,
AmountStatePreviewData.amountErrorState,
AmountStatePreviewData.amountStateV2WithoutRates,
)
}
// endregion

View file

@ -2,9 +2,11 @@ package com.tangem.common.ui.amountScreen.utils
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.BigDecimalFormatter.EMPTY_BALANCE_SIGN
import com.tangem.core.ui.format.bigdecimal.approximateAmount
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.utils.StringsSigns.DASH_SIGN
import java.math.BigDecimal
fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? {
@ -19,12 +21,19 @@ fun getFiatString(
appCurrency: AppCurrency,
approximate: Boolean = false,
): String {
if (value == null || rate == null) return EMPTY_BALANCE_SIGN
if (value == null || rate == null) return DASH_SIGN
val feeValue = value.multiply(rate)
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
withApproximateSign = approximate,
)
return feeValue.format {
if (approximate) {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
).approximateAmount()
} else {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
}
}

View file

@ -43,9 +43,9 @@ fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) {
config = config,
containerColor = TangemTheme.colors.background.secondary,
titleText = resourceReference(R.string.give_permission_title),
titleAction = TopAppBarButtonUM(
titleAction = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_information_24,
onIconClicked = { isPermissionAlertShow = true },
onClicked = { isPermissionAlertShow = true },
),
content = { content: GiveTxPermissionBottomSheetConfig ->
GiveTxPermissionBottomSheetContent(content = content)

View file

@ -49,10 +49,10 @@ fun SendingText(footerText: TextReference, modifier: Modifier = Modifier) {
text = footerText.resolveAnnotatedReference(),
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
.padding(16.dp),
)
}
}

View file

@ -15,6 +15,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -22,18 +25,18 @@ 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.Keyboard
import com.tangem.core.ui.components.SecondaryButtonIconStart
import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.buttons.common.contentColor
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import com.tangem.core.ui.utils.singleEvent
import com.tangem.core.ui.test.StakingSendScreenTestTags
@Composable
fun NavigationButtonsBlock(
@ -47,7 +50,7 @@ fun NavigationButtonsBlock(
modifier = modifier.fillMaxWidth(),
) {
InfoText(footerText)
ExtraButtons(state?.extraButtons, state?.txUrl)
DoneButtons(state?.extraButtons)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
@ -58,9 +61,33 @@ fun NavigationButtonsBlock(
}
}
@Composable
fun NavigationButtonsBlockV2(
navigationUM: NavigationUM,
modifier: Modifier = Modifier,
footerText: TextReference? = null,
) {
val navigationUM = navigationUM as? NavigationUM.Content
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.fillMaxWidth(),
) {
InfoText(footerText)
DoneButtons(navigationUM?.secondaryPairButtonsUM)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
PreviousButton(navigationUM?.prevButton)
NavigationPrimaryButton(navigationUM?.primaryButton, modifier = Modifier.weight(1f))
}
}
}
@Composable
fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier = Modifier) {
val wrappedButton by rememberNavigationButton(primaryButton)
val hapticFeedback = LocalHapticFeedback.current
AnimatedContent(
targetState = wrappedButton,
transitionSpec = { navigationButtonsTransition() },
@ -83,7 +110,12 @@ fun NavigationPrimaryButton(primaryButton: NavigationButton?, modifier: Modifier
TangemButton(
text = button.textReference.resolveReference(),
enabled = button.isEnabled,
onClick = button.onClick,
onClick = {
if (button.isHapticClick) {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
}
button.onClick()
},
showProgress = button.showProgress,
colors = color,
textStyle = TangemTheme.typography.subtitle1,
@ -116,40 +148,47 @@ private fun PreviousButton(prevButton: NavigationButton?) {
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable(onClick = button.onClick)
.padding(TangemTheme.dimens.spacing12),
.padding(TangemTheme.dimens.spacing12)
.testTag(StakingSendScreenTestTags.PREVIOUS_BUTTON),
)
}
}
}
@Composable
private fun ExtraButtons(extraButtons: ImmutableList<NavigationButton>?, txUrl: String?) {
fun DoneButtons(pairButtons: Pair<NavigationButton, NavigationButton>?, modifier: Modifier = Modifier) {
AnimatedVisibility(
visible = !txUrl.isNullOrBlank() && extraButtons != null,
visible = pairButtons != null,
enter = slideInVertically(initialOffsetY = { it / 2 }).plus(fadeIn()),
exit = slideOutVertically(targetOffsetY = { it / 2 }).plus(fadeOut()),
label = "Animate show sent state buttons",
modifier = Modifier.fillMaxWidth(),
modifier = modifier.fillMaxWidth(),
) {
val buttons = remember(this) { requireNotNull(extraButtons) }
val (leftButton, rightButton) = remember(this) { requireNotNull(pairButtons) }
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
) {
buttons.forEach { button ->
val icon = button.iconRes?.let { TangemButtonIconPosition.Start(iconResId = it) }
?: TangemButtonIconPosition.None
TangemButton(
text = button.textReference.resolveReference(),
icon = icon,
textStyle = TangemTheme.typography.subtitle1,
onClick = rememberHapticFeedback(state = button, onAction = button.onClick),
modifier = Modifier.weight(1f),
enabled = button.isEnabled,
showProgress = false,
colors = TangemButtonsDefaults.secondaryButtonColors,
)
}
SecondaryButtonIconStart(
text = leftButton.textReference.resolveReference(),
iconResId = requireNotNull(leftButton.iconRes),
onClick = {
singleEvent {
leftButton.onClick()
}
},
modifier = Modifier.weight(1f),
)
SecondaryButtonIconStart(
text = rightButton.textReference.resolveReference(),
iconResId = requireNotNull(rightButton.iconRes),
onClick = {
singleEvent {
rightButton.onClick()
}
},
modifier = Modifier.weight(1f),
)
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.common.ui.navigationButtons
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
sealed class NavigationButtonsState {
data object Empty : NavigationButtonsState()
@ -10,7 +9,7 @@ sealed class NavigationButtonsState {
data class Data(
val primaryButton: NavigationButton?,
val prevButton: NavigationButton?,
val extraButtons: ImmutableList<NavigationButton>,
val extraButtons: Pair<NavigationButton, NavigationButton>?,
val txUrl: String? = null,
val onTextClick: (String) -> Unit,
) : NavigationButtonsState()

View file

@ -5,29 +5,25 @@ import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import kotlinx.collections.immutable.persistentListOf
internal object NavigationButtonsPreview {
private val extraButtons = persistentListOf(
NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
),
NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
),
private val extraButtons = NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
) to NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = {},
)
private val prev = NavigationButton(

View file

@ -0,0 +1,11 @@
package com.tangem.common.ui.notifications
/**
* NotificationId represents unique identifiers for notifications in the app.
*
* These ids can be used with [ShouldShowNotificationUseCase] and [SetShouldShowNotificationUseCase]
* to check or update the visibility state of notifications.
*/
enum class NotificationId(val key: String) {
SendViaSwapTokenSelectorNotification("SendViaSwapTokenSelectorNotificationKey"),
}

View file

@ -273,6 +273,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
iconResId: Int = R.drawable.ic_alert_circle_24,
buttonsState: NotificationConfig.ButtonsState? = null,
onCloseClick: (() -> Unit)? = null,
iconTint: NotificationConfig.IconTint = NotificationConfig.IconTint.Unspecified,
) : NotificationUM(
config = NotificationConfig(
title = title,
@ -280,6 +281,7 @@ sealed class NotificationUM(val config: NotificationConfig) {
iconResId = iconResId,
buttonsState = buttonsState,
onCloseClick = onCloseClick,
iconTint = iconTint,
),
)
@ -373,5 +375,15 @@ sealed class NotificationUM(val config: NotificationConfig) {
formatArgs = wrappedList(rentInfo.exemptionAmount),
),
)
data class RentExemptionDestination(
private val rentExemptionAmount: BigDecimal,
) : Error(
title = TextReference.Res(R.string.send_notification_invalid_amount_title),
subtitle = TextReference.Res(
id = R.string.send_notification_invalid_amount_rent_destination,
formatArgs = wrappedList(rentExemptionAmount),
),
)
}
}

View file

@ -11,7 +11,7 @@ import com.tangem.core.ui.format.bigdecimal.uncapped
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.transaction.error.GetFeeError
@ -354,7 +354,11 @@ object NotificationsFactory {
onReduceClick = onReduceClick,
)
is BlockchainSdkError.DestinationTagRequired -> addRequireDestinationTagErrorNotification()
null -> minAdaValue?.let {
is BlockchainSdkError.Solana.DestinationRentExemption -> addRentExemptionDestinationNotification(
rentExemptionAmount = validationError.rentAmount,
)
null,
-> minAdaValue?.let {
add(
NotificationUM.Cardano.MinAdaValueCharged(
tokenName = cryptoCurrency.name,
@ -439,6 +443,14 @@ object NotificationsFactory {
add(NotificationUM.Solana.RentInfo(rentWarning))
}
fun MutableList<NotificationUM>.addRentExemptionDestinationNotification(rentExemptionAmount: BigDecimal) {
add(
NotificationUM.Solana.RentExemptionDestination(
rentExemptionAmount = rentExemptionAmount,
),
)
}
fun MutableList<NotificationUM>.addHighFeeWarningNotification(
enteredAmountValue: BigDecimal,
cryptoCurrencyStatus: CryptoCurrencyStatus,

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@ -29,6 +30,7 @@ import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SwapStoriesScreenTestTags
import kotlinx.collections.immutable.persistentListOf
private val SubtitleColor = Color(0xFFB0B0B0)
@ -46,7 +48,8 @@ fun SwapStoriesScreen(config: SwapStoriesUM) {
Box(
modifier = Modifier
.fillMaxSize()
.background(TangemColorPalette.Black),
.background(TangemColorPalette.Black)
.testTag(SwapStoriesScreenTestTags.SCREEN_CONTAINER),
) {
SubcomposeAsyncImage(
modifier = Modifier.fillMaxSize(),

View file

@ -13,9 +13,9 @@ import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero

View file

@ -3,7 +3,9 @@ package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -22,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow
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 coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.R
@ -30,6 +33,9 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
@ -67,8 +73,10 @@ fun UserWalletItem(
balance = state.balance,
)
state.label?.let { Label(it) }
when (state.endIcon) {
UserWalletItemUM.EndIcon.None -> {}
UserWalletItemUM.EndIcon.None -> Unit
UserWalletItemUM.EndIcon.Arrow -> {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_right_24),
@ -179,6 +187,19 @@ fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modi
radius = TangemTheme.dimens.size2,
)
}
is UserWalletItemUM.ImageState.MobileWallet -> {
Image(
modifier = Modifier
.size(36.dp)
.background(
color = TangemTheme.colors.field.focused,
shape = RoundedCornerShape(10.dp),
)
.padding(6.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_mobile_wallet_icon_24),
contentDescription = null,
)
}
is UserWalletItemUM.ImageState.Image -> {
val verifiedArtwork = imageState.artwork.verifiedArtwork
if (verifiedArtwork != null) {
@ -257,6 +278,18 @@ private fun Preview_UserWalletItem(
private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWalletItemUM> {
override val values: Sequence<UserWalletItemUM>
get() = sequenceOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_0".encodeToByteArray()),
name = stringReference("Mobile Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Locked,
label = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
@ -347,6 +380,18 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Failed,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
imageState = UserWalletItemUM.ImageState.MobileWallet,
isEnabled = true,
onClick = {},
),
)
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {

View file

@ -2,7 +2,10 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.label.entity.LabelStyle
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.fiat
@ -32,6 +35,7 @@ class UserWalletItemUMConverter(
private val appCurrency: AppCurrency? = null,
private val balance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val authMode: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
private val artwork: ArtworkModel? = null,
) : Converter<UserWallet, UserWalletItemUM> {
@ -45,16 +49,38 @@ class UserWalletItemUMConverter(
name = stringReference(name),
information = getInfo(userWallet = this),
balance = getBalanceInfo(userWallet = this),
isEnabled = !isLocked,
isEnabled = isEnabled(userWallet = this),
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = artwork?.let {
UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it))
} ?: UserWalletItemUM.ImageState.Loading,
imageState = getImageState(userWallet = value),
label = getLabelOrNull(userWallet = this),
)
}
}
private fun isEnabled(userWallet: UserWallet): Boolean {
return authMode || userWallet.isLocked.not()
}
private fun getLabelOrNull(userWallet: UserWallet): LabelUM? {
return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) {
LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
)
} else {
null
}
}
private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState {
return when {
userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet
artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork))
else -> UserWalletItemUM.ImageState.Loading
}
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) {
is UserWallet.Cold -> {

View file

@ -1,6 +1,7 @@
package com.tangem.common.ui.userwallet.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.wallet.UserWalletId
import javax.annotation.concurrent.Immutable
@ -15,6 +16,7 @@ data class UserWalletItemUM(
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
val onClick: () -> Unit,
val label: LabelUM? = null,
) {
enum class EndIcon {
None,
@ -54,6 +56,8 @@ data class UserWalletItemUM(
data object Loading : ImageState()
data object MobileWallet : ImageState()
data class Image(
val artwork: ArtworkUM,
) : ImageState()