Updated on 2026-08-14
This commit is contained in:
parent
24d7953389
commit
8683516c59
24 changed files with 817 additions and 148 deletions
|
|
@ -316,6 +316,12 @@ dependencies {
|
||||||
implementation(projects.features.tangempay.main.impl)
|
implementation(projects.features.tangempay.main.impl)
|
||||||
implementation(projects.features.tangempay.onboarding.api)
|
implementation(projects.features.tangempay.onboarding.api)
|
||||||
implementation(projects.features.tangempay.onboarding.impl)
|
implementation(projects.features.tangempay.onboarding.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.onboarding.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.onboarding.api)
|
||||||
|
implementation(projects.features.virtualAccounts.main.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.main.api)
|
||||||
|
implementation(projects.features.virtualAccounts.details.impl)
|
||||||
|
implementation(projects.features.virtualAccounts.details.api)
|
||||||
implementation(projects.features.tokenRecieve.api)
|
implementation(projects.features.tokenRecieve.api)
|
||||||
implementation(projects.features.tokenRecieve.impl)
|
implementation(projects.features.tokenRecieve.impl)
|
||||||
implementation(projects.features.yieldSupply.api)
|
implementation(projects.features.yieldSupply.api)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||||
|
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
|
||||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||||
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
|
|
@ -10,19 +11,20 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||||
import com.tangem.domain.networks.repository.NetworksRepository
|
import com.tangem.domain.networks.repository.NetworksRepository
|
||||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||||
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
|
||||||
import com.tangem.domain.stories.StoriesRepository
|
|
||||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||||
import com.tangem.domain.staking.StakingIdFactory
|
import com.tangem.domain.staking.StakingIdFactory
|
||||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||||
import com.tangem.domain.staking.repositories.StakingRepository
|
import com.tangem.domain.staking.repositories.StakingRepository
|
||||||
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
|
import com.tangem.domain.stories.StoriesRepository
|
||||||
import com.tangem.domain.tokens.*
|
import com.tangem.domain.tokens.*
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
import com.tangem.domain.tokens.repository.CurrencyChecksRepository
|
||||||
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository
|
||||||
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository
|
||||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
|
import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -162,6 +164,8 @@ internal object TokensDomainModule {
|
||||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher: VirtualAccountStatusFetcher,
|
||||||
|
virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
|
||||||
stakingIdFactory: StakingIdFactory,
|
stakingIdFactory: StakingIdFactory,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): WalletBalanceFetcher {
|
): WalletBalanceFetcher {
|
||||||
|
|
@ -175,6 +179,8 @@ internal object TokensDomainModule {
|
||||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher = virtualAccountStatusFetcher,
|
||||||
|
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||||
stakingIdFactory = stakingIdFactory,
|
stakingIdFactory = stakingIdFactory,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.adapter.*
|
||||||
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
||||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||||
|
import com.tangem.datasource.local.visa.entity.VirtualAccountStatusValueDM
|
||||||
import com.tangem.datasource.utils.SerializeNullsFactory
|
import com.tangem.datasource.utils.SerializeNullsFactory
|
||||||
import com.tangem.domain.models.scan.serialization.*
|
import com.tangem.domain.models.scan.serialization.*
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -56,6 +57,18 @@ class MoshiModule {
|
||||||
.withSubtype(PaymentAccountStatusValueDM.DeactivatedAccount::class.java, "deactivated_account")
|
.withSubtype(PaymentAccountStatusValueDM.DeactivatedAccount::class.java, "deactivated_account")
|
||||||
.withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
|
.withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
|
||||||
)
|
)
|
||||||
|
.add(
|
||||||
|
NamePolymorphicAdapterFactory.of(VirtualAccountStatusValueDM::class.java)
|
||||||
|
.withSubtype(VirtualAccountStatusValueDM.Empty::class.java, "empty")
|
||||||
|
.withSubtype(VirtualAccountStatusValueDM.NotCreated::class.java, "not_created")
|
||||||
|
.withSubtype(VirtualAccountStatusValueDM.UnderReview::class.java, "kyc_status")
|
||||||
|
.withSubtype(VirtualAccountStatusValueDM.Provisioning::class.java, "provisioning")
|
||||||
|
.withSubtype(
|
||||||
|
VirtualAccountStatusValueDM.CountryNotSupported::class.java,
|
||||||
|
"country_not_supported",
|
||||||
|
)
|
||||||
|
.withSubtype(VirtualAccountStatusValueDM.ActiveAccount::class.java, "active_account"),
|
||||||
|
)
|
||||||
.add(
|
.add(
|
||||||
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
|
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
|
||||||
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
|
.withSubtype(NFTCollection.Identifier.EVM::class.java, "evm")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
@file:Suppress("BooleanPropertyNaming")
|
||||||
|
package com.tangem.datasource.local.visa.entity
|
||||||
|
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.tangem.domain.models.kyc.KycStatus
|
||||||
|
import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType
|
||||||
|
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual account status for storage in the local cache.
|
||||||
|
*
|
||||||
|
* @see [com.tangem.domain.models.account.AccountStatus.Virtual]
|
||||||
|
*/
|
||||||
|
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
|
||||||
|
sealed interface VirtualAccountStatusValueDM {
|
||||||
|
|
||||||
|
@NameLabel("empty")
|
||||||
|
data class Empty(
|
||||||
|
@Json(name = "empty") val marker: Boolean = true,
|
||||||
|
) : VirtualAccountStatusValueDM
|
||||||
|
|
||||||
|
@NameLabel("not_created")
|
||||||
|
data class NotCreated(
|
||||||
|
@Json(name = "not_created") val marker: Boolean = true,
|
||||||
|
) : VirtualAccountStatusValueDM
|
||||||
|
|
||||||
|
@NameLabel("kyc_status")
|
||||||
|
data class UnderReview(
|
||||||
|
@Json(name = "kyc_status") val kycStatus: KycStatus,
|
||||||
|
@Json(name = "customer_id") val customerId: String,
|
||||||
|
) : VirtualAccountStatusValueDM
|
||||||
|
|
||||||
|
@NameLabel("provisioning")
|
||||||
|
data class Provisioning(
|
||||||
|
@Json(name = "provisioning") val marker: Boolean = true,
|
||||||
|
) : VirtualAccountStatusValueDM
|
||||||
|
|
||||||
|
@NameLabel("country_not_supported")
|
||||||
|
data class CountryNotSupported(
|
||||||
|
@Json(name = "country_not_supported") val marker: Boolean = true,
|
||||||
|
) : VirtualAccountStatusValueDM
|
||||||
|
|
||||||
|
@NameLabel("active_account")
|
||||||
|
data class ActiveAccount(
|
||||||
|
@Json(name = "active_account") val marker: Boolean = true,
|
||||||
|
@Json(name = "customer_id") val customerId: String,
|
||||||
|
@Json(name = "currency_code") val currencyCode: String,
|
||||||
|
@Json(name = "deposit_address") val depositAddress: String?,
|
||||||
|
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
|
||||||
|
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
|
||||||
|
@Json(name = "fiat_rate") val fiatRate: BigDecimal?,
|
||||||
|
@Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal,
|
||||||
|
) : VirtualAccountStatusValueDM
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class FiatBalanceDM(
|
||||||
|
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||||
|
@Json(name = "currency") val currency: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class CryptoBalanceDM(
|
||||||
|
@Json(name = "id") val id: String,
|
||||||
|
@Json(name = "chain_id") val chainId: Long,
|
||||||
|
@Json(name = "deposit_address") val depositAddress: String,
|
||||||
|
@Json(name = "token_contract_address") val tokenContractAddress: String,
|
||||||
|
@Json(name = "balance") val balance: BigDecimal,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package com.tangem.datasource.local.visa.entity
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import com.squareup.moshi.adapter
|
||||||
|
import com.tangem.datasource.di.MoshiModule
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that the network Moshi (see [MoshiModule.provideNetworkMoshi]) resolves and round-trips the
|
||||||
|
* polymorphic [VirtualAccountStatusValueDM] adapter.
|
||||||
|
*
|
||||||
|
* Guards against the missing `NamePolymorphicAdapterFactory` registration that caused a runtime
|
||||||
|
* `ClassNotFoundException: ...VirtualAccountStatusValueDMJsonAdapter` (the adapter is registered manually,
|
||||||
|
* not generated).
|
||||||
|
*/
|
||||||
|
class VirtualAccountStatusValueDMSerializationTest {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private val adapter = MoshiModule().provideNetworkMoshi().adapter<VirtualAccountStatusValueDM>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `round-trip NotCreated`() {
|
||||||
|
val model: VirtualAccountStatusValueDM = VirtualAccountStatusValueDM.NotCreated()
|
||||||
|
|
||||||
|
val restored = adapter.fromJson(adapter.toJson(model))
|
||||||
|
|
||||||
|
Truth.assertThat(restored).isInstanceOf(VirtualAccountStatusValueDM.NotCreated::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `round-trip Provisioning`() {
|
||||||
|
val model: VirtualAccountStatusValueDM = VirtualAccountStatusValueDM.Provisioning()
|
||||||
|
|
||||||
|
val restored = adapter.fromJson(adapter.toJson(model))
|
||||||
|
|
||||||
|
Truth.assertThat(restored).isInstanceOf(VirtualAccountStatusValueDM.Provisioning::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `round-trip CountryNotSupported`() {
|
||||||
|
val model: VirtualAccountStatusValueDM = VirtualAccountStatusValueDM.CountryNotSupported()
|
||||||
|
|
||||||
|
val restored = adapter.fromJson(adapter.toJson(model))
|
||||||
|
|
||||||
|
Truth.assertThat(restored).isInstanceOf(VirtualAccountStatusValueDM.CountryNotSupported::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `round-trip ActiveAccount`() {
|
||||||
|
val model: VirtualAccountStatusValueDM = VirtualAccountStatusValueDM.ActiveAccount(
|
||||||
|
customerId = "cust-1",
|
||||||
|
currencyCode = "USD",
|
||||||
|
depositAddress = "0xabc",
|
||||||
|
fiatBalance = VirtualAccountStatusValueDM.FiatBalanceDM(
|
||||||
|
availableBalance = BigDecimal("101.56"),
|
||||||
|
currency = "USD",
|
||||||
|
),
|
||||||
|
cryptoBalance = VirtualAccountStatusValueDM.CryptoBalanceDM(
|
||||||
|
id = "usd-coin",
|
||||||
|
chainId = 137L,
|
||||||
|
depositAddress = "0xabc",
|
||||||
|
tokenContractAddress = "0xdef",
|
||||||
|
balance = BigDecimal("101.56"),
|
||||||
|
),
|
||||||
|
fiatRate = BigDecimal("0.95"),
|
||||||
|
availableForWithdrawal = BigDecimal("100.00"),
|
||||||
|
)
|
||||||
|
|
||||||
|
val restored = adapter.fromJson(adapter.toJson(model))
|
||||||
|
|
||||||
|
Truth.assertThat(restored).isEqualTo(model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ android {
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
implementation(projects.features.virtualAccounts.details.api) // VIRTUAL_ACCOUNTS_ENABLED
|
||||||
|
|
||||||
// region Project - Common
|
// region Project - Common
|
||||||
implementation(projects.common.ui) // It's needed for getting AccountName.DefaultMain value
|
implementation(projects.common.ui) // It's needed for getting AccountName.DefaultMain value
|
||||||
// endregion
|
// endregion
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
internal fun String.toAccountId(userWalletId: UserWalletId): AccountId {
|
internal fun String.toAccountId(userWalletId: UserWalletId): AccountId {
|
||||||
return when {
|
return when {
|
||||||
startsWith(AccountId.PaymentAccountIdPrefix) -> AccountId.forPaymentAccount(userWalletId).right()
|
startsWith(AccountId.PaymentAccountIdPrefix) -> AccountId.forPaymentAccount(userWalletId).right()
|
||||||
|
startsWith(AccountId.VirtualAccountIdPrefix) -> AccountId.forVirtualAccount(userWalletId).right()
|
||||||
else -> AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId)
|
else -> AccountId.forCryptoPortfolio(value = this, userWalletId = userWalletId)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
error("Unable to create AccountId from value: $this. Cause: $it")
|
error("Unable to create AccountId from value: $this. Cause: $it")
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.tangem.domain.common.wallets.getSyncStrict
|
||||||
import com.tangem.domain.core.flow.FlowProducerTools
|
import com.tangem.domain.core.flow.FlowProducerTools
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.hot.sdk.model.HotWalletId
|
import com.tangem.hot.sdk.model.HotWalletId
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
|
@ -37,6 +38,7 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
||||||
override val flowProducerTools: FlowProducerTools,
|
override val flowProducerTools: FlowProducerTools,
|
||||||
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
|
private val walletAccountListFlowFactory: WalletAccountListFlowFactory,
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
|
private val virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : SingleAccountListProducer {
|
) : SingleAccountListProducer {
|
||||||
|
|
||||||
|
|
@ -51,18 +53,9 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
||||||
return walletAccountListFlowFactory.create(walletId)
|
return walletAccountListFlowFactory.create(walletId)
|
||||||
.map { accountList ->
|
.map { accountList ->
|
||||||
val userWallet = userWalletsListRepository.getSyncStrict(id = walletId)
|
val userWallet = userWalletsListRepository.getSyncStrict(id = walletId)
|
||||||
val isPaymentSupported = userWallet.isPaymentAccountSupported()
|
accountList
|
||||||
logger.i(
|
.addAccountIf(userWallet.isPaymentAccountSupported()) { Account.Payment(walletId) }
|
||||||
"produce()[$walletId]: userWallet resolved (type=${userWallet::class.simpleName}), " +
|
.addAccountIf(userWallet.isVirtualAccountSupported()) { Account.Virtual(walletId) }
|
||||||
"isPaymentAccountSupported=$isPaymentSupported",
|
|
||||||
)
|
|
||||||
if (isPaymentSupported) {
|
|
||||||
accountList.plus(Account.Payment(walletId)).getOrElse { throwable ->
|
|
||||||
error("Can not combine account list and payment account status: $throwable")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
accountList
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.flowOn(dispatchers.default)
|
.flowOn(dispatchers.default)
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +65,25 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor(
|
||||||
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun UserWallet.isVirtualAccountSupported(): Boolean {
|
||||||
|
if (!virtualAccountsFeatureToggles.isVirtualAccountsEnabled) return false
|
||||||
|
|
||||||
|
return when (this) {
|
||||||
|
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
||||||
|
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun AccountList.addAccountIf(condition: Boolean, account: () -> Account): AccountList {
|
||||||
|
return if (condition) {
|
||||||
|
plus(account()).getOrElse { throwable ->
|
||||||
|
error("Can not combine account list and special account: $throwable")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : SingleAccountListProducer.Factory {
|
interface Factory : SingleAccountListProducer.Factory {
|
||||||
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
|
override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
package com.tangem.data.account.producer
|
package com.tangem.data.account.producer
|
||||||
|
|
||||||
|
import arrow.core.getOrElse
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import com.tangem.domain.account.models.AccountList
|
import com.tangem.domain.account.models.AccountList
|
||||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||||
import com.tangem.domain.core.flow.FlowProducerTools
|
import com.tangem.domain.core.flow.FlowProducerTools
|
||||||
import com.tangem.domain.models.TokensSortType
|
import com.tangem.domain.models.TokensSortType
|
||||||
|
import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.hot.sdk.model.HotWalletId
|
import com.tangem.hot.sdk.model.HotWalletId
|
||||||
import com.tangem.test.core.getEmittedValues
|
import com.tangem.test.core.getEmittedValues
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
|
@ -40,12 +43,16 @@ class DefaultSingleAccountListProducerTest {
|
||||||
private val userWalletsListRepository = mockk<UserWalletsListRepository> {
|
private val userWalletsListRepository = mockk<UserWalletsListRepository> {
|
||||||
every { userWallets } returns MutableStateFlow<List<UserWallet>?>(value = listOf(userWallet))
|
every { userWallets } returns MutableStateFlow<List<UserWallet>?>(value = listOf(userWallet))
|
||||||
}
|
}
|
||||||
|
private val virtualAccountsFeatureToggles = mockk<VirtualAccountFeatureToggles> {
|
||||||
|
every { isVirtualAccountsEnabled } returns false
|
||||||
|
}
|
||||||
|
|
||||||
private val producer = DefaultSingleAccountListProducer(
|
private val producer = DefaultSingleAccountListProducer(
|
||||||
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||||
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||||
flowProducerTools = flowProducerTools,
|
flowProducerTools = flowProducerTools,
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -72,6 +79,45 @@ class DefaultSingleAccountListProducerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `GIVEN virtual accounts enabled WHEN produce THEN account list contains virtual account`() = runTest {
|
||||||
|
// Arrange
|
||||||
|
val supportedWallet = mockk<UserWallet.Hot> {
|
||||||
|
every { walletId } returns userWalletId
|
||||||
|
every { hotWalletId } returns mockk {
|
||||||
|
every { authType } returns HotWalletId.AuthType.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val userWalletsListRepository = mockk<UserWalletsListRepository> {
|
||||||
|
every { userWallets } returns MutableStateFlow<List<UserWallet>?>(value = listOf(supportedWallet))
|
||||||
|
}
|
||||||
|
val virtualAccountsFeatureToggles = mockk<VirtualAccountFeatureToggles> {
|
||||||
|
every { isVirtualAccountsEnabled } returns true
|
||||||
|
}
|
||||||
|
val producer = DefaultSingleAccountListProducer(
|
||||||
|
params = SingleAccountListProducer.Params(userWalletId = userWalletId),
|
||||||
|
walletAccountListFlowFactory = walletAccountListFlowFactory,
|
||||||
|
flowProducerTools = flowProducerTools,
|
||||||
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||||
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val accountList = AccountList.empty(userWalletId)
|
||||||
|
every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val actual = producer.produce().let(::getEmittedValues)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
val expected = accountList
|
||||||
|
.plus(Account.Payment(userWalletId))
|
||||||
|
.getOrElse { error("Unable to add payment account: $it") }
|
||||||
|
.plus(Account.Virtual(userWalletId))
|
||||||
|
.getOrElse { error("Unable to add virtual account: $it") }
|
||||||
|
Truth.assertThat(actual).containsExactly(expected)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `flow will updated if factoryFlow is updated`() = runTest {
|
fun `flow will updated if factoryFlow is updated`() = runTest {
|
||||||
// Arrange
|
// Arrange
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
package com.tangem.data.virtualaccount.converter
|
||||||
|
|
||||||
|
import com.tangem.datasource.local.visa.entity.VirtualAccountStatusValueDM
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
import com.tangem.domain.models.account.VirtualAccountStatusValue
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.pay.TangemPayCurrencyFactory
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-way converter between [VirtualAccountStatusValue] and [VirtualAccountStatusValueDM].
|
||||||
|
*
|
||||||
|
* [convert] maps domain → data model. Returns null for transient statuses that should not be persisted
|
||||||
|
* (Loading, ExposedDevice, Unavailable, NotSynced).
|
||||||
|
*
|
||||||
|
* [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source.
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
internal class VirtualAccountStatusValueDMConverter @Inject constructor(
|
||||||
|
private val tangemPayCurrencyFactory: TangemPayCurrencyFactory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun convert(value: VirtualAccountStatusValue): VirtualAccountStatusValueDM? {
|
||||||
|
return when (value) {
|
||||||
|
is VirtualAccountStatusValue.Empty -> VirtualAccountStatusValueDM.Empty()
|
||||||
|
is VirtualAccountStatusValue.NotCreated -> VirtualAccountStatusValueDM.NotCreated()
|
||||||
|
is VirtualAccountStatusValue.UnderReview -> VirtualAccountStatusValueDM.UnderReview(
|
||||||
|
kycStatus = value.kycStatus,
|
||||||
|
customerId = value.customerId,
|
||||||
|
)
|
||||||
|
is VirtualAccountStatusValue.Provisioning -> VirtualAccountStatusValueDM.Provisioning()
|
||||||
|
is VirtualAccountStatusValue.CountryNotSupported -> VirtualAccountStatusValueDM.CountryNotSupported()
|
||||||
|
is VirtualAccountStatusValue.Active -> VirtualAccountStatusValueDM.ActiveAccount(
|
||||||
|
customerId = value.customerId,
|
||||||
|
currencyCode = value.currencyCode,
|
||||||
|
depositAddress = value.depositAddress,
|
||||||
|
fiatBalance = value.fiatBalance.toDM(),
|
||||||
|
cryptoBalance = value.cryptoBalance.toDM(),
|
||||||
|
fiatRate = value.fiatRate,
|
||||||
|
availableForWithdrawal = value.availableForWithdrawal,
|
||||||
|
)
|
||||||
|
// Transient statuses are not persisted
|
||||||
|
is VirtualAccountStatusValue.Loading,
|
||||||
|
is VirtualAccountStatusValue.Error.ExposedDevice,
|
||||||
|
is VirtualAccountStatusValue.Error.Unavailable,
|
||||||
|
is VirtualAccountStatusValue.Error.NotSynced,
|
||||||
|
-> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertBack(userWalletId: UserWalletId, value: VirtualAccountStatusValueDM?): VirtualAccountStatusValue {
|
||||||
|
return when (value) {
|
||||||
|
is VirtualAccountStatusValueDM.Empty -> VirtualAccountStatusValue.Empty
|
||||||
|
is VirtualAccountStatusValueDM.NotCreated -> VirtualAccountStatusValue.NotCreated
|
||||||
|
is VirtualAccountStatusValueDM.Provisioning -> VirtualAccountStatusValue.Provisioning(
|
||||||
|
source = StatusSource.CACHE,
|
||||||
|
)
|
||||||
|
is VirtualAccountStatusValueDM.CountryNotSupported -> VirtualAccountStatusValue.CountryNotSupported
|
||||||
|
is VirtualAccountStatusValueDM.UnderReview -> VirtualAccountStatusValue.UnderReview(
|
||||||
|
source = StatusSource.CACHE,
|
||||||
|
kycStatus = value.kycStatus,
|
||||||
|
customerId = value.customerId,
|
||||||
|
)
|
||||||
|
is VirtualAccountStatusValueDM.ActiveAccount -> VirtualAccountStatusValue.Active(
|
||||||
|
source = StatusSource.CACHE,
|
||||||
|
customerId = value.customerId,
|
||||||
|
currencyCode = value.currencyCode,
|
||||||
|
depositAddress = value.depositAddress,
|
||||||
|
fiatBalance = value.fiatBalance.toDomain(),
|
||||||
|
cryptoBalance = value.cryptoBalance.toDomain(),
|
||||||
|
availableForWithdrawal = value.availableForWithdrawal,
|
||||||
|
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
|
||||||
|
fiatRate = value.fiatRate,
|
||||||
|
)
|
||||||
|
null -> VirtualAccountStatusValue.Error.Unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VirtualAccountStatusValue.FiatBalance.toDM(): VirtualAccountStatusValueDM.FiatBalanceDM {
|
||||||
|
return VirtualAccountStatusValueDM.FiatBalanceDM(
|
||||||
|
availableBalance = availableBalance,
|
||||||
|
currency = currency,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VirtualAccountStatusValue.CryptoBalance.toDM(): VirtualAccountStatusValueDM.CryptoBalanceDM {
|
||||||
|
return VirtualAccountStatusValueDM.CryptoBalanceDM(
|
||||||
|
id = id,
|
||||||
|
chainId = chainId,
|
||||||
|
depositAddress = depositAddress,
|
||||||
|
tokenContractAddress = tokenContractAddress,
|
||||||
|
balance = balance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VirtualAccountStatusValueDM.FiatBalanceDM.toDomain(): VirtualAccountStatusValue.FiatBalance {
|
||||||
|
return VirtualAccountStatusValue.FiatBalance(
|
||||||
|
availableBalance = availableBalance,
|
||||||
|
currency = currency,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun VirtualAccountStatusValueDM.CryptoBalanceDM.toDomain(): VirtualAccountStatusValue.CryptoBalance {
|
||||||
|
return VirtualAccountStatusValue.CryptoBalance(
|
||||||
|
id = id,
|
||||||
|
chainId = chainId,
|
||||||
|
depositAddress = depositAddress,
|
||||||
|
tokenContractAddress = tokenContractAddress,
|
||||||
|
balance = balance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.tangem.data.virtualaccount.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.core.DataStoreFactory
|
||||||
|
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||||
|
import androidx.datastore.dataStoreFile
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.tangem.data.virtualaccount.converter.VirtualAccountStatusValueDMConverter
|
||||||
|
import com.tangem.data.virtualaccount.flow.DefaultVirtualAccountStatusFetcher
|
||||||
|
import com.tangem.data.virtualaccount.flow.DefaultVirtualAccountStatusProducer
|
||||||
|
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
|
||||||
|
import com.tangem.datasource.di.NetworkMoshi
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.datasource.local.visa.entity.VirtualAccountStatusValueDM
|
||||||
|
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||||
|
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusProducer
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusSupplier
|
||||||
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal interface VirtualAccountDataModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindVirtualAccountStatusProducerFactory(
|
||||||
|
impl: DefaultVirtualAccountStatusProducer.Factory,
|
||||||
|
): VirtualAccountStatusProducer.Factory
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindVirtualAccountStatusFetcher(impl: DefaultVirtualAccountStatusFetcher): VirtualAccountStatusFetcher
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideVirtualAccountStatusesStore(
|
||||||
|
@NetworkMoshi moshi: Moshi,
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
scope: AppCoroutineScope,
|
||||||
|
converter: VirtualAccountStatusValueDMConverter,
|
||||||
|
): VirtualAccountStatusesStore {
|
||||||
|
return VirtualAccountStatusesStore(
|
||||||
|
runtimeStore = RuntimeSharedStore(),
|
||||||
|
persistenceDataStore = DataStoreFactory.create(
|
||||||
|
serializer = MoshiDataStoreSerializer(
|
||||||
|
moshi = moshi,
|
||||||
|
types = mapWithStringKeyTypes<VirtualAccountStatusValueDM>(),
|
||||||
|
defaultValue = emptyMap(),
|
||||||
|
),
|
||||||
|
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
|
||||||
|
produceFile = { context.dataStoreFile(fileName = "virtual_account_statuses") },
|
||||||
|
scope = scope,
|
||||||
|
),
|
||||||
|
converter = converter,
|
||||||
|
scope = scope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideVirtualAccountStatusSupplier(
|
||||||
|
factory: VirtualAccountStatusProducer.Factory,
|
||||||
|
): VirtualAccountStatusSupplier {
|
||||||
|
return object : VirtualAccountStatusSupplier(
|
||||||
|
factory = factory,
|
||||||
|
keyCreator = { "virtual_account_status_${it.userWalletId.stringValue}" },
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.tangem.data.virtualaccount.flow
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
|
||||||
|
import com.tangem.domain.core.utils.catchOn
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
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.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal class DefaultVirtualAccountStatusFetcher @Inject constructor(
|
||||||
|
private val virtualAccountStatusesStore: VirtualAccountStatusesStore,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : VirtualAccountStatusFetcher {
|
||||||
|
|
||||||
|
override suspend fun invoke(params: VirtualAccountStatusFetcher.Params) = Either.catchOn(dispatchers.default) {
|
||||||
|
val account = Account.Virtual(userWalletId = params.userWalletId)
|
||||||
|
// TODO([REDACTED_TASK_KEY]): Replace with the real VA status fetch (provisioning state, balance and banking
|
||||||
|
// details) from the backend once Virtual Account status endpoints are available. Until then the
|
||||||
|
// account is surfaced as NotCreated so the entity flows through the app end-to-end.
|
||||||
|
virtualAccountStatusesStore.store(
|
||||||
|
userWalletId = params.userWalletId,
|
||||||
|
status = AccountStatus.Virtual(account = account, value = VirtualAccountStatusValue.NotCreated),
|
||||||
|
)
|
||||||
|
}.onLeft {
|
||||||
|
virtualAccountStatusesStore.updateStatusSource(
|
||||||
|
userWalletId = params.userWalletId,
|
||||||
|
source = StatusSource.ONLY_CACHE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.tangem.data.virtualaccount.flow
|
||||||
|
|
||||||
|
import arrow.core.Option
|
||||||
|
import arrow.core.some
|
||||||
|
import com.tangem.data.virtualaccount.store.VirtualAccountStatusesStore
|
||||||
|
import com.tangem.domain.core.flow.FlowProducerTools
|
||||||
|
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.virtualaccount.flow.VirtualAccountStatusProducer
|
||||||
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
internal class DefaultVirtualAccountStatusProducer @AssistedInject constructor(
|
||||||
|
@Assisted private val params: VirtualAccountStatusProducer.Params,
|
||||||
|
override val flowProducerTools: FlowProducerTools,
|
||||||
|
private val virtualAccountStatusesStore: VirtualAccountStatusesStore,
|
||||||
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
|
) : VirtualAccountStatusProducer {
|
||||||
|
|
||||||
|
private val logger = TangemLogger.withTag(TAG)
|
||||||
|
private val account = Account.Virtual(userWalletId = params.userWalletId)
|
||||||
|
|
||||||
|
override val fallback: Option<AccountStatus.Virtual>
|
||||||
|
get() = AccountStatus.Virtual(account = account, value = VirtualAccountStatusValue.Error.Unavailable).some()
|
||||||
|
|
||||||
|
override fun produce(): Flow<AccountStatus.Virtual> {
|
||||||
|
return virtualAccountStatusesStore.get(userWalletId = params.userWalletId)
|
||||||
|
.map { status ->
|
||||||
|
if (status != null) {
|
||||||
|
logger.i("[${params.userWalletId}] flow emits statusType=${status.value::class.simpleName}")
|
||||||
|
AccountStatus.Virtual(
|
||||||
|
account = account,
|
||||||
|
value = status.value,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.i("[${params.userWalletId}] status is null: emitting Empty fallback")
|
||||||
|
AccountStatus.Virtual(
|
||||||
|
account = account,
|
||||||
|
value = VirtualAccountStatusValue.Empty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flowOn(dispatchers.default)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory : VirtualAccountStatusProducer.Factory {
|
||||||
|
override fun create(params: VirtualAccountStatusProducer.Params): DefaultVirtualAccountStatusProducer
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val TAG = "VirtualAccountStatusProducer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package com.tangem.data.virtualaccount.store
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import com.tangem.data.virtualaccount.converter.VirtualAccountStatusValueDMConverter
|
||||||
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
|
import com.tangem.datasource.local.visa.entity.VirtualAccountStatusValueDM
|
||||||
|
import com.tangem.domain.models.StatusSource
|
||||||
|
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.wallet.UserWalletId
|
||||||
|
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||||
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
|
import com.tangem.utils.logging.TangemLogger
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
internal typealias WalletIdWithVirtualStatus = Map<String, AccountStatus.Virtual>
|
||||||
|
internal typealias WalletIdWithVirtualStatusDM = Map<String, VirtualAccountStatusValueDM>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store for virtual account statuses with dual storage (runtime + persistence).
|
||||||
|
*
|
||||||
|
* @property runtimeStore runtime store for fast in-memory access
|
||||||
|
* @property persistenceDataStore persistence store for caching across app restarts
|
||||||
|
*/
|
||||||
|
internal class VirtualAccountStatusesStore(
|
||||||
|
private val runtimeStore: RuntimeSharedStore<WalletIdWithVirtualStatus>,
|
||||||
|
private val persistenceDataStore: DataStore<WalletIdWithVirtualStatusDM>,
|
||||||
|
private val converter: VirtualAccountStatusValueDMConverter,
|
||||||
|
scope: AppCoroutineScope,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val logger = TangemLogger.withTag(TAG)
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch
|
||||||
|
runtimeStore.store(
|
||||||
|
value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) ->
|
||||||
|
val account = Account.Virtual(userWalletId = UserWalletId(rawUserWalletId))
|
||||||
|
val statusValue = converter.convertBack(userWalletId = account.userWalletId, value = statusDM)
|
||||||
|
AccountStatus.Virtual(account = account, value = statusValue)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
runSuspendCatching { persistenceDataStore.updateData { emptyMap() } }
|
||||||
|
logger.e("Error while loading cached virtual account statuses", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(userWalletId: UserWalletId): Flow<AccountStatus.Virtual?> {
|
||||||
|
return runtimeStore.get()
|
||||||
|
.onStart { logger.i("get($userWalletId): subscribed to runtimeStore") }
|
||||||
|
.onEach { map ->
|
||||||
|
logger.i(
|
||||||
|
"get($userWalletId): runtimeStore emitted map size=${map.size}, " +
|
||||||
|
"hasEntry=${map.containsKey(userWalletId.stringValue)}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map { it[userWalletId.stringValue] }
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Virtual? {
|
||||||
|
return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) {
|
||||||
|
runtimeStore.update(emptyMap()) { stored ->
|
||||||
|
stored.toMutableMap().apply {
|
||||||
|
val status = this[userWalletId.stringValue] ?: return@update stored
|
||||||
|
val newValue = status.copy(value = status.value.copySealed(source = source))
|
||||||
|
put(key = userWalletId.stringValue, value = newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Virtual) {
|
||||||
|
coroutineScope {
|
||||||
|
launch { storeInRuntime(userWalletId = userWalletId, status = status) }
|
||||||
|
launch { storeInPersistence(userWalletId = userWalletId, status = status.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun contains(userWalletId: UserWalletId): Boolean {
|
||||||
|
return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Virtual) {
|
||||||
|
runtimeStore.update(default = emptyMap()) { stored ->
|
||||||
|
stored.toMutableMap().apply {
|
||||||
|
put(key = userWalletId.stringValue, value = status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun storeInPersistence(userWalletId: UserWalletId, status: VirtualAccountStatusValue) {
|
||||||
|
val statusDM = converter.convert(value = status) ?: return
|
||||||
|
persistenceDataStore.updateData { storedStatuses ->
|
||||||
|
storedStatuses.toMutableMap().apply {
|
||||||
|
put(key = userWalletId.stringValue, value = statusDM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val TAG = "VirtualAccountStatusesStore"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package com.tangem.domain.account.status.producer
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
import arrow.core.none
|
import arrow.core.none
|
||||||
import arrow.core.toOption
|
import arrow.core.toOption
|
||||||
import com.tangem.common.card.FirmwareVersion
|
|
||||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||||
import com.tangem.domain.account.models.AccountCurrencyId
|
import com.tangem.domain.account.models.AccountCurrencyId
|
||||||
import com.tangem.domain.account.models.AccountList
|
import com.tangem.domain.account.models.AccountList
|
||||||
|
|
@ -16,9 +15,7 @@ import com.tangem.domain.core.utils.lceContent
|
||||||
import com.tangem.domain.core.utils.lceLoading
|
import com.tangem.domain.core.utils.lceLoading
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.TotalFiatBalance
|
import com.tangem.domain.models.TotalFiatBalance
|
||||||
import com.tangem.domain.models.account.Account
|
import com.tangem.domain.models.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.CryptoCurrency
|
||||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
import com.tangem.domain.models.network.Network
|
import com.tangem.domain.models.network.Network
|
||||||
|
|
@ -45,7 +42,7 @@ import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory
|
||||||
import com.tangem.domain.tokens.operations.PriceChangeCalculator
|
import com.tangem.domain.tokens.operations.PriceChangeCalculator
|
||||||
import com.tangem.domain.tokens.operations.TokenListFactory
|
import com.tangem.domain.tokens.operations.TokenListFactory
|
||||||
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
|
import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator
|
||||||
import com.tangem.hot.sdk.model.HotWalletId
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusSupplier
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
|
|
@ -83,6 +80,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
||||||
private val userWalletsListRepository: UserWalletsListRepository,
|
private val userWalletsListRepository: UserWalletsListRepository,
|
||||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||||
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier,
|
||||||
|
private val virtualAccountStatusSupplier: VirtualAccountStatusSupplier,
|
||||||
private val networksRepository: NetworksRepository,
|
private val networksRepository: NetworksRepository,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val networkStatusSupplier: MultiNetworkStatusSupplier,
|
private val networkStatusSupplier: MultiNetworkStatusSupplier,
|
||||||
|
|
@ -93,8 +91,8 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
||||||
) : SingleAccountStatusListProducer {
|
) : SingleAccountStatusListProducer {
|
||||||
|
|
||||||
private val logger = TangemLogger.withTag(TAG)
|
private val logger = TangemLogger.withTag(TAG)
|
||||||
|
private val Account.Payment.errorPaymentAccountStatus: AccountStatus.Payment
|
||||||
// VirtualAccount status pipeline lands in a follow-up PR; until then surface an unavailable status.
|
get() = AccountStatus.Payment(this, PaymentAccountStatusValue.Error.Unavailable)
|
||||||
private val Account.Virtual.errorVirtualAccountStatus: AccountStatus.Virtual
|
private val Account.Virtual.errorVirtualAccountStatus: AccountStatus.Virtual
|
||||||
get() = AccountStatus.Virtual(this, VirtualAccountStatusValue.Error.Unavailable)
|
get() = AccountStatus.Virtual(this, VirtualAccountStatusValue.Error.Unavailable)
|
||||||
|
|
||||||
|
|
@ -148,141 +146,100 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo
|
||||||
flattenCurrency = flattenCurrency,
|
flattenCurrency = flattenCurrency,
|
||||||
)
|
)
|
||||||
|
|
||||||
val isPaymentSupported = userWallet.isPaymentAccountSupported()
|
val accounts = accountListFlow.value.accounts
|
||||||
logger.i("flattenFlow[$walletId]: isPaymentAccountSupported=$isPaymentSupported")
|
val hasPaymentAccount = accounts.any { it is Account.Payment }
|
||||||
if (isPaymentSupported) {
|
val hasVirtualAccount = accounts.any { it is Account.Virtual }
|
||||||
combineWithPaymentAccount(
|
logger.i("flattenFlow[$walletId]: payment=$hasPaymentAccount, virtual=$hasVirtualAccount")
|
||||||
accountListFlow = accountListFlow,
|
val specialStatusFlows = buildList<Flow<AccountStatus>> {
|
||||||
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
|
if (hasPaymentAccount) {
|
||||||
paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId)
|
add(
|
||||||
.onEach { paymentStatus ->
|
paymentAccountStatusSupplier.invoke(userWalletId = walletId)
|
||||||
logger.i(
|
.onEach { status ->
|
||||||
"flattenFlow[$walletId]: paymentAccountStatus emitted " +
|
logger.i(
|
||||||
"valueType=${paymentStatus.value::class.simpleName}",
|
"flattenFlow[$walletId]: paymentAccountStatus emitted " +
|
||||||
)
|
"valueType=${status.value::class.simpleName}",
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
combineWithoutPaymentAccount(
|
|
||||||
accountListFlow = accountListFlow,
|
|
||||||
cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.collect { accountStatusList -> channel.send(accountStatusList) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun combineWithPaymentAccount(
|
|
||||||
accountListFlow: StateFlow<AccountList>,
|
|
||||||
cryptoCurrencyStatusFlow: Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>>,
|
|
||||||
paymentAccountStatusFlow: Flow<AccountStatus.Payment>,
|
|
||||||
): Flow<AccountStatusList> {
|
|
||||||
return combine(
|
|
||||||
flow = accountListFlow,
|
|
||||||
flow2 = cryptoCurrencyStatusFlow,
|
|
||||||
flow3 = paymentAccountStatusFlow,
|
|
||||||
transform = { accountList, currencyStatusMap, paymentAccountStatus ->
|
|
||||||
logger.i(
|
|
||||||
"combineWithPayment[${params.userWalletId}] transform: " +
|
|
||||||
"accounts=${accountList.accounts.size}, " +
|
|
||||||
"currencyStatusMap=${currencyStatusMap.size}, " +
|
|
||||||
"paymentType=${paymentAccountStatus.value::class.simpleName}",
|
|
||||||
)
|
|
||||||
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 {
|
|
||||||
val statuses: List<CryptoCurrencyStatus> =
|
|
||||||
account.cryptoCurrencies.map { currency ->
|
|
||||||
val acId = account.accountId to currency.id
|
|
||||||
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
|
|
||||||
}
|
|
||||||
AccountStatus.CryptoPortfolio(
|
|
||||||
account = account,
|
|
||||||
tokenList = TokenListFactory.create(
|
|
||||||
statuses = statuses,
|
|
||||||
groupType = accountList.groupType,
|
|
||||||
sortType = accountList.sortType,
|
|
||||||
),
|
|
||||||
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
if (hasVirtualAccount) {
|
||||||
|
add(
|
||||||
|
virtualAccountStatusSupplier.invoke(userWalletId = walletId)
|
||||||
|
.onEach { status ->
|
||||||
|
logger.i(
|
||||||
|
"flattenFlow[$walletId]: virtualAccountStatus emitted " +
|
||||||
|
"valueType=${status.value::class.simpleName}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val specialStatusesFlow: Flow<Map<AccountId, AccountStatus>> = if (specialStatusFlows.isEmpty()) {
|
||||||
|
flowOf(emptyMap())
|
||||||
|
} else {
|
||||||
|
combine(specialStatusFlows) { statuses -> statuses.associateBy(AccountStatus::accountId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
combine(
|
||||||
|
accountListFlow,
|
||||||
|
cryptoCurrencyStatusFlow,
|
||||||
|
specialStatusesFlow,
|
||||||
|
) { accountList, currencyStatusMap, specialStatuses ->
|
||||||
|
logger.i(
|
||||||
|
"combine[$walletId] transform:" +
|
||||||
|
"accounts=${accountList.accounts.size}, " +
|
||||||
|
"currencyStatusMap=${currencyStatusMap.size}, " +
|
||||||
|
"specialStatuses=${specialStatuses.size}",
|
||||||
|
)
|
||||||
|
val accountStatuses = accountList.accounts.map { account ->
|
||||||
|
when (account) {
|
||||||
|
is Account.CryptoPortfolio -> buildCryptoPortfolioStatus(account, currencyStatusMap, accountList)
|
||||||
|
is Account.Payment -> specialStatuses[account.accountId] ?: account.errorPaymentAccountStatus
|
||||||
|
is Account.Virtual -> specialStatuses[account.accountId] ?: account.errorVirtualAccountStatus
|
||||||
}
|
}
|
||||||
val balances = accountStatuses.flattenTotalFiatBalance()
|
}
|
||||||
|
buildAccountStatusList(accountList = accountList, accountStatuses = accountStatuses)
|
||||||
|
}.collect { accountStatusList -> channel.send(accountStatusList) }
|
||||||
|
}
|
||||||
|
|
||||||
AccountStatusList(
|
private fun buildCryptoPortfolioStatus(
|
||||||
userWalletId = accountList.userWalletId,
|
account: Account.CryptoPortfolio,
|
||||||
accountStatuses = accountStatuses,
|
currencyStatusMap: Map<AccountCurrencyId, CryptoCurrencyStatus>,
|
||||||
totalAccounts = accountList.totalAccounts,
|
accountList: AccountList,
|
||||||
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
|
): AccountStatus.CryptoPortfolio {
|
||||||
totalArchivedAccounts = accountList.totalArchivedAccounts,
|
if (account.cryptoCurrencies.isEmpty()) return account.toEmptyAccountStatus()
|
||||||
sortType = accountList.sortType,
|
|
||||||
groupType = accountList.groupType,
|
val statuses: List<CryptoCurrencyStatus> = account.cryptoCurrencies.map { currency ->
|
||||||
)
|
val acId = account.accountId to currency.id
|
||||||
},
|
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
|
||||||
|
}
|
||||||
|
return AccountStatus.CryptoPortfolio(
|
||||||
|
account = account,
|
||||||
|
tokenList = TokenListFactory.create(
|
||||||
|
statuses = statuses,
|
||||||
|
groupType = accountList.groupType,
|
||||||
|
sortType = accountList.sortType,
|
||||||
|
),
|
||||||
|
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun combineWithoutPaymentAccount(
|
private fun buildAccountStatusList(
|
||||||
accountListFlow: StateFlow<AccountList>,
|
accountList: AccountList,
|
||||||
cryptoCurrencyStatusFlow: Flow<Map<AccountCurrencyId, CryptoCurrencyStatus>>,
|
accountStatuses: List<AccountStatus>,
|
||||||
): Flow<AccountStatusList> {
|
): AccountStatusList {
|
||||||
return combine(
|
val balances = accountStatuses.flattenTotalFiatBalance()
|
||||||
flow = accountListFlow,
|
return AccountStatusList(
|
||||||
flow2 = cryptoCurrencyStatusFlow,
|
userWalletId = accountList.userWalletId,
|
||||||
transform = { accountList, currencyStatusMap ->
|
accountStatuses = accountStatuses,
|
||||||
logger.i(
|
totalAccounts = accountList.totalAccounts,
|
||||||
"combineWithoutPayment[${params.userWalletId}] transform: " +
|
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
|
||||||
"accounts=${accountList.accounts.size}, " +
|
totalArchivedAccounts = accountList.totalArchivedAccounts,
|
||||||
"currencyStatusMap=${currencyStatusMap.size}",
|
sortType = accountList.sortType,
|
||||||
)
|
groupType = accountList.groupType,
|
||||||
val accountStatuses = accountList.accounts
|
|
||||||
.filterIsInstance<Account.CryptoPortfolio>()
|
|
||||||
.map { account ->
|
|
||||||
when (account) {
|
|
||||||
is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) {
|
|
||||||
account.toEmptyAccountStatus()
|
|
||||||
} else {
|
|
||||||
val statuses: List<CryptoCurrencyStatus> =
|
|
||||||
account.cryptoCurrencies.map { currency ->
|
|
||||||
val acId = account.accountId to currency.id
|
|
||||||
currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus()
|
|
||||||
}
|
|
||||||
AccountStatus.CryptoPortfolio(
|
|
||||||
account = account,
|
|
||||||
tokenList = TokenListFactory.create(
|
|
||||||
statuses = statuses,
|
|
||||||
groupType = accountList.groupType,
|
|
||||||
sortType = accountList.sortType,
|
|
||||||
),
|
|
||||||
priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val balances = accountStatuses.flattenTotalFiatBalance()
|
|
||||||
|
|
||||||
AccountStatusList(
|
|
||||||
userWalletId = accountList.userWalletId,
|
|
||||||
accountStatuses = accountStatuses,
|
|
||||||
totalAccounts = accountList.totalAccounts,
|
|
||||||
totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances),
|
|
||||||
totalArchivedAccounts = accountList.totalArchivedAccounts,
|
|
||||||
sortType = accountList.sortType,
|
|
||||||
groupType = accountList.groupType,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) {
|
|
||||||
is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable
|
|
||||||
is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ProducerScope<AccountStatusList>.flattenCurrencyStatusFlow(
|
private fun ProducerScope<AccountStatusList>.flattenCurrencyStatusFlow(
|
||||||
userWallet: UserWallet,
|
userWallet: UserWallet,
|
||||||
flattenCurrency: MutableSharedFlow<Map<AccountCurrencyId, CryptoCurrency>>,
|
flattenCurrency: MutableSharedFlow<Map<AccountCurrencyId, CryptoCurrency>>,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
implementation(projects.features.staking.api)
|
implementation(projects.features.staking.api)
|
||||||
implementation(projects.features.markets.api)
|
implementation(projects.features.markets.api)
|
||||||
implementation(projects.features.swap.api)
|
implementation(projects.features.swap.api)
|
||||||
|
implementation(projects.features.virtualAccounts.details.api) //VIRTUAL_ACCOUNTS_ENABLED
|
||||||
|
|
||||||
/** Project - Other */
|
/** Project - Other */
|
||||||
implementation(projects.core.configToggles)
|
implementation(projects.core.configToggles)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||||
import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher
|
import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher
|
||||||
import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher
|
import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher
|
||||||
import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher
|
import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.logging.TangemLogger
|
import com.tangem.utils.logging.TangemLogger
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
|
@ -56,6 +58,8 @@ class WalletBalanceFetcher internal constructor(
|
||||||
private val singleWalletBalanceFetcher: BaseWalletBalanceFetcher,
|
private val singleWalletBalanceFetcher: BaseWalletBalanceFetcher,
|
||||||
private val balanceFetchingOperations: BalanceFetchingOperations,
|
private val balanceFetchingOperations: BalanceFetchingOperations,
|
||||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
|
private val virtualAccountStatusFetcher: VirtualAccountStatusFetcher,
|
||||||
|
private val virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : FlowFetcher<WalletBalanceFetcher.Params> {
|
) : FlowFetcher<WalletBalanceFetcher.Params> {
|
||||||
|
|
||||||
|
|
@ -70,7 +74,9 @@ class WalletBalanceFetcher internal constructor(
|
||||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher: VirtualAccountStatusFetcher,
|
||||||
stakingIdFactory: StakingIdFactory,
|
stakingIdFactory: StakingIdFactory,
|
||||||
|
virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
) : this(
|
) : this(
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
|
@ -85,6 +91,8 @@ class WalletBalanceFetcher internal constructor(
|
||||||
stakingIdFactory = stakingIdFactory,
|
stakingIdFactory = stakingIdFactory,
|
||||||
),
|
),
|
||||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher = virtualAccountStatusFetcher,
|
||||||
|
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -99,6 +107,8 @@ class WalletBalanceFetcher internal constructor(
|
||||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
multiStakingBalanceFetcher: MultiStakingBalanceFetcher,
|
||||||
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher: VirtualAccountStatusFetcher,
|
||||||
|
virtualAccountsFeatureToggles: VirtualAccountFeatureToggles,
|
||||||
stakingIdFactory: StakingIdFactory,
|
stakingIdFactory: StakingIdFactory,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
) : this(
|
) : this(
|
||||||
|
|
@ -121,6 +131,8 @@ class WalletBalanceFetcher internal constructor(
|
||||||
stakingIdFactory = stakingIdFactory,
|
stakingIdFactory = stakingIdFactory,
|
||||||
),
|
),
|
||||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher = virtualAccountStatusFetcher,
|
||||||
|
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -177,6 +189,14 @@ class WalletBalanceFetcher internal constructor(
|
||||||
balanceFetchingOperations.fetchQuotes(rawCurrencyIds = setOf(TangemPayCurrencyFactory.TOKEN_ID))
|
balanceFetchingOperations.fetchQuotes(rawCurrencyIds = setOf(TangemPayCurrencyFactory.TOKEN_ID))
|
||||||
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
|
paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch Virtual account separately for the same reason as TangemPay
|
||||||
|
if (
|
||||||
|
fetchingSources.any { it is WalletFetchingSource.VirtualAccount } &&
|
||||||
|
virtualAccountsFeatureToggles.isVirtualAccountsEnabled
|
||||||
|
) {
|
||||||
|
virtualAccountStatusFetcher.invoke(VirtualAccountStatusFetcher.Params(userWalletId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ sealed class WalletFetchingSource {
|
||||||
*/
|
*/
|
||||||
data object TangemPay : WalletFetchingSource()
|
data object TangemPay : WalletFetchingSource()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual account fetching source.
|
||||||
|
* Handled separately from standard balance sources via
|
||||||
|
* [com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher].
|
||||||
|
*/
|
||||||
|
data object VirtualAccount : WalletFetchingSource()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard balance fetching sources (NETWORK, QUOTE, STAKING).
|
* Standard balance fetching sources (NETWORK, QUOTE, STAKING).
|
||||||
* Processed via [BalanceFetchingOperations.fetchAll].
|
* Processed via [BalanceFetchingOperations.fetchAll].
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ internal class MultiWalletBalanceFetcher(
|
||||||
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING),
|
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING),
|
||||||
),
|
),
|
||||||
WalletFetchingSource.TangemPay,
|
WalletFetchingSource.TangemPay,
|
||||||
|
WalletFetchingSource.VirtualAccount,
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set<CryptoCurrency> {
|
override suspend fun getCryptoCurrencies(userWallet: UserWallet): Set<CryptoCurrency> {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import com.tangem.domain.tokens.FetchingSource
|
||||||
import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher
|
import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher
|
||||||
import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher
|
import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher
|
||||||
import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher
|
import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher
|
||||||
|
import com.tangem.domain.virtualaccount.flow.VirtualAccountStatusFetcher
|
||||||
|
import com.tangem.features.virtualaccount.VirtualAccountFeatureToggles
|
||||||
import com.tangem.test.core.assertEither
|
import com.tangem.test.core.assertEither
|
||||||
import com.tangem.test.core.assertEitherRight
|
import com.tangem.test.core.assertEitherRight
|
||||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||||
|
|
@ -50,7 +52,11 @@ internal class WalletBalanceFetcherTest {
|
||||||
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk()
|
private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk()
|
||||||
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk()
|
private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk()
|
||||||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
|
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk()
|
||||||
|
private val virtualAccountStatusFetcher: VirtualAccountStatusFetcher = mockk()
|
||||||
private val stakingIdFactory: StakingIdFactory = mockk()
|
private val stakingIdFactory: StakingIdFactory = mockk()
|
||||||
|
private val virtualAccountsFeatureToggles: VirtualAccountFeatureToggles = mockk {
|
||||||
|
every { isVirtualAccountsEnabled } returns false
|
||||||
|
}
|
||||||
|
|
||||||
private val fetcher = WalletBalanceFetcher(
|
private val fetcher = WalletBalanceFetcher(
|
||||||
userWalletsListRepository = userWalletsListRepository,
|
userWalletsListRepository = userWalletsListRepository,
|
||||||
|
|
@ -62,7 +68,9 @@ internal class WalletBalanceFetcherTest {
|
||||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||||
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
multiStakingBalanceFetcher = multiStakingBalanceFetcher,
|
||||||
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
paymentAccountStatusFetcher = paymentAccountStatusFetcher,
|
||||||
|
virtualAccountStatusFetcher = virtualAccountStatusFetcher,
|
||||||
stakingIdFactory = stakingIdFactory,
|
stakingIdFactory = stakingIdFactory,
|
||||||
|
virtualAccountsFeatureToggles = virtualAccountsFeatureToggles,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
import com.tangem.domain.models.wallet.UserWallet
|
import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.tokens.FetchingSource
|
||||||
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
|
import com.tangem.domain.tokens.MultiWalletAccountListFetcher
|
||||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer
|
||||||
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
|
||||||
import com.tangem.domain.tokens.FetchingSource
|
|
||||||
import com.tangem.domain.tokens.wallet.WalletFetchingSource
|
import com.tangem.domain.tokens.wallet.WalletFetchingSource
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
|
@ -54,6 +54,7 @@ class MultiWalletBalanceFetcherTest {
|
||||||
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING),
|
sources = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING),
|
||||||
),
|
),
|
||||||
WalletFetchingSource.TangemPay,
|
WalletFetchingSource.TangemPay,
|
||||||
|
WalletFetchingSource.VirtualAccount,
|
||||||
)
|
)
|
||||||
Truth.assertThat(actual).isEqualTo(expected)
|
Truth.assertThat(actual).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.tangem.domain.virtualaccount.flow
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import com.tangem.domain.core.flow.FlowFetcher
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
interface VirtualAccountStatusFetcher : FlowFetcher<VirtualAccountStatusFetcher.Params> {
|
||||||
|
|
||||||
|
suspend operator fun invoke(userWalletId: UserWalletId): Either<Throwable, Unit> {
|
||||||
|
return invoke(Params(userWalletId))
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Params(val userWalletId: UserWalletId)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.tangem.domain.virtualaccount.flow
|
||||||
|
|
||||||
|
import com.tangem.domain.core.flow.FlowProducer
|
||||||
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
|
||||||
|
interface VirtualAccountStatusProducer : FlowProducer<AccountStatus.Virtual> {
|
||||||
|
data class Params(val userWalletId: UserWalletId)
|
||||||
|
|
||||||
|
interface Factory : FlowProducer.Factory<Params, VirtualAccountStatusProducer>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.domain.virtualaccount.flow
|
||||||
|
|
||||||
|
import com.tangem.domain.core.flow.FlowCachingSupplier
|
||||||
|
import com.tangem.domain.models.account.AccountStatus
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
open class VirtualAccountStatusSupplier(
|
||||||
|
override val factory: VirtualAccountStatusProducer.Factory,
|
||||||
|
override val keyCreator: (VirtualAccountStatusProducer.Params) -> String,
|
||||||
|
) : FlowCachingSupplier<VirtualAccountStatusProducer, VirtualAccountStatusProducer.Params, AccountStatus.Virtual>() {
|
||||||
|
|
||||||
|
operator fun invoke(userWalletId: UserWalletId): Flow<AccountStatus.Virtual> {
|
||||||
|
val params = VirtualAccountStatusProducer.Params(userWalletId)
|
||||||
|
return this.invoke(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue