Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-09 16:11:28 +04:00
parent c3ea39c47d
commit fb4020ac91
13 changed files with 160 additions and 152 deletions

View file

@ -0,0 +1,64 @@
package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import timber.log.Timber
import javax.inject.Inject
private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory"
/**
* Custom token parameters. Will be used only for F&F.
*/
private const val TOKEN_ID = "usd-coin"
private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
) : TangemPayCryptoCurrencyFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
NetworkFactory(excludedBlockchains)
}
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
override fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency> {
return catch {
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
val blockchain = requireNotNull(chain.blockchain)
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
)
cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
)
}.mapLeft { exception ->
Timber.tag(TAG).e(exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,85 +0,0 @@
package com.tangem.data.pay
import arrow.core.Either
import arrow.core.Either.Companion.catch
import com.squareup.moshi.Moshi
import com.tangem.blockchain.blockchains.ethereum.Chain
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.core.error.UniversalError
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.network.NetworkFactory
import com.tangem.data.pay.util.TangemPayErrorConverter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPaySwapDataFactory
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
private const val TAG = "TangemPay: DefaultDataForTopUpFactory"
/**
* Custom token parameters. Will be used only for F&F.
*/
private const val TOKEN_ID = "usd-coin"
private const val TOKEN_NAME = "USDC"
private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359"
private const val TOKEN_DECIMALS = 6
internal class DefaultTangemPaySwapDataFactory @Inject constructor(
@NetworkMoshi moshi: Moshi,
excludedBlockchains: ExcludedBlockchains,
) : TangemPaySwapDataFactory {
private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
CryptoCurrencyFactory(excludedBlockchains)
}
private val networkFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
NetworkFactory(excludedBlockchains)
}
private val errorConverter by lazy(mode = LazyThreadSafetyMode.NONE) { TangemPayErrorConverter(moshi) }
private fun getCurrency(userWallet: UserWallet, chainId: Int): CryptoCurrency {
val chain = requireNotNull(Chain.entries.find { it.id == chainId }) { "Can not find chain with $chainId" }
val blockchain = requireNotNull(chain.blockchain)
val network = networkFactory.create(
blockchain = blockchain,
extraDerivationPath = null,
userWallet = userWallet,
)
return cryptoCurrencyFactory.createToken(
network = requireNotNull(network),
rawId = CryptoCurrency.RawID(TOKEN_ID),
name = TOKEN_NAME,
symbol = TOKEN_NAME,
contractAddress = TOKEN_CONTRACT_ADDRESS,
decimals = TOKEN_DECIMALS,
)
}
override fun create(
userWallet: UserWallet,
depositAddress: String,
chainId: Int,
cryptoBalance: BigDecimal,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData> {
return catch {
val currency = getCurrency(userWallet, chainId)
TangemPayTopUpData(
currency = currency,
walletId = userWallet.walletId,
cryptoBalance = cryptoBalance,
fiatBalance = fiatBalance,
depositAddress = depositAddress,
receiveAddress = listOf(ReceiveAddressModel(nameService = NameService.Default, value = depositAddress)),
)
}.mapLeft { exception ->
Timber.tag(TAG).e(exception)
errorConverter.convert(exception)
}
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.data.pay.di
import com.tangem.data.pay.DefaultTangemPaySwapDataFactory
import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory
import com.tangem.data.pay.repository.*
import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase
import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.repository.*
import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase
import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase
@ -48,7 +48,9 @@ internal interface TangemPayDataModule {
@Binds
@Singleton
fun bindTangemPaySwapDataFactory(factory: DefaultTangemPaySwapDataFactory): TangemPaySwapDataFactory
fun bindTangemPayCryptoCurrencyFactory(
factory: DefaultTangemPayCryptoCurrencyFactory,
): TangemPayCryptoCurrencyFactory
@Binds
@Singleton

View file

@ -55,6 +55,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor(
fiatBalance = result.fiat.availableBalance,
currencyCode = result.fiat.currency,
cryptoBalance = result.crypto.balance,
availableForWithdrawal = result.availableForWithdrawal.amount,
chainId = result.crypto.chainId,
depositAddress = result.crypto.depositAddress,
contractAddress = result.crypto.tokenContractAddress,

View file

@ -0,0 +1,11 @@
package com.tangem.domain.pay
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
interface TangemPayCryptoCurrencyFactory {
fun create(userWallet: UserWallet, chainId: Int): Either<UniversalError, CryptoCurrency>
}

View file

@ -6,6 +6,7 @@ data class TangemPayCardBalance(
val fiatBalance: BigDecimal,
val currencyCode: String,
val cryptoBalance: BigDecimal,
val availableForWithdrawal: BigDecimal,
val chainId: Int,
val depositAddress: String?,
val contractAddress: String,

View file

@ -1,24 +1,10 @@
package com.tangem.domain.pay
package com.tangem.domain.pay.model
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import java.math.BigDecimal
interface TangemPaySwapDataFactory {
fun create(
userWallet: UserWallet,
depositAddress: String,
chainId: Int,
cryptoBalance: BigDecimal,
fiatBalance: BigDecimal,
): Either<UniversalError, TangemPayTopUpData>
}
data class TangemPayTopUpData(
val walletId: UserWalletId,
val currency: CryptoCurrency,

View file

@ -5,7 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.features.tangempay.model.TangemPayAddFundsModel
import com.tangem.features.tangempay.ui.TangemPayAddFundsContent
import java.math.BigDecimal

View file

@ -4,7 +4,10 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.models.ReceiveAddressModel
import com.tangem.domain.models.ReceiveAddressModel.NameService
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.tangempay.components.TangemPayAddFundsComponent
import com.tangem.features.tangempay.entity.TangemPayAddFundsUM
@ -17,7 +20,7 @@ import javax.inject.Inject
internal class TangemPayAddFundsModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model() {
@ -26,17 +29,25 @@ internal class TangemPayAddFundsModel @Inject constructor(
val uiState: TangemPayAddFundsUM = getInitialState()
private fun getInitialState(): TangemPayAddFundsUM {
val userWallet = requireNotNull(
getUserWalletUseCase(params.walletId).getOrNull(),
) { "User wallet not found for id: ${params.walletId}" }
val data = tangemPaySwapDataFactory.create(
userWallet = userWallet,
depositAddress = params.depositAddress,
chainId = params.chainId,
cryptoBalance = params.cryptoBalance,
fiatBalance = params.fiatBalance,
).getOrNull()
val userWallet = getUserWalletUseCase(params.walletId).getOrNull()
val currency = userWallet?.let {
tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.chainId).getOrNull()
}
val data = currency?.let {
TangemPayTopUpData(
currency = currency,
walletId = params.walletId,
cryptoBalance = params.cryptoBalance,
fiatBalance = params.fiatBalance,
depositAddress = params.depositAddress,
receiveAddress = listOf(
ReceiveAddressModel(
nameService = NameService.Default,
value = params.depositAddress,
),
),
)
}
return TangemPayAddFundsUMConverter(listener = params.listener).convert(data)
}

View file

@ -18,8 +18,8 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.models.TokenReceiveConfig
import com.tangem.domain.pay.TangemPaySwapDataFactory
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.repository.CustomerOrderRepository
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
@ -70,7 +70,7 @@ internal class TangemPayDetailsModel @Inject constructor(
private val uiMessageSender: UiMessageSender,
private val cardDetailsEventListener: CardDetailsEventListener,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPaySwapDataFactory: TangemPaySwapDataFactory,
private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val orderRepository: CustomerOrderRepository,
private val getUserWalletUseCase: GetUserWalletUseCase,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
@ -236,27 +236,22 @@ internal class TangemPayDetailsModel @Inject constructor(
if (hasActiveWithdrawal) {
showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress)
} else {
val userWallet = requireNotNull(
getUserWalletUseCase(params.userWalletId).getOrNull(),
) { "User wallet not found: ${params.userWalletId}" }
val data = tangemPaySwapDataFactory.create(
userWallet = userWallet,
depositAddress = depositAddress,
chainId = params.config.chainId,
cryptoBalance = currentBalance.cryptoBalance,
fiatBalance = currentBalance.fiatBalance,
).getOrNull()
if (data != null) {
val userWallet = getUserWalletUseCase(params.userWalletId).getOrNull()
val currency = userWallet?.let {
tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId)
.getOrNull()
}
if (currency != null) {
router.push(
AppRoute.Swap(
currencyFrom = data.currency,
userWalletId = data.walletId,
currencyFrom = currency,
userWalletId = params.userWalletId,
isInitialReverseOrder = false,
screenSource = AnalyticsParam.ScreensSources.TangemPay.value,
tangemPayInput = AppRoute.Swap.TangemPayInput(
cryptoAmount = data.cryptoBalance,
fiatAmount = data.fiatBalance,
depositAddress = data.depositAddress,
cryptoAmount = currentBalance.availableForWithdrawal,
fiatAmount = currentBalance.availableForWithdrawal,
depositAddress = depositAddress,
isWithdrawal = true,
),
),
@ -277,7 +272,13 @@ internal class TangemPayDetailsModel @Inject constructor(
Timber.e(e)
return@launch
}
uiState.update(DetailsBalanceTransformer(balance = result))
uiState.update(
transformer = DetailsBalanceTransformer(
balance = result,
userWallet = getUserWalletUseCase(params.userWalletId).getOrNull(),
cryptoCurrencyFactory = tangemPayCryptoCurrencyFactory,
),
)
}.saveIn(fetchBalanceJobHolder)
}

View file

@ -2,17 +2,24 @@ package com.tangem.features.tangempay.model.transformers
import arrow.core.Either
import com.tangem.core.error.UniversalError
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory
import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.util.Currency
internal class DetailsBalanceTransformer(
private val balance: Either<UniversalError, TangemPayCardBalance>,
private val cryptoCurrencyFactory: TangemPayCryptoCurrencyFactory,
private val userWallet: UserWallet?,
) : Transformer<TangemPayDetailsUM> {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
@ -21,22 +28,32 @@ internal class DetailsBalanceTransformer(
TangemPayDetailsBalanceBlockState.Error(actionButtons = persistentListOf())
}
is Either.Right<TangemPayCardBalance> -> {
TangemPayDetailsBalanceBlockState.Content(
isBalanceFlickering = false,
fiatBalance = getBalanceText(balance.value),
// TODO [REDACTED_TASK_KEY]: Add crypto balance when the BFF is ready
cryptoBalance = "",
actionButtons = prevState.balanceBlockState.actionButtons,
)
val cryptoCurrency = userWallet?.let {
cryptoCurrencyFactory.create(userWallet, balance.value.chainId).getOrNull()
}
if (cryptoCurrency == null) {
TangemPayDetailsBalanceBlockState.Error(actionButtons = persistentListOf())
} else {
TangemPayDetailsBalanceBlockState.Content(
isBalanceFlickering = false,
fiatBalance = getFiatBalanceText(balance.value),
cryptoBalance = getCryptoBalanceText(balance.value.cryptoBalance, cryptoCurrency),
actionButtons = prevState.balanceBlockState.actionButtons,
)
}
}
}
return prevState.copy(balanceBlockState = balance)
}
private fun getBalanceText(balance: TangemPayCardBalance): String {
private fun getFiatBalanceText(balance: TangemPayCardBalance): String {
val currency = Currency.getInstance(balance.currencyCode)
return balance.fiatBalance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
private fun getCryptoBalanceText(cryptoBalance: BigDecimal, cryptoCurrency: CryptoCurrency): String {
return cryptoBalance.format { crypto(cryptoCurrency = cryptoCurrency) }
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.pay.TangemPayTopUpData
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.features.tangempay.components.AddFundsListener
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.tangempay.entity.TangemPayAddFundsItemUM

View file

@ -157,12 +157,11 @@ internal fun TangemPayDetailsBalanceBlock(
state = state,
isBalanceHidden = isBalanceHidden,
)
// TODO [REDACTED_TASK_KEY]: Uncomment after adding crypto balance when the BFF is ready
// CryptoBalance(
// modifier = Modifier.padding(start = 12.dp, top = 4.dp),
// state = state,
// isBalanceHidden = isBalanceHidden,
// )
CryptoBalance(
modifier = Modifier.padding(start = 12.dp, top = 4.dp),
state = state,
isBalanceHidden = isBalanceHidden,
)
if (state.actionButtons.isNotEmpty()) {
HorizontalActionChips(
modifier = Modifier.padding(top = 12.dp),