From a89f2c7225349fc2868ad4448cd61c93152f8347 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Sep 2025 16:40:30 +0400 Subject: [PATCH] Updated on 2026-08-14 --- data/visa/build.gradle.kts | 4 + .../repository/TangemPayRequestPerformer.kt | 50 +++++++------ .../wallet/child/wallet/model/WalletModel.kt | 31 ++++---- .../transformers/TangemPayStateTransformer.kt | 74 +++++++++++++++++++ .../converter/TangemPayStateConverter.kt | 68 ----------------- 5 files changed, 124 insertions(+), 103 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 05adcdc660..ae8f40e9b4 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -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) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index c6f22818b8..dffb6ede24 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -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> = if (hotWalletFeatureToggles.isHotWalletEnabled) { + userWalletsListRepository.userWallets.map { requireNotNull(it) } + } else { + userWalletsListManager.userWallets + } + private suspend fun performRequest( requestBlock: suspend (header: String) -> ApiResponse, 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.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() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index fd4e553563..042e3c9fe3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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() - 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), + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt new file mode 100644 index 0000000000..a54982b7ac --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt @@ -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 { + + 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) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt deleted file mode 100644 index 5fb3f32ccd..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayStateConverter.kt +++ /dev/null @@ -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) - } - } -} \ No newline at end of file