Updated on 2026-08-14
This commit is contained in:
parent
210ba5ca59
commit
a89f2c7225
5 changed files with 124 additions and 103 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue