Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-10 00:30:26 -07:00
parent 819fecffb6
commit b3640235ef
15 changed files with 175 additions and 147 deletions

View file

@ -40,6 +40,7 @@ sealed interface PaymentAccountStatusValueDM {
@NameLabel("active_account") @NameLabel("active_account")
data class ActiveAccount( data class ActiveAccount(
@Json(name = "active_account") val marker: Boolean = true,
@Json(name = "customer_id") val customerId: String, @Json(name = "customer_id") val customerId: String,
@Json(name = "currency_code") val currencyCode: String, @Json(name = "currency_code") val currencyCode: String,
@Json(name = "deposit_address") val depositAddress: String?, @Json(name = "deposit_address") val depositAddress: String?,
@ -59,9 +60,11 @@ sealed interface PaymentAccountStatusValueDM {
@NameLabel("deactivated_account") @NameLabel("deactivated_account")
data class DeactivatedAccount( data class DeactivatedAccount(
@Json(name = "deactivated_account") val marker: Boolean = true, @Json(name = "deactivated_account") val marker: Boolean = true,
@Json(name = "customer_id") val customerId: String,
@Json(name = "fiat_rate") val fiatRate: BigDecimal?, @Json(name = "fiat_rate") val fiatRate: BigDecimal?,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
@Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal,
) : PaymentAccountStatusValueDM ) : PaymentAccountStatusValueDM
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View file

@ -39,11 +39,11 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard() is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount( is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount(
customerId = value.customerId, customerId = value.customerId,
currencyCode = value.currencyCode, currencyCode = value.balance.fiatBalance.currency,
depositAddress = value.depositAddress, depositAddress = value.depositAddress,
fiatBalance = value.fiatBalance.toDM(), fiatBalance = value.balance.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(), cryptoBalance = value.balance.cryptoBalance.toDM(),
availableForWithdrawal = value.availableForWithdrawal, availableForWithdrawal = value.balance.availableForWithdrawal,
fiatRate = value.fiatRate, fiatRate = value.fiatRate,
cards = value.cards.map { card -> cards = value.cards.map { card ->
PaymentAccountStatusValueDM.TangemPayCard( PaymentAccountStatusValueDM.TangemPayCard(
@ -63,9 +63,11 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
) )
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty() is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount( is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
customerId = value.customerId,
fiatRate = value.fiatRate, fiatRate = value.fiatRate,
fiatBalance = value.fiatBalance.toDM(), fiatBalance = value.balance.fiatBalance.toDM(),
cryptoBalance = value.cryptoBalance.toDM(), cryptoBalance = value.balance.cryptoBalance.toDM(),
availableForWithdrawal = value.balance.availableForWithdrawal,
) )
// Transient statuses are not persisted // Transient statuses are not persisted
is PaymentAccountStatusValue.Loading, is PaymentAccountStatusValue.Loading,
@ -90,11 +92,12 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded( is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded(
source = StatusSource.CACHE, source = StatusSource.CACHE,
customerId = value.customerId, customerId = value.customerId,
currencyCode = value.currencyCode,
depositAddress = value.depositAddress, depositAddress = value.depositAddress,
fiatBalance = value.fiatBalance.toDomain(), balance = PaymentAccountStatusValue.Balance(
cryptoBalance = value.cryptoBalance.toDomain(), fiatBalance = value.fiatBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal, cryptoBalance = value.cryptoBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal,
),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate, fiatRate = value.fiatRate,
cards = value.cards.map { card -> cards = value.cards.map { card ->
@ -123,8 +126,12 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
) )
is PaymentAccountStatusValueDM.DeactivatedAccount -> PaymentAccountStatusValue.Deactivated( is PaymentAccountStatusValueDM.DeactivatedAccount -> PaymentAccountStatusValue.Deactivated(
source = StatusSource.CACHE, source = StatusSource.CACHE,
fiatBalance = value.fiatBalance.toDomain(), customerId = value.customerId,
cryptoBalance = value.cryptoBalance.toDomain(), balance = PaymentAccountStatusValue.Balance(
fiatBalance = value.fiatBalance.toDomain(),
cryptoBalance = value.cryptoBalance.toDomain(),
availableForWithdrawal = value.availableForWithdrawal,
),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate, fiatRate = value.fiatRate,
) )

View file

@ -33,6 +33,7 @@ import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -287,11 +288,16 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
customerId = requireNotNull(customerId) { "CustomerId must not be null" }, customerId = requireNotNull(customerId) { "CustomerId must not be null" },
) )
} }
fiatBalance != null && cryptoBalance != null && (isDeactivated || isFormer) -> { fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() &&
(isDeactivated || isFormer) -> {
PaymentAccountStatusValue.Deactivated( PaymentAccountStatusValue.Deactivated(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
fiatBalance = fiatBalance, customerId = requireNotNull(customerId) { "CustomerId must not be null" },
cryptoBalance = cryptoBalance, balance = PaymentAccountStatusValue.Balance(
fiatBalance = fiatBalance,
cryptoBalance = cryptoBalance,
availableForWithdrawal = availableForWithdrawal.orZero(),
),
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
fiatRate = quotesData?.fiatRate, fiatRate = quotesData?.fiatRate,
) )
@ -321,11 +327,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return PaymentAccountStatusValue.Loaded( return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
customerId = customerId, customerId = customerId,
currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress, depositAddress = cardInfo.depositAddress,
fiatBalance = cardInfo.fiatBalance, balance = PaymentAccountStatusValue.Balance(
cryptoBalance = cardInfo.cryptoBalance, fiatBalance = cardInfo.fiatBalance,
availableForWithdrawal = cardInfo.availableForWithdrawal, cryptoBalance = cardInfo.cryptoBalance,
availableForWithdrawal = cardInfo.availableForWithdrawal,
),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = fiatRate, fiatRate = fiatRate,
cards = listOf( cards = listOf(

View file

@ -65,6 +65,7 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
state = CustomerInfo.State.fromString(value.state), state = CustomerInfo.State.fromString(value.state),
fiatBalance = fiatBalance?.toDomain(), fiatBalance = fiatBalance?.toDomain(),
cryptoBalance = cryptoBalance?.toDomain(), cryptoBalance = cryptoBalance?.toDomain(),
availableForWithdrawal = value.balance?.availableForWithdrawal?.amount,
) )
} }

View file

@ -64,11 +64,15 @@ internal class PaymentAccountStatusValueDMConverterTest {
// GIVEN // GIVEN
val domain = PaymentAccountStatusValue.Deactivated( val domain = PaymentAccountStatusValue.Deactivated(
source = StatusSource.ACTUAL, source = StatusSource.ACTUAL,
fiatBalance = PaymentAccountStatusValue.FiatBalance( customerId = "customer-1",
availableBalance = BigDecimal("100"), balance = PaymentAccountStatusValue.Balance(
currency = "USD", fiatBalance = PaymentAccountStatusValue.FiatBalance(
availableBalance = BigDecimal("100"),
currency = "USD",
),
cryptoBalance = cryptoBalance(),
availableForWithdrawal = BigDecimal("7"),
), ),
cryptoBalance = cryptoBalance(),
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
fiatRate = BigDecimal("1.05"), fiatRate = BigDecimal("1.05"),
) )
@ -79,8 +83,10 @@ internal class PaymentAccountStatusValueDMConverterTest {
// THEN // THEN
assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.DeactivatedAccount::class.java) assertThat(result).isInstanceOf(PaymentAccountStatusValueDM.DeactivatedAccount::class.java)
val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount val dm = result as PaymentAccountStatusValueDM.DeactivatedAccount
assertThat(dm.customerId).isEqualTo("customer-1")
assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100")) assertThat(dm.fiatBalance.availableBalance).isEqualTo(BigDecimal("100"))
assertThat(dm.fiatBalance.currency).isEqualTo("USD") assertThat(dm.fiatBalance.currency).isEqualTo("USD")
assertThat(dm.availableForWithdrawal).isEqualTo(BigDecimal("7"))
assertThat(dm.fiatRate).isEqualTo(BigDecimal("1.05")) assertThat(dm.fiatRate).isEqualTo(BigDecimal("1.05"))
} }
@ -141,12 +147,14 @@ internal class PaymentAccountStatusValueDMConverterTest {
fun `GIVEN DM DeactivatedAccount WHEN convertBack THEN returns domain Deactivated with CACHE source`() { fun `GIVEN DM DeactivatedAccount WHEN convertBack THEN returns domain Deactivated with CACHE source`() {
// GIVEN // GIVEN
val dm = PaymentAccountStatusValueDM.DeactivatedAccount( val dm = PaymentAccountStatusValueDM.DeactivatedAccount(
customerId = "customer-2",
fiatBalance = PaymentAccountStatusValueDM.FiatBalanceDM( fiatBalance = PaymentAccountStatusValueDM.FiatBalanceDM(
availableBalance = BigDecimal("200"), availableBalance = BigDecimal("200"),
currency = "EUR", currency = "EUR",
), ),
cryptoBalance = cryptoBalanceDM(), cryptoBalance = cryptoBalanceDM(),
fiatRate = BigDecimal("0.92"), fiatRate = BigDecimal("0.92"),
availableForWithdrawal = BigDecimal("5"),
) )
// WHEN // WHEN
@ -156,8 +164,10 @@ internal class PaymentAccountStatusValueDMConverterTest {
assertThat(result).isInstanceOf(PaymentAccountStatusValue.Deactivated::class.java) assertThat(result).isInstanceOf(PaymentAccountStatusValue.Deactivated::class.java)
val deactivated = result as PaymentAccountStatusValue.Deactivated val deactivated = result as PaymentAccountStatusValue.Deactivated
assertThat(deactivated.source).isEqualTo(StatusSource.CACHE) assertThat(deactivated.source).isEqualTo(StatusSource.CACHE)
assertThat(deactivated.fiatBalance.availableBalance).isEqualTo(BigDecimal("200")) assertThat(deactivated.customerId).isEqualTo("customer-2")
assertThat(deactivated.fiatBalance.currency).isEqualTo("EUR") assertThat(deactivated.balance.fiatBalance.availableBalance).isEqualTo(BigDecimal("200"))
assertThat(deactivated.balance.fiatBalance.currency).isEqualTo("EUR")
assertThat(deactivated.balance.availableForWithdrawal).isEqualTo(BigDecimal("5"))
assertThat(deactivated.fiatRate).isEqualTo(BigDecimal("0.92")) assertThat(deactivated.fiatRate).isEqualTo(BigDecimal("0.92"))
} }

View file

@ -33,11 +33,11 @@ sealed class PaymentAccountStatusValue {
is Loading -> TotalFiatBalance.Loading is Loading -> TotalFiatBalance.Loading
is Loaded -> { is Loaded -> {
val rate = this.fiatRate ?: return TotalFiatBalance.Failed val rate = this.fiatRate ?: return TotalFiatBalance.Failed
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source) TotalFiatBalance.Loaded(amount = balance.fiatBalance.availableBalance.multiply(rate), source = source)
} }
is Deactivated -> { is Deactivated -> {
val rate = this.fiatRate ?: return TotalFiatBalance.Failed val rate = this.fiatRate ?: return TotalFiatBalance.Failed
TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source) TotalFiatBalance.Loaded(amount = balance.fiatBalance.availableBalance.multiply(rate), source = source)
} }
} }
@ -104,8 +104,8 @@ sealed class PaymentAccountStatusValue {
* Represents a state where the account is deactivated. * Represents a state where the account is deactivated.
* *
* @property source The source of the status information. * @property source The source of the status information.
* @property fiatBalance The fiat balance details. * @property customerId The unique identifier of the customer.
* @property cryptoBalance The crypto balance details. * @property balance The balance details (fiat, crypto and amount available for withdrawal).
* @property cryptoCurrency The crypto currency held by the deactivated account. * @property cryptoCurrency The crypto currency held by the deactivated account.
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
* or `null` if the quote is not yet available. When `null`, * or `null` if the quote is not yet available. When `null`,
@ -114,18 +114,18 @@ sealed class PaymentAccountStatusValue {
@Serializable @Serializable
data class Deactivated( data class Deactivated(
override val source: StatusSource, override val source: StatusSource,
val fiatBalance: FiatBalance, val customerId: String,
val cryptoBalance: CryptoBalance, val balance: Balance,
val cryptoCurrency: CryptoCurrency.Token, val cryptoCurrency: CryptoCurrency.Token,
val fiatRate: SerializedBigDecimal?, val fiatRate: SerializedBigDecimal?,
) : PaymentAccountStatusValue() { ) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,
value = buildCryptoCurrencyStatusValue( value = buildCryptoCurrencyStatusValue(
amount = cryptoBalance.balance, amount = balance.cryptoBalance.balance,
fiatAmount = fiatBalance.availableBalance, fiatAmount = balance.fiatBalance.availableBalance,
fiatRate = fiatRate, fiatRate = fiatRate,
depositAddress = cryptoBalance.depositAddress, depositAddress = balance.cryptoBalance.depositAddress,
), ),
) )
} }
@ -135,11 +135,9 @@ sealed class PaymentAccountStatusValue {
* *
* @property source The source of the status information. * @property source The source of the status information.
* @property customerId The unique identifier of the customer. * @property customerId The unique identifier of the customer.
* @property currencyCode The code of the currency.
* @property depositAddress The address for deposits, if available. * @property depositAddress The address for deposits, if available.
* @property fiatBalance The fiat balance details. * @property balance The balance details (fiat, crypto and amount available for withdrawal).
* @property cryptoBalance The crypto balance details. * The fiat currency code is available via [Balance.fiatBalance].
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds).
* @property cryptoCurrency The crypto currency held by the account. * @property cryptoCurrency The crypto currency held by the account.
* @property cards The list of user's cards. * @property cards The list of user's cards.
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency, * @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
@ -150,11 +148,8 @@ sealed class PaymentAccountStatusValue {
data class Loaded( data class Loaded(
override val source: StatusSource, override val source: StatusSource,
val customerId: String, val customerId: String,
val currencyCode: String,
val depositAddress: String?, val depositAddress: String?,
val fiatBalance: FiatBalance, val balance: Balance,
val cryptoBalance: CryptoBalance,
val availableForWithdrawal: SerializedBigDecimal,
val cryptoCurrency: CryptoCurrency.Token, val cryptoCurrency: CryptoCurrency.Token,
val cards: List<TangemPayCard>, val cards: List<TangemPayCard>,
val fiatRate: SerializedBigDecimal?, val fiatRate: SerializedBigDecimal?,
@ -162,10 +157,10 @@ sealed class PaymentAccountStatusValue {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency, currency = cryptoCurrency,
value = buildCryptoCurrencyStatusValue( value = buildCryptoCurrencyStatusValue(
amount = availableForWithdrawal, amount = balance.availableForWithdrawal,
fiatAmount = fiatBalance.availableBalance, fiatAmount = balance.fiatBalance.availableBalance,
fiatRate = fiatRate, fiatRate = fiatRate,
depositAddress = cryptoBalance.depositAddress, depositAddress = balance.cryptoBalance.depositAddress,
), ),
) )
} }
@ -202,6 +197,21 @@ sealed class PaymentAccountStatusValue {
} }
} }
/**
* Aggregates all balance data of a payment account, as returned by the `customer/me` endpoint.
*
* @property fiatBalance The fiat balance details.
* @property cryptoBalance The crypto balance details.
* @property availableForWithdrawal The crypto amount currently available for withdrawal/swap
* (excludes pending/locked funds).
*/
@Serializable
data class Balance(
val fiatBalance: FiatBalance,
val cryptoBalance: CryptoBalance,
val availableForWithdrawal: SerializedBigDecimal,
)
/** /**
* Represents the fiat balance of the payment account. * Represents the fiat balance of the payment account.
* *

View file

@ -28,6 +28,7 @@ data class CustomerInfo(
val state: State, val state: State,
val fiatBalance: PaymentAccountStatusValue.FiatBalance?, val fiatBalance: PaymentAccountStatusValue.FiatBalance?,
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance?, val cryptoBalance: PaymentAccountStatusValue.CryptoBalance?,
val availableForWithdrawal: BigDecimal?,
) { ) {
enum class State { enum class State {
NEW, NEW,

View file

@ -271,7 +271,7 @@ internal class SendDestinationModel @Inject constructor(
private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? { private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null
val (paymentAccountAddress, currency) = when (val status = this.value) { val (paymentAccountAddress, currency) = when (val status = this.value) {
is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress to status.cryptoCurrency is PaymentAccountStatusValue.Loaded -> status.balance.cryptoBalance.depositAddress to status.cryptoCurrency
else -> return null else -> return null
} }

View file

@ -97,7 +97,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
?.takeIf { it.period == TangemPayCardLimitPeriod.DAY } ?.takeIf { it.period == TangemPayCardLimitPeriod.DAY }
?.amount ?.amount
val currency = getJavaCurrencyByCode(status.currencyCode) val currency = getJavaCurrencyByCode(status.balance.fiatBalance.currency)
uiState.update { state -> uiState.update { state ->
val amount = if (index == 0) { val amount = if (index == 0) {
currentLimit?.stripTrailingZeros()?.toPlainString().orEmpty() currentLimit?.stripTrailingZeros()?.toPlainString().orEmpty()

View file

@ -118,8 +118,9 @@ internal class TangemPayCardPageModel @Inject constructor(
val dailyLimitState = if (limit != null) { val dailyLimitState = if (limit != null) {
TangemPayDailyLimitBlockState.Content( TangemPayDailyLimitBlockState.Content(
limit = limit.amount.format { limit = limit.amount.format {
val symbol = getJavaCurrencyByCode(status.currencyCode).symbol val currencyCode = status.balance.fiatBalance.currency
fiat(status.currencyCode, symbol).optionalDecimals() val symbol = getJavaCurrencyByCode(currencyCode).symbol
fiat(currencyCode, symbol).optionalDecimals()
}, },
onChangeClick = ::onClickLimitChange, onChangeClick = ::onClickLimitChange,
) )

View file

@ -25,8 +25,8 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState import com.tangem.domain.models.pay.TangemPayCardState
import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
@ -40,8 +40,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.features.tangempay.model.listener.CardDetailsEvent
import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.* import com.tangem.features.tangempay.model.transformers.*
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
import com.tangem.features.tangempay.utils.* import com.tangem.features.tangempay.utils.*
@ -52,7 +50,6 @@ import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger import com.tangem.utils.logging.TangemLogger
import com.tangem.utils.transformer.update import com.tangem.utils.transformer.update
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -70,33 +67,36 @@ internal class TangemPayDetailsModel @Inject constructor(
private val cardDetailsRepository: TangemPayCardDetailsRepository, private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val uiMessageSender: UiMessageSender, private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val tangemPayWithdrawRepository: TangemPayWithdrawRepository,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val expressTransactionsEventListener: ExpressTransactionsEventListener, private val expressTransactionsEventListener: ExpressTransactionsEventListener,
private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles,
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener { ) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
private val userWalletId = params.initialStatus.userWalletId
private val isTangemPayDeactivated = params.initialStatus.isDeactivated private val isTangemPayDeactivated = params.initialStatus.isDeactivated
private val loaded: PaymentAccountStatusValue.Loaded? =
params.initialStatus.value as? PaymentAccountStatusValue.Loaded
private val firstCard = loaded?.cards?.firstOrNull()
val cryptoCurrency: CryptoCurrency = params.initialStatus.cryptoCurrency
private val initialCardFrozenState: TangemPayCardFrozenState = when { private val initialCard = params.initialStatus.ifLoadedOrNull { it.cards.firstOrNull() }
firstCard == null -> TangemPayCardFrozenState.Unfrozen
else -> firstCard.frozenState private val currentStatus = MutableStateFlow(params.initialStatus)
}
private val userWalletId
get() = currentStatus.value.userWalletId
val cryptoCurrency
get() = currentStatus.value.cryptoCurrency
private val stateFactory = TangemPayDetailsStateFactory( private val stateFactory = TangemPayDetailsStateFactory(
onBack = router::pop, onBack = router::pop,
onOpenMenu = ::onOpenMenu, onOpenMenu = ::onOpenMenu,
intents = this, intents = this,
cardFrozenState = initialCardFrozenState, cardFrozenState = when {
initialCard == null -> TangemPayCardFrozenState.Unfrozen
else -> initialCard.frozenState
},
isRedesignEnabled = isRedesignEnabled(), isRedesignEnabled = isRedesignEnabled(),
) )
@ -104,39 +104,49 @@ internal class TangemPayDetailsModel @Inject constructor(
field = MutableStateFlow( field = MutableStateFlow(
stateFactory.getInitialState( stateFactory.getInitialState(
isTangemPayDeactivated = isTangemPayDeactivated, isTangemPayDeactivated = isTangemPayDeactivated,
cardNumberEnd = firstCard?.lastDigits.orEmpty(), cardNumberEnd = initialCard?.lastDigits.orEmpty(),
isReissuing = firstCard == null || firstCard.state != TangemPayCardState.Active, isReissuing = initialCard == null || initialCard.state != TangemPayCardState.Active,
), ),
) )
private val refreshStateJobHolder = JobHolder() private val refreshStateJobHolder = JobHolder()
private val fetchBalanceJobHolder = JobHolder()
private val addToWalletBannerJobHolder = JobHolder() private val addToWalletBannerJobHolder = JobHolder()
private var balance: TangemPayCardBalance? = null
val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation() val bottomSheetNavigation: SlotNavigation<TangemPayDetailsNavigation> = SlotNavigation()
init { init {
analytics.send(TangemPayAnalyticsEvents.MainScreenOpened()) analytics.send(TangemPayAnalyticsEvents.MainScreenOpened())
handleBalanceHiding() handleBalanceHiding()
fetchBalance()
if (!isTangemPayDeactivated && firstCard != null) {
subscribeToCardFrozenState(firstCard.id)
fetchAddToWalletBanner()
paymentAccountStatusSupplier.invoke(userWalletId) val statusFlow = paymentAccountStatusSupplier.invoke(userWalletId)
.map { it.value } .onEach { status -> currentStatus.update { status } }
.map { it.value }
if (isTangemPayDeactivated) {
statusFlow
.filterIsInstance<PaymentAccountStatusValue.Deactivated>()
.onEach { state ->
uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
}
.launchIn(modelScope)
} else {
if (initialCard != null) {
subscribeToCardFrozenState(initialCard.id)
}
fetchAddToWalletBanner()
statusFlow
.filterIsInstance<PaymentAccountStatusValue.Loaded>() .filterIsInstance<PaymentAccountStatusValue.Loaded>()
.filter { it.source == StatusSource.ACTUAL } .filter { it.source == StatusSource.ACTUAL }
.onEach { state -> .onEach { state ->
val card = state.cards.firstOrNull() ?: return@onEach uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
uiState.update( state.cards.firstOrNull()?.let { card ->
TangemPayCardDataTransformer( uiState.update(
card = card, TangemPayCardDataTransformer(
onCardClick = { onCardClick() }, card = card,
), onCardClick = { onCardClick() },
) ),
)
}
} }
.launchIn(modelScope) .launchIn(modelScope)
} }
@ -165,17 +175,16 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onClickAddFunds() { override fun onClickAddFunds() {
analytics.send(TangemPayAnalyticsEvents.AddFundsClicked()) analytics.send(TangemPayAnalyticsEvents.AddFundsClicked())
val currentBalance = balance val balance = currentStatus.value.balanceOrNull()
val depositAddress = currentBalance?.depositAddress if (balance == null) {
if (currentBalance == null || depositAddress == null) {
showBottomSheetError(TangemPayDetailsErrorType.Receive) showBottomSheetError(TangemPayDetailsErrorType.Receive)
} else { } else {
bottomSheetNavigation.activate( bottomSheetNavigation.activate(
TangemPayDetailsNavigation.AddFunds( TangemPayDetailsNavigation.AddFunds(
walletId = userWalletId, walletId = userWalletId,
fiatBalance = currentBalance.availableForWithdrawal, fiatBalance = balance.availableForWithdrawal,
cryptoBalance = currentBalance.availableForWithdrawal, cryptoBalance = balance.availableForWithdrawal,
depositAddress = depositAddress, depositAddress = balance.cryptoBalance.depositAddress,
cryptoCurrency = cryptoCurrency, cryptoCurrency = cryptoCurrency,
), ),
) )
@ -184,12 +193,6 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onClickWithdraw() { override fun onClickWithdraw() {
analytics.send(TangemPayAnalyticsEvents.WithdrawClicked()) analytics.send(TangemPayAnalyticsEvents.WithdrawClicked())
val currentBalance = balance
val depositAddress = currentBalance?.depositAddress
if (currentBalance == null || depositAddress == null) {
showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
return
}
modelScope.launch { modelScope.launch {
val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWalletId) val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWalletId)
if (hasActiveWithdrawal) { if (hasActiveWithdrawal) {
@ -197,18 +200,19 @@ internal class TangemPayDetailsModel @Inject constructor(
} else { } else {
uiMessageSender.send( uiMessageSender.send(
message = TangemPayMessagesFactory.createWithdrawWarning( message = TangemPayMessagesFactory.createWithdrawWarning(
onGotItClick = { onConfirmWithdrawal(cryptoCurrency, currentBalance, depositAddress) }, onGotItClick = { onConfirmWithdrawal(cryptoCurrency) },
), ),
) )
} }
} }
} }
private fun onConfirmWithdrawal( private fun onConfirmWithdrawal(currency: CryptoCurrency) {
currency: CryptoCurrency, val balance = currentStatus.value.balanceOrNull()
currentBalance: TangemPayCardBalance, if (balance == null) {
depositAddress: String, showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
) { return
}
router.push( router.push(
AppRoute.Swap( AppRoute.Swap(
cryptoCurrency = currency, cryptoCurrency = currency,
@ -216,27 +220,15 @@ internal class TangemPayDetailsModel @Inject constructor(
screenSource = AnalyticsParam.ScreensSources.TangemPay.value, screenSource = AnalyticsParam.ScreensSources.TangemPay.value,
currencyPosition = AppRoute.Swap.CurrencyPosition.FROM, currencyPosition = AppRoute.Swap.CurrencyPosition.FROM,
tangemPayInput = AppRoute.Swap.TangemPayInput( tangemPayInput = AppRoute.Swap.TangemPayInput(
cryptoAmount = currentBalance.availableForWithdrawal, cryptoAmount = balance.availableForWithdrawal,
fiatAmount = currentBalance.availableForWithdrawal, fiatAmount = balance.availableForWithdrawal,
depositAddress = depositAddress, depositAddress = balance.cryptoBalance.depositAddress,
isWithdrawal = true, isWithdrawal = true,
), ),
), ),
) )
} }
private fun fetchBalance(): Job {
return modelScope.launch {
val result = try {
cardDetailsRepository.getCardBalance(userWalletId).onRight { balance = it }
} catch (e: Exception) {
TangemLogger.e("Error", e)
return@launch
}
uiState.update(transformer = DetailsBalanceTransformer(balance = result))
}.saveIn(fetchBalanceJobHolder)
}
private fun fetchAddToWalletBanner() { private fun fetchAddToWalletBanner() {
modelScope.launch { modelScope.launch {
val isDone = try { val isDone = try {
@ -263,7 +255,7 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onContactSupportClicked() { override fun onContactSupportClicked() {
analytics.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay)) analytics.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay))
val customerId = loaded?.customerId ?: return val customerId = currentStatus.value.ifLoadedOrNull { it.customerId } ?: return
modelScope.launch { modelScope.launch {
sendFeedbackEmailUseCase.invoke( sendFeedbackEmailUseCase.invoke(
type = FeedbackEmailType.Visa.FeatureIsBeta( type = FeedbackEmailType.Visa.FeatureIsBeta(
@ -277,10 +269,9 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onRefreshSwipe(refreshState: ShowRefreshState) { override fun onRefreshSwipe(refreshState: ShowRefreshState) {
modelScope.launch { modelScope.launch {
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value)) uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value))
cardDetailsEventListener.send(CardDetailsEvent.Hide) paymentAccountStatusFetcher.invoke(userWalletId)
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
txHistoryUpdateListener.triggerUpdate() txHistoryUpdateListener.triggerUpdate()
fetchBalance().join()
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false)) uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false))
}.saveIn(refreshStateJobHolder) }.saveIn(refreshStateJobHolder)
} }

View file

@ -1,45 +1,32 @@
package com.tangem.features.tangempay.model.transformers package com.tangem.features.tangempay.model.transformers
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.formatStyled import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
import java.util.Currency import java.util.Currency
internal class DetailsBalanceTransformer( internal class DetailsBalanceTransformer(
private val balance: Either<UniversalError, TangemPayCardBalance>, private val fiatBalance: PaymentAccountStatusValue.FiatBalance,
) : Transformer<TangemPayDetailsUM> { ) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM { override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
val balance = when (balance) { val balance = TangemPayDetailsBalanceBlockState.Content(
is Either.Left<UniversalError> -> { isBalanceFlickering = false,
TangemPayDetailsBalanceBlockState.Error( fiatBalance = getFiatBalanceText(fiatBalance),
actionButtons = persistentListOf(), actionButtons = prevState.balanceBlockState.actionButtons,
cardsBlockState = prevState.balanceBlockState.cardsBlockState, cardsBlockState = prevState.balanceBlockState.cardsBlockState,
) )
}
is Either.Right<TangemPayCardBalance> -> {
TangemPayDetailsBalanceBlockState.Content(
isBalanceFlickering = false,
fiatBalance = getFiatBalanceText(balance.value),
actionButtons = prevState.balanceBlockState.actionButtons,
cardsBlockState = prevState.balanceBlockState.cardsBlockState,
)
}
}
return prevState.copy(balanceBlockState = balance) return prevState.copy(balanceBlockState = balance)
} }
private fun getFiatBalanceText(balance: TangemPayCardBalance): TextReference { private fun getFiatBalanceText(fiatBalance: PaymentAccountStatusValue.FiatBalance): TextReference {
val currency = Currency.getInstance(balance.currencyCode) val currency = Currency.getInstance(fiatBalance.currency)
return balance.fiatBalance.formatStyled { return fiatBalance.availableBalance.formatStyled {
fiat( fiat(
fiatCurrencyCode = currency.currencyCode, fiatCurrencyCode = currency.currencyCode,
fiatCurrencySymbol = currency.symbol, fiatCurrencySymbol = currency.symbol,

View file

@ -36,6 +36,12 @@ internal inline fun <T> AccountStatus.Payment.ifLoadedOrNull(call: (PaymentAccou
} }
} }
internal fun AccountStatus.Payment.balanceOrNull(): PaymentAccountStatusValue.Balance? = when (val v = value) {
is PaymentAccountStatusValue.Loaded -> v.balance
is PaymentAccountStatusValue.Deactivated -> v.balance
else -> null
}
internal fun AccountStatus.Payment.findCard( internal fun AccountStatus.Payment.findCard(
initialCardId: String, initialCardId: String,
initialStatus: AccountStatus.Payment, initialStatus: AccountStatus.Payment,

View file

@ -85,7 +85,11 @@ internal class TangemPayCardLimitSetupModelTest {
val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) { val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
every { source } returns StatusSource.ACTUAL every { source } returns StatusSource.ACTUAL
every { cards } returns listOf(cardWithLimit) every { cards } returns listOf(cardWithLimit)
every { currencyCode } returns "USD" every { balance } returns mockk(relaxed = true) {
every { fiatBalance } returns mockk(relaxed = true) {
every { currency } returns "USD"
}
}
} }
val paymentStatusWithLimit: AccountStatus.Payment = mockk(relaxed = true) { val paymentStatusWithLimit: AccountStatus.Payment = mockk(relaxed = true) {
every { value } returns statusWithLimit every { value } returns statusWithLimit

View file

@ -58,8 +58,8 @@ internal class TangemPayMainBlockConverter(
subtitle = TextReference.Res(R.string.tangempay_status_deactivated), subtitle = TextReference.Res(R.string.tangempay_status_deactivated),
isBalanceFlickering = statusValue.source == StatusSource.CACHE, isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText( balance = getBalanceText(
currencyCode = statusValue.fiatBalance.currency, currencyCode = statusValue.balance.fiatBalance.currency,
balance = statusValue.fiatBalance.availableBalance, balance = statusValue.balance.fiatBalance.availableBalance,
), ),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol), balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
@ -75,8 +75,8 @@ internal class TangemPayMainBlockConverter(
}, },
isBalanceFlickering = statusValue.source == StatusSource.CACHE, isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText( balance = getBalanceText(
currencyCode = statusValue.currencyCode, currencyCode = statusValue.balance.fiatBalance.currency,
balance = statusValue.fiatBalance.availableBalance, balance = statusValue.balance.fiatBalance.availableBalance,
), ),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol), balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,