Updated on 2026-08-14
This commit is contained in:
parent
c637c9a194
commit
0b3914cf6a
29 changed files with 453 additions and 37 deletions
|
|
@ -66,6 +66,12 @@ fun AccountIcon(
|
|||
fun AccountIcon(name: TextReference, icon: AccountIconUM, size: AccountIconSize, modifier: Modifier = Modifier) {
|
||||
when (icon) {
|
||||
is AccountIconUM.Payment -> PaymentAccountIcon(size = size, modifier = modifier)
|
||||
is AccountIconUM.Virtual -> AccountResIcon(
|
||||
resId = icon.icon.getResId(),
|
||||
color = icon.color.getUiColor(),
|
||||
size = size,
|
||||
modifier = modifier,
|
||||
)
|
||||
is AccountIconUM.CryptoPortfolio -> AccountIcon(
|
||||
name = name,
|
||||
icon = icon,
|
||||
|
|
|
|||
|
|
@ -7,4 +7,9 @@ sealed class AccountIconUM {
|
|||
data class CryptoPortfolio(val value: Icon, val color: Color) : AccountIconUM()
|
||||
|
||||
data object Payment : AccountIconUM()
|
||||
|
||||
data object Virtual : AccountIconUM() {
|
||||
val icon: Icon = Icon.Safe
|
||||
val color: Color = Color.VitalGreen
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,10 @@ private fun PreviewAccountTitle() {
|
|||
accountTitleUM = AccountTitleUM.Account.payment(prefixText = stringReference(StringsSigns.DOT)),
|
||||
modifier = Modifier.padding(4.dp),
|
||||
)
|
||||
AccountTitle(
|
||||
accountTitleUM = AccountTitleUM.Account.virtual(prefixText = stringReference(StringsSigns.DOT)),
|
||||
modifier = Modifier.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ 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
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
||||
/**
|
||||
* A sealed interface representing the title of an account, which can be either a simple text
|
||||
|
|
@ -31,6 +32,14 @@ sealed interface AccountTitleUM {
|
|||
icon = AccountIconUM.Payment,
|
||||
)
|
||||
}
|
||||
|
||||
fun virtual(prefixText: TextReference = TextReference.EMPTY): Account {
|
||||
return Account(
|
||||
prefixText = prefixText,
|
||||
name = stringReference("Virtual account"),
|
||||
icon = AccountIconUM.Virtual,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -31,6 +31,7 @@ class AmountAccountConverter(
|
|||
return when (account) {
|
||||
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(account.icon)
|
||||
is Account.Payment -> AccountIconUM.Payment
|
||||
is Account.Virtual -> AccountIconUM.Virtual
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,8 @@ internal class DefaultAccountTxHistoryFetcher @AssistedInject constructor(
|
|||
.filterIsInstance<Account.CryptoPortfolio>()
|
||||
.controlFetchersForCryptoAccount()
|
||||
is Account.Payment -> controlFetchersForPaymentAccount()
|
||||
// Virtual account tx-history isn't wired yet (separate task) — no express fetchers for now.
|
||||
is Account.Virtual -> emptyFlow()
|
||||
}
|
||||
controlFetchersFlow.launchIn(this)
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ data class AccountList private constructor(
|
|||
when (account) {
|
||||
is Account.CryptoPortfolio -> account.cryptoCurrencies
|
||||
is Account.Payment -> emptyList()
|
||||
is Account.Virtual -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,6 +157,12 @@ data class AccountList private constructor(
|
|||
"$tag: The number of payment accounts must not exceed $MAX_PAYMENT_ACCOUNTS_COUNT"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object ExceedsMaxVirtualAccountsCount : Error {
|
||||
override fun toString(): String =
|
||||
"$tag: The number of virtual accounts must not exceed $MAX_VIRTUAL_ACCOUNTS_COUNT"
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object DuplicateAccountIds : Error {
|
||||
override fun toString(): String = "$tag: Account list contains duplicate account IDs"
|
||||
|
|
@ -175,6 +182,7 @@ data class AccountList private constructor(
|
|||
companion object {
|
||||
|
||||
const val MAX_PAYMENT_ACCOUNTS_COUNT = 1
|
||||
const val MAX_VIRTUAL_ACCOUNTS_COUNT = 1
|
||||
const val MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT = 20
|
||||
const val MAX_ARCHIVED_ACCOUNTS_COUNT = 1000
|
||||
private const val MAX_MAIN_ACCOUNTS_COUNT = 1
|
||||
|
|
@ -200,6 +208,9 @@ data class AccountList private constructor(
|
|||
val paymentAccounts = accounts.filterIsInstance<Account.Payment>()
|
||||
ensure(paymentAccounts.size <= MAX_PAYMENT_ACCOUNTS_COUNT) { Error.ExceedsMaxPaymentAccountsCount }
|
||||
|
||||
val virtualAccounts = accounts.filterIsInstance<Account.Virtual>()
|
||||
ensure(virtualAccounts.size <= MAX_VIRTUAL_ACCOUNTS_COUNT) { Error.ExceedsMaxVirtualAccountsCount }
|
||||
|
||||
val cryptoAccounts = accounts.filterIsInstance<Account.CryptoPortfolio>()
|
||||
ensure(cryptoAccounts.size <= MAX_CRYPTO_PORTFOLIO_ACCOUNTS_COUNT) { Error.ExceedsMaxAccountsCount }
|
||||
|
||||
|
|
|
|||
|
|
@ -63,5 +63,6 @@ fun AccountStatusList.hasMultiCurrencyAccount(): Boolean = accountStatuses.any {
|
|||
when (status) {
|
||||
is AccountStatus.CryptoPortfolio -> status.tokenList.flattenCurrencies().size > 1
|
||||
is AccountStatus.Payment -> false
|
||||
is AccountStatus.Virtual -> false
|
||||
}
|
||||
}
|
||||
|
|
@ -128,6 +128,14 @@ internal class AccountListTest {
|
|||
accounts = createAccounts(count = 21),
|
||||
expected = AccountList.Error.ExceedsMaxAccountsCount.left(),
|
||||
),
|
||||
CreateTestModel(
|
||||
accounts = listOf(
|
||||
Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
Account.Virtual(userWalletId),
|
||||
Account.Virtual(userWalletId),
|
||||
),
|
||||
expected = AccountList.Error.ExceedsMaxVirtualAccountsCount.left(),
|
||||
),
|
||||
CreateTestModel(
|
||||
accounts = listOf(
|
||||
createAccount(derivationIndex = 1),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountStatus
|
||||
import com.tangem.domain.models.account.VirtualAccountStatusValue
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
|
|
@ -93,6 +94,10 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
|
||||
private val logger = TangemLogger.withTag(TAG)
|
||||
|
||||
// VirtualAccount status pipeline lands in a follow-up PR; until then surface an unavailable status.
|
||||
private val Account.Virtual.errorVirtualAccountStatus: AccountStatus.Virtual
|
||||
get() = AccountStatus.Virtual(this, VirtualAccountStatusValue.Error.Unavailable)
|
||||
|
||||
override val fallback: Option<AccountStatusList> = none()
|
||||
|
||||
override fun produce(): Flow<AccountStatusList> {
|
||||
|
|
@ -185,6 +190,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
val accountStatuses = accountList.accounts.map { account ->
|
||||
when (account) {
|
||||
is Account.Payment -> paymentAccountStatus
|
||||
is Account.Virtual -> account.errorVirtualAccountStatus
|
||||
is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) {
|
||||
account.toEmptyAccountStatus()
|
||||
} else {
|
||||
|
|
@ -410,6 +416,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
|
||||
is AccountStatus.Payment -> accountStatus.value.totalFiatBalance
|
||||
is AccountStatus.Virtual -> accountStatus.value.totalFiatBalance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -435,6 +442,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
|||
)
|
||||
}
|
||||
is Account.Payment -> null
|
||||
is Account.Virtual -> null
|
||||
}
|
||||
},
|
||||
totalAccounts = accountList.totalAccounts,
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ sealed interface Account {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
data class Payment(
|
||||
data class Payment private constructor(
|
||||
override val accountId: AccountId,
|
||||
) : Account {
|
||||
override val accountName: AccountName.Custom = AccountName.Custom("Payment").getOrElse {
|
||||
|
|
@ -189,10 +189,26 @@ sealed interface Account {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Virtual private constructor(
|
||||
override val accountId: AccountId,
|
||||
) : Account {
|
||||
override val accountName: AccountName.Custom = AccountName.Custom("Virtual").getOrElse {
|
||||
error("Can not create account name for Virtual account with userWalletId = ${accountId.userWalletId}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(userWalletId: UserWalletId): Virtual {
|
||||
return Virtual(accountId = AccountId.forVirtualAccount(userWalletId = userWalletId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val Account.derivationIndex: DerivationIndex?
|
||||
get() = when (this) {
|
||||
is Account.CryptoPortfolio -> derivationIndex
|
||||
is Account.Payment -> null
|
||||
is Account.Virtual -> null
|
||||
}
|
||||
|
|
@ -38,6 +38,7 @@ data class AccountId private constructor(
|
|||
companion object {
|
||||
|
||||
const val PaymentAccountIdPrefix = "payment_"
|
||||
const val VirtualAccountIdPrefix = "virtual_"
|
||||
|
||||
private val sha256Digest: MessageDigest by lazy { MessageDigest.getInstance("SHA-256") }
|
||||
private val hexRegex = Regex("^[a-fA-F0-9]{64}$")
|
||||
|
|
@ -77,5 +78,9 @@ data class AccountId private constructor(
|
|||
fun forPaymentAccount(userWalletId: UserWalletId): AccountId {
|
||||
return AccountId(value = "$PaymentAccountIdPrefix$userWalletId", userWalletId = userWalletId)
|
||||
}
|
||||
|
||||
fun forVirtualAccount(userWalletId: UserWalletId): AccountId {
|
||||
return AccountId(value = "$VirtualAccountIdPrefix$userWalletId", userWalletId = userWalletId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,12 @@ sealed interface AccountStatus {
|
|||
override val account: Account.Payment,
|
||||
val value: PaymentAccountStatusValue,
|
||||
) : AccountStatus
|
||||
|
||||
@Serializable
|
||||
data class Virtual(
|
||||
override val account: Account.Virtual,
|
||||
val value: VirtualAccountStatusValue,
|
||||
) : AccountStatus
|
||||
}
|
||||
|
||||
fun Iterable<AccountStatus>.filterCryptoPortfolio(): List<AccountStatus.CryptoPortfolio> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.kyc.KycStatus
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Represents the various states a virtual account (VA) can have, encapsulating different information based on
|
||||
* the state. Mirrors [PaymentAccountStatusValue] but carries VA-specific states (no card-related variants).
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
*/
|
||||
@Serializable
|
||||
sealed class VirtualAccountStatusValue {
|
||||
abstract val source: StatusSource
|
||||
|
||||
/** The total fiat balance associated with this status. */
|
||||
val totalFiatBalance: TotalFiatBalance
|
||||
get() = when (this) {
|
||||
is Empty,
|
||||
is NotCreated,
|
||||
is UnderReview,
|
||||
is Provisioning,
|
||||
is CountryNotSupported,
|
||||
is Error,
|
||||
-> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source)
|
||||
is Loading -> TotalFiatBalance.Loading
|
||||
is Active -> {
|
||||
val rate = fiatRate ?: return TotalFiatBalance.Failed
|
||||
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the status with a new [source].
|
||||
*
|
||||
* @param source The new source of the status information.
|
||||
*/
|
||||
fun copySealed(source: StatusSource): VirtualAccountStatusValue {
|
||||
return when (this) {
|
||||
is UnderReview -> copy(source = source)
|
||||
is Provisioning -> copy(source = source)
|
||||
is Active -> copy(source = source)
|
||||
is Loading,
|
||||
is Empty,
|
||||
is NotCreated,
|
||||
is CountryNotSupported,
|
||||
is Error,
|
||||
-> this
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents an empty virtual account status when no specific state is available. */
|
||||
@Serializable
|
||||
data object Empty : VirtualAccountStatusValue() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Represents the Loading state of a virtual account, typically while fetching its details. */
|
||||
@Serializable
|
||||
data object Loading : VirtualAccountStatusValue() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Represents a state where the virtual account has not been created yet. */
|
||||
@Serializable
|
||||
data object NotCreated : VirtualAccountStatusValue() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where the virtual account is under review (KYC).
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
* @property kycStatus The current KYC status.
|
||||
* @property customerId The unique identifier of the customer.
|
||||
*/
|
||||
@Serializable
|
||||
data class UnderReview(
|
||||
override val source: StatusSource,
|
||||
val kycStatus: KycStatus,
|
||||
val customerId: String,
|
||||
) : VirtualAccountStatusValue()
|
||||
|
||||
/**
|
||||
* Represents a state where the virtual account is being provisioned on the backend (e.g. via Rain),
|
||||
* after KYC approval and terms acceptance.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
*/
|
||||
@Serializable
|
||||
data class Provisioning(override val source: StatusSource) : VirtualAccountStatusValue()
|
||||
|
||||
/** Represents a state where the user's country is not eligible for a virtual account. */
|
||||
@Serializable
|
||||
data object CountryNotSupported : VirtualAccountStatusValue() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a state where the virtual account is successfully loaded with complete information.
|
||||
*
|
||||
* @property source The source of the status information.
|
||||
* @property customerId The unique identifier of the customer.
|
||||
* @property currencyCode The code of the currency.
|
||||
* @property depositAddress The on-chain address for deposits, if available.
|
||||
* @property fiatBalance The fiat balance details.
|
||||
* @property cryptoBalance The crypto balance details.
|
||||
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap.
|
||||
* @property cryptoCurrency The crypto currency held in the account (e.g. USDC).
|
||||
* @property fiatRate Exchange rate of [cryptoCurrency] to the app's selected fiat currency,
|
||||
* or `null` if the quote is not yet available. When `null`,
|
||||
* [totalFiatBalance] resolves to [TotalFiatBalance.Failed].
|
||||
*/
|
||||
@Serializable
|
||||
data class Active(
|
||||
override val source: StatusSource,
|
||||
val customerId: String,
|
||||
val currencyCode: String,
|
||||
val depositAddress: String?,
|
||||
val fiatBalance: FiatBalance,
|
||||
val cryptoBalance: CryptoBalance,
|
||||
val availableForWithdrawal: SerializedBigDecimal,
|
||||
val cryptoCurrency: CryptoCurrency.Token,
|
||||
val fiatRate: SerializedBigDecimal?,
|
||||
) : VirtualAccountStatusValue() {
|
||||
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
|
||||
currency = cryptoCurrency,
|
||||
value = buildCryptoCurrencyStatusValue(
|
||||
amount = availableForWithdrawal,
|
||||
fiatAmount = fiatBalance.availableBalance,
|
||||
fiatRate = fiatRate,
|
||||
depositAddress = cryptoBalance.depositAddress,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/** Represents an error state for the virtual account status. */
|
||||
@Serializable
|
||||
sealed class Error : VirtualAccountStatusValue() {
|
||||
/** Error state indicating the device is exposed. */
|
||||
@Serializable
|
||||
data object ExposedDevice : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Error state indicating the account is unavailable. */
|
||||
@Serializable
|
||||
data object Unavailable : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
|
||||
/** Error state indicating the account data is not synced. */
|
||||
@Serializable
|
||||
data object NotSynced : Error() {
|
||||
override val source: StatusSource = StatusSource.ACTUAL
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the fiat balance of the virtual account.
|
||||
*
|
||||
* @property availableBalance The amount of available balance in fiat.
|
||||
* @property currency The currency of the balance.
|
||||
*/
|
||||
@Serializable
|
||||
data class FiatBalance(val availableBalance: SerializedBigDecimal, val currency: String)
|
||||
|
||||
/**
|
||||
* Represents the crypto balance of the virtual account.
|
||||
*
|
||||
* @property id The unique identifier of the crypto asset.
|
||||
* @property chainId The identifier of the blockchain network.
|
||||
* @property depositAddress The on-chain address for deposits.
|
||||
* @property tokenContractAddress The contract address of the token.
|
||||
* @property balance The amount of the crypto balance.
|
||||
*/
|
||||
@Serializable
|
||||
data class CryptoBalance(
|
||||
val id: String,
|
||||
val chainId: Long,
|
||||
val depositAddress: String,
|
||||
val tokenContractAddress: String,
|
||||
val balance: SerializedBigDecimal,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildCryptoCurrencyStatusValue(
|
||||
amount: SerializedBigDecimal,
|
||||
fiatAmount: SerializedBigDecimal,
|
||||
fiatRate: SerializedBigDecimal?,
|
||||
depositAddress: String,
|
||||
): CryptoCurrencyStatus.Value {
|
||||
val networkAddress = NetworkAddress.Single(
|
||||
defaultAddress = NetworkAddress.Address(
|
||||
type = NetworkAddress.Address.Type.Primary,
|
||||
value = depositAddress,
|
||||
),
|
||||
)
|
||||
return if (fiatRate != null) {
|
||||
CryptoCurrencyStatus.Loaded(
|
||||
amount = amount,
|
||||
fiatAmount = fiatAmount,
|
||||
fiatRate = fiatRate,
|
||||
priceChange = BigDecimal.ZERO,
|
||||
networkAddress = networkAddress,
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
pendingTransactions = emptySet(),
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
)
|
||||
} else {
|
||||
CryptoCurrencyStatus.NoQuote(
|
||||
amount = amount,
|
||||
networkAddress = networkAddress,
|
||||
stakingBalance = null,
|
||||
yieldSupplyStatus = null,
|
||||
hasCurrentNetworkTransactions = false,
|
||||
pendingTransactions = emptySet(),
|
||||
sources = CryptoCurrencyStatus.Sources(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.tangem.domain.models.account
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Verifies that [VirtualAccountStatusValue.Active] converts its fiat balance to the app's selected currency
|
||||
* via [VirtualAccountStatusValue.Active.fiatRate] (mirror of the Payment account fix, [REDACTED_TASK_KEY]).
|
||||
*/
|
||||
class VirtualAccountStatusValueTest {
|
||||
|
||||
private val cryptoCurrency = mockk<CryptoCurrency.Token>(relaxed = true)
|
||||
|
||||
private fun activeWith(fiatRate: BigDecimal?, balance: BigDecimal = BigDecimal("100")) =
|
||||
VirtualAccountStatusValue.Active(
|
||||
source = StatusSource.ACTUAL,
|
||||
customerId = "customer",
|
||||
currencyCode = "USD",
|
||||
depositAddress = "0xabc",
|
||||
fiatBalance = VirtualAccountStatusValue.FiatBalance(availableBalance = balance, currency = "USD"),
|
||||
cryptoBalance = VirtualAccountStatusValue.CryptoBalance(
|
||||
id = "usd-coin",
|
||||
chainId = 137L,
|
||||
depositAddress = "0xabc",
|
||||
tokenContractAddress = "0xdef",
|
||||
balance = balance,
|
||||
),
|
||||
availableForWithdrawal = balance,
|
||||
cryptoCurrency = cryptoCurrency,
|
||||
fiatRate = fiatRate,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `totalFiatBalance converts balance via fiatRate when rate is present`() {
|
||||
// Arrange
|
||||
val rate = BigDecimal("0.9")
|
||||
val active = activeWith(fiatRate = rate, balance = BigDecimal("100"))
|
||||
|
||||
// Act
|
||||
val result = active.totalFiatBalance
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(result).isInstanceOf(TotalFiatBalance.Loaded::class.java)
|
||||
Truth.assertThat((result as TotalFiatBalance.Loaded).amount)
|
||||
.isEqualTo(BigDecimal("100").multiply(rate))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `totalFiatBalance is Failed when fiatRate is null`() {
|
||||
// Arrange
|
||||
val active = activeWith(fiatRate = null)
|
||||
|
||||
// Act & Assert
|
||||
Truth.assertThat(active.totalFiatBalance).isEqualTo(TotalFiatBalance.Failed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cryptoCurrencyStatus is NoQuote when fiatRate is null`() {
|
||||
// Arrange
|
||||
val active = activeWith(fiatRate = null)
|
||||
|
||||
// Act & Assert
|
||||
Truth.assertThat(active.cryptoCurrencyStatus.value)
|
||||
.isInstanceOf(CryptoCurrencyStatus.NoQuote::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cryptoCurrencyStatus is Loaded with the rate when fiatRate is present`() {
|
||||
// Arrange
|
||||
val rate = BigDecimal("0.9")
|
||||
val active = activeWith(fiatRate = rate)
|
||||
|
||||
// Act
|
||||
val value = active.cryptoCurrencyStatus.value
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(value).isInstanceOf(CryptoCurrencyStatus.Loaded::class.java)
|
||||
Truth.assertThat((value as CryptoCurrencyStatus.Loaded).fiatRate).isEqualTo(rate)
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ internal class ChooseTokenListItemConverter(
|
|||
when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.toPortfolioItem(params)
|
||||
is AccountStatus.Payment -> accountStatus.createPaymentAccountItem(params.expandedAccounts)
|
||||
is AccountStatus.Virtual -> null
|
||||
}
|
||||
}
|
||||
.filter { portfolio -> portfolio.tokens.isNotEmpty() }
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
|||
import com.tangem.common.ui.account.PortfolioSelectUM
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.addtoken.AddTokenUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.extensions.iconResId
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.common.ui.extensions.iconResId
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
|
||||
|
|
@ -51,6 +51,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor(
|
|||
accountIcon = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon)
|
||||
is AccountStatus.Payment -> AccountIconUM.Payment
|
||||
is AccountStatus.Virtual -> AccountIconUM.Virtual
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ internal class SendDestinationModel @Inject constructor(
|
|||
when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.getDestinationWalletUM(wallet)
|
||||
is AccountStatus.Payment -> listOfNotNull(accountStatus.getDestinationWalletUM(wallet))
|
||||
is AccountStatus.Virtual -> emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ internal class SendRecipientWalletListConverter(
|
|||
account.icon,
|
||||
)
|
||||
is Account.Payment -> AccountIconUM.Payment
|
||||
is Account.Virtual -> AccountIconUM.Virtual
|
||||
},
|
||||
prefixText = stringReference(StringsSigns.DOT),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ internal class InitialCurrenciesResolver @Inject constructor(
|
|||
val currencyStatuses = when (accountStatus) {
|
||||
is AccountStatus.CryptoPortfolio -> accountStatus.flattenCurrencies()
|
||||
is AccountStatus.Payment -> getPaymentAccountCurrencies(accountStatus)
|
||||
// Virtual account isn't a swap source in the MVP (withdrawal reuses the send flow)
|
||||
is AccountStatus.Virtual -> emptyList()
|
||||
}
|
||||
val availabilityStates = rampStateManager.availableForSwap(
|
||||
userWalletId,
|
||||
|
|
|
|||
|
|
@ -1571,6 +1571,8 @@ internal class SwapModel @Inject constructor(
|
|||
userWalletId = swapCurrencyStatus.userWalletId,
|
||||
cryptoCurrency = swapCurrency,
|
||||
).map { (_, status) -> status }
|
||||
// Virtual account isn't a swap source in the MVP (withdrawal reuses the send flow)
|
||||
is Account.Virtual -> emptyFlow()
|
||||
}.distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes
|
||||
.onEach { currencyStatus ->
|
||||
|
||||
|
|
|
|||
|
|
@ -1265,6 +1265,7 @@ internal class StateBuilder(
|
|||
return when (this) {
|
||||
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon)
|
||||
is Account.Payment -> AccountIconUM.Payment
|
||||
is Account.Virtual -> AccountIconUM.Virtual
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ internal class SwapTransferStateBuilder @Inject constructor(
|
|||
return when (this) {
|
||||
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon)
|
||||
is Account.Payment -> AccountIconUM.Payment
|
||||
is Account.Virtual -> AccountIconUM.Virtual
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.features.virtualaccount
|
||||
|
||||
interface VirtualAccountFeatureToggles {
|
||||
val isVirtualAccountsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
|
|
@ -9,4 +11,13 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.virtualAccounts.details.api)
|
||||
|
||||
implementation(projects.core.configToggles)
|
||||
|
||||
implementation(deps.compose.runtime)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -1,15 +1,12 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.impl
|
||||
package com.tangem.features.virtualaccount
|
||||
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.virtualaccount.onboarding.api.VirtualAccountsFeatureToggles
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class DefaultVirtualAccountsFeatureToggles
|
||||
@Inject constructor(
|
||||
internal class DefaultVirtualAccountFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : VirtualAccountsFeatureToggles {
|
||||
|
||||
) : VirtualAccountFeatureToggles {
|
||||
override val isVirtualAccountsEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(toggle = FeatureToggles.VIRTUAL_ACCOUNTS_ENABLED)
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.features.virtualaccount.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.virtualaccount.DefaultVirtualAccountFeatureToggles
|
||||
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object VirtualAccountDetailsModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVirtualAccountFeatureToggles(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): VirtualAccountFeatureToggles {
|
||||
return DefaultVirtualAccountFeatureToggles(featureTogglesManager = featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.api
|
||||
|
||||
interface VirtualAccountsFeatureToggles {
|
||||
val isVirtualAccountsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.features.virtualaccount.onboarding.impl.di
|
||||
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.features.virtualaccount.onboarding.api.VirtualAccountsFeatureToggles
|
||||
import com.tangem.features.virtualaccount.onboarding.impl.DefaultVirtualAccountsFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object VirtualAccountsOnboardingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVirtualAccountsFeatureToggles(
|
||||
featureTogglesManager: FeatureTogglesManager,
|
||||
): VirtualAccountsFeatureToggles {
|
||||
return DefaultVirtualAccountsFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue