Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-29 16:40:30 +04:00
parent 210ba5ca59
commit a89f2c7225
5 changed files with 124 additions and 103 deletions

View file

@ -28,6 +28,10 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.tokens)
implementation(projects.domain.networks)
/** Feature API - remove after removing [HotWalletFeatureToggles] */
implementation(projects.features.hotWallet.api)
/** Project - Utils */
implementation(projects.core.utils)

View file

@ -2,6 +2,7 @@ package com.tangem.data.pay.repository
import arrow.core.Either
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.error.UniversalError
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError
@ -9,39 +10,40 @@ import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.pay.models.response.VisaErrorResponseJsonAdapter
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.networks.repository.NetworksRepository
import com.tangem.domain.pay.datasource.TangemPayAuthDataSource
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.derivations.derivationStyleProvider
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
/**
* For TangemPay Customer Wallet auth we are using polygon address
*/
private const val POL_VALUE = "coin⟨POLYGON⟩polygon-ecosystem-token"
@Suppress("LongParameterList")
internal class TangemPayRequestPerformer @Inject constructor(
@NetworkMoshi moshi: Moshi,
private val dispatchers: CoroutineDispatcherProvider,
private val tangemPayStorage: TangemPayStorage,
private val getCurrencyUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val networksRepository: NetworksRepository,
private val authDataSource: TangemPayAuthDataSource,
private val getWalletsUseCase: GetWalletsUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val userWalletsListRepository: UserWalletsListRepository,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
private var customerWalletAddress: String? = null
@ -79,6 +81,12 @@ internal class TangemPayRequestPerformer @Inject constructor(
)
}
private fun getWallets(): Flow<List<UserWallet>> = if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.userWallets.map { requireNotNull(it) }
} else {
userWalletsListManager.userWallets
}
private suspend fun <T : Any> performRequest(
requestBlock: suspend (header: String) -> ApiResponse<T>,
getTokens: (suspend () -> VisaAuthTokens),
@ -130,26 +138,26 @@ internal class TangemPayRequestPerformer @Inject constructor(
}
private suspend fun fetchAuthInputData(): AuthInputData {
val userWallets = getWalletsUseCase()
val userWallets = getWallets()
.filter { it.isNotEmpty() }
.first()
val wallet = userWallets.find { it is UserWallet.Cold } as? UserWallet.Cold
?: error("Cannot find cold user wallet")
val address = getCurrencyUseCase.invokeMultiWallet(
userWalletId = wallet.walletId,
currencyId = CryptoCurrency.ID.fromValue(POL_VALUE),
isSingleWalletWithTokens = false,
)
.filter { it.getAddress() != null }.first().getAddress() ?: error("Cannot find polygon network address")
val blockchain = Blockchain.Polygon
val derivationPath = getDerivationPath(blockchain, wallet)
val address = networksRepository.getNetworkAddresses(wallet.walletId, Network.RawID(blockchain.id))
.find { it.cryptoCurrency.network.derivationPath.value == derivationPath }?.address
?: error("Cannot get polygon address")
customerWalletAddress = address
return AuthInputData(address, wallet.cardId)
}
private fun Either<CurrencyStatusError, CryptoCurrencyStatus>.getAddress() =
getOrNull()?.value?.networkAddress?.defaultAddress?.value
private fun getDerivationPath(blockchain: Blockchain, wallet: UserWallet) =
blockchain.derivationPath(wallet.derivationStyleProvider.getDerivationStyle())?.rawPath
?: error("Cannot get derivation path")
private suspend fun fetchTokens(): VisaAuthTokens {
val inputData = fetchAuthInputData()

View file

@ -35,7 +35,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.features.biometry.AskBiometryComponent
@ -52,7 +51,7 @@ import kotlinx.coroutines.flow.*
import timber.log.Timber
import javax.inject.Inject
private const val TANGEM_PAY_UPDATE_INTERVAL = 60000L
private const val TANGEM_PAY_UPDATE_INTERVAL = 60_000L
@Suppress("LongParameterList", "LargeClass")
@Stable
@ -92,7 +91,6 @@ internal class WalletModel @Inject constructor(
private val userWalletsListRepository: UserWalletsListRepository,
private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase,
private val tangemPayIssueOrderUseCase: TangemPayIssueOrderUseCase,
private val tangemPayStateConverter: TangemPayStateConverter,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
val screenLifecycleProvider: ScreenLifecycleProvider,
val innerWalletRouter: InnerWalletRouter,
@ -106,11 +104,10 @@ internal class WalletModel @Inject constructor(
private val refreshWalletJobHolder = JobHolder()
private var needToRefreshWallet = false
private val clearNFTCacheJobHolder = JobHolder()
private val updateTangemPayJobHolder = JobHolder()
private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>()
private var updateTangemPayJob: Job? = null
init {
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened)
@ -332,17 +329,15 @@ internal class WalletModel @Inject constructor(
* and every minute while user stays on the main screen
*/
screenLifecycleProvider.isBackgroundState.onEach { inBackground ->
updateTangemPayJobHolder.cancel()
if (!inBackground && tangemPayFeatureToggles.isTangemPayEnabled) {
updateTangemPayJob = modelScope.launch {
modelScope.launch {
refreshTangemPayInfo()
while (isActive) {
delay(TANGEM_PAY_UPDATE_INTERVAL)
refreshTangemPayInfo()
}
}
} else {
updateTangemPayJob?.cancel()
updateTangemPayJob = null
}.saveIn(updateTangemPayJobHolder)
}
}.launchIn(modelScope)
}
@ -350,11 +345,11 @@ internal class WalletModel @Inject constructor(
private suspend fun refreshTangemPayInfo() {
val newState = withContext(dispatchers.io) {
tangemPayMainScreenCustomerInfoUseCase()?.let { info ->
tangemPayStateConverter.convert(
TangemPayStateTransformer(
value = info,
onIssueOrderClick = ::issueOrder,
onContinueKycClick = innerWalletRouter::openTangemPayOnboarding,
)
).transform(stateHolder.uiState.value.tangemPayState)
}
} ?: return
stateHolder.update { it.copy(tangemPayState = newState) }
@ -362,11 +357,19 @@ internal class WalletModel @Inject constructor(
private fun issueOrder() {
modelScope.launch {
stateHolder.update { it.copy(tangemPayState = tangemPayStateConverter.getIssueProgress()) }
stateHolder.update {
it.copy(
tangemPayState = TangemPayStateTransformer(issueProgressState = true)
.transform(it.tangemPayState),
)
}
withContext(dispatchers.io) {
tangemPayIssueOrderUseCase()
} ?: stateHolder.update {
it.copy(tangemPayState = tangemPayStateConverter.getIssueState(::issueOrder))
it.copy(
tangemPayState = TangemPayStateTransformer(issueState = true, onIssueOrderClick = ::issueOrder)
.transform(it.tangemPayState),
)
}
}
}

View file

@ -0,0 +1,74 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus.NOT_ISSUED
import com.tangem.domain.pay.model.OrderStatus.CANCELED
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import com.tangem.utils.transformer.Transformer
import java.util.Currency
internal class TangemPayStateTransformer(
private val value: MainScreenCustomerInfo? = null,
private val onIssueOrderClick: () -> Unit = {},
private val onContinueKycClick: () -> Unit = {},
private val issueProgressState: Boolean = false,
private val issueState: Boolean = false,
) : Transformer<TangemPayState> {
override fun transform(prevState: TangemPayState): TangemPayState = when {
issueProgressState -> createIssueProgressState()
issueState -> createIssueState()
else -> createInitialState()
}
private fun createInitialState(): TangemPayState {
val cardInfo = value?.info?.cardInfo
return when {
value == null -> TangemPayState.Empty
!value.info.isKycApproved() -> createKycInProgressState(onContinueKycClick)
value.orderStatus == NOT_ISSUED || value.orderStatus == CANCELED -> createIssueState()
cardInfo != null -> getCardInfoState(cardInfo)
else -> createIssueProgressState()
}
}
private fun createIssueProgressState(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
showProgress = true,
)
private fun createIssueState() = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onIssueOrderClick,
)
private fun createKycInProgressState(onContinueKycClick: () -> Unit): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onContinueKycClick,
)
private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
)
private fun getBalanceText(cardInfo: CardInfo): String {
val currency = Currency.getInstance(cardInfo.currencyCode)
return cardInfo.balance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
}

View file

@ -1,68 +0,0 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.pay.model.CustomerInfo.CardInfo
import com.tangem.domain.pay.model.MainScreenCustomerInfo
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState
import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress
import java.util.Currency
import javax.inject.Inject
internal class TangemPayStateConverter @Inject constructor() {
fun convert(
value: MainScreenCustomerInfo,
onIssueOrderClick: () -> Unit,
onContinueKycClick: () -> Unit,
): TangemPayState {
val cardInfo = value.info.cardInfo
return when {
!value.info.isKycApproved() -> {
Progress(
title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title),
buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button),
iconRes = R.drawable.ic_promo_kyc_36,
onButtonClick = onContinueKycClick,
)
}
value.orderStatus == OrderStatus.NOT_ISSUED || value.orderStatus == OrderStatus.CANCELED -> {
getIssueState(onIssueOrderClick)
}
cardInfo != null -> {
TangemPayState.Card(
lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"),
balanceText = TextReference.Str(getBalanceText(cardInfo)),
)
}
else -> {
getIssueProgress()
}
}
}
fun getIssueProgress(): TangemPayState = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.EMPTY,
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = {},
showProgress = true,
)
fun getIssueState(onIssueOrderClick: () -> Unit) = Progress(
title = TextReference.Res(R.string.tangempay_issue_card_notification_title),
buttonText = TextReference.Res(R.string.common_continue),
iconRes = R.drawable.ic_tangem_pay_promo_card_36,
onButtonClick = onIssueOrderClick,
)
private fun getBalanceText(cardInfo: CardInfo): String {
val currency = Currency.getInstance(cardInfo.currencyCode)
return cardInfo.balance.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
}
}