From 438b8ab22bb88ad281ecdc4cda4bdacbbdca3b4d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 16:03:27 +0400 Subject: [PATCH 1/4] Updated on 2026-08-14 --- ...cieve_new_24.xml => ic_receive_new_24.xml} | 0 .../blockaid/TransactionCheckResultsUM.kt | 6 ++ .../blockaid/TransactionCheckResultsItem.kt | 75 +++++++++++++++++++ .../blockaid/WcEstimatedWalletChangesItem.kt | 2 +- .../blockaid/WcTransactionCheckErrorItem.kt | 49 ++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) rename core/ui/src/main/res/drawable/{ic_recieve_new_24.xml => ic_receive_new_24.xml} (100%) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt diff --git a/core/ui/src/main/res/drawable/ic_recieve_new_24.xml b/core/ui/src/main/res/drawable/ic_receive_new_24.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_recieve_new_24.xml rename to core/ui/src/main/res/drawable/ic_receive_new_24.xml diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt new file mode 100644 index 0000000000..6bb884bc3c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/TransactionCheckResultsUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.walletconnect.transaction.entity.blockaid + +internal data class TransactionCheckResultsUM( + val estimatedWalletChanges: WcEstimatedWalletChangesUM, + val notificationText: String? = null, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt new file mode 100644 index 0000000000..a4cbbcc8cc --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt @@ -0,0 +1,75 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM +import com.tangem.features.walletconnect.transaction.entity.blockaid.TransactionCheckResultsUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TransactionCheckResultsItem(item: TransactionCheckResultsUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding(12.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (item.notificationText != null) { + WcTransactionCheckErrorItem(item.notificationText) + } + WcEstimatedWalletChangesItem(item.estimatedWalletChanges) + } +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TransactionCheckResultsItemPreview( + @PreviewParameter(TransactionCheckResultsItemProvider::class) item: TransactionCheckResultsUM, +) { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + TransactionCheckResultsItem(item = item) + } + } +} + +private class TransactionCheckResultsItemProvider : PreviewParameterProvider { + override val values = sequenceOf( + TransactionCheckResultsUM( + notificationText = "The transaction approves erc20 tokens to a known malicious address", + estimatedWalletChanges = WcEstimatedWalletChangesUM( + items = persistentListOf( + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_send_new_24, + title = resourceReference(R.string.common_send), + description = "- 42 USDT", + tokenIconUrl = "https://tangem.com", + ), + WcEstimatedWalletChangeUM( + iconRes = R.drawable.ic_receive_new_24, + title = resourceReference(R.string.common_receive), + description = "+ 1,131.46 MATIC", + tokenIconUrl = "https://tangem.com", + ), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt index b0b37f31ac..cbe8e3f59a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesItem.kt @@ -138,7 +138,7 @@ private class EstimatedWalletChangesPreviewProviderTwoItems : tokenIconUrl = "https://tangem.com", ), WcEstimatedWalletChangeUM( - iconRes = R.drawable.ic_recieve_new_24, + iconRes = R.drawable.ic_receive_new_24, title = resourceReference(R.string.common_receive), description = "+ 1,131.46 MATIC", tokenIconUrl = "https://tangem.com", diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt new file mode 100644 index 0000000000..a8431340b3 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcTransactionCheckErrorItem.kt @@ -0,0 +1,49 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.impl.R + +@Composable +internal fun WcTransactionCheckErrorItem(notificationText: String, modifier: Modifier = Modifier) { + Notification( + modifier = modifier + .fillMaxWidth(), + config = NotificationConfig( + title = resourceReference(R.string.wc_malicious_transaction), + subtitle = TextReference.Str(notificationText), + iconResId = R.drawable.ic_alert_circle_24, + ), + containerColor = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + titleColor = TangemTheme.colors.text.warning, + subtitleColor = TangemTheme.colors.text.primary1, + iconTint = TangemTheme.colors.icon.warning, + ) +} + +@Composable +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WcTransactionCheckErrorItemPreview() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + WcTransactionCheckErrorItem("The transaction approves erc20 tokens to a known malicious address") + } + } +} \ No newline at end of file From c38fa7ad19bdb20d8761a69faaec80c37d4ba2e0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 15:10:32 +0300 Subject: [PATCH 2/4] Updated on 2026-08-14 --- .../tasks/visa/VisaCardActivationTask.kt | 59 ++---------------- .../visa/VisaCustomerWalletApproveTask.kt | 9 +-- .../tap/domain/visa/VisaCardScanHandler.kt | 62 ++----------------- .../com/tangem/domain/visa/error/VisaError.kt | 2 + 4 files changed, 15 insertions(+), 117 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index dd4e607371..3b07d873a0 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -14,12 +14,10 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.hasSavedOTP -import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError @@ -29,7 +27,6 @@ import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.attestation.AttestCardKeyCommand -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse @@ -95,23 +92,7 @@ class VisaCardActivationTask @AssistedInject constructor( context.signAuthorizationChallenge(mode.authorizationChallenge) } is VisaCardActivationTaskMode.SignOnly -> { - val wallet = - card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } - ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - - val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - context.signData( - mode.dataToSignByCardWallet, - derivedPublicKey, - ) + context.signData(mode.dataToSignByCardWallet) } } } @@ -164,16 +145,7 @@ class VisaCardActivationTask @AssistedInject constructor( card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } .value @@ -190,10 +162,7 @@ class VisaCardActivationTask @AssistedInject constructor( otpTaskDeferred.await() - signData( - dataToSign = dataToSign, - derivedPublicKey = derivedPublicKey, - ) + signData(dataToSign = dataToSign) } } @@ -294,7 +263,6 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.signData( dataToSign: VisaDataToSignByCardWallet, - derivedPublicKey: ExtendedPublicKey, ): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) @@ -306,7 +274,6 @@ class VisaCardActivationTask @AssistedInject constructor( val task = SignHashCommand( hash = dataToSign.hashToSign.hexToBytes(), walletPublicKey = wallet.publicKey, - derivationPath = VisaUtilities.visaDefaultDerivationPath, ) val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { @@ -325,7 +292,7 @@ class VisaCardActivationTask @AssistedInject constructor( handleSignedData( dataToSign = dataToSign, response = result.data, - derivedPublicKey = derivedPublicKey, + walletPublicKey = wallet.publicKey, ) } is CompletionResult.Failure -> { @@ -335,23 +302,9 @@ class VisaCardActivationTask @AssistedInject constructor( } } - private suspend fun SessionContext.deriveKey(publicKey: ByteArray): CompletionResult { - val derivationPath = VisaUtilities.visaDefaultDerivationPath - ?: return CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError) - - val derivationTask = DeriveWalletPublicKeyTask(publicKey, derivationPath) - val derivationTaskResult = suspendCancellableCoroutine { continuation -> - derivationTask.run(session) { result -> - continuation.resume(result) - } - } - - return derivationTaskResult - } - private suspend fun SessionContext.handleSignedData( dataToSign: VisaDataToSignByCardWallet, - derivedPublicKey: ExtendedPublicKey, + walletPublicKey: ByteArray, response: SignHashResponse, ): CompletionResult { val otp = otpStorage.getOTP(cardId) ?: run { @@ -362,7 +315,7 @@ class VisaCardActivationTask @AssistedInject constructor( val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( signature = response.signature, hash = dataToSign.hashToSign.hexToBytes(), - publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), + publicKey = walletPublicKey.toDecompressedPublicKey(), ).asRSVLegacyEVM().toHexString() val signedActivationData = dataToSign.sign( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 55dd835a3e..232724142a 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -40,17 +40,12 @@ class VisaCustomerWalletApproveTask( } if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) { - // TODO TVF-21 - callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve"))) + callback(CompletionResult.Failure(VisaActivationError.VisaCardForApproval.tangemError)) return } if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { - callback( - CompletionResult.Failure( - TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), // TODO TVF-21 - ), - ) + callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) return } diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index b83d23f8d9..e31948a49c 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -2,17 +2,13 @@ package com.tangem.tap.domain.visa import arrow.core.getOrElse import com.tangem.common.CompletionResult -import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage -import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.error.VisaAuthorizationAPIError @@ -22,7 +18,6 @@ import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyResponse -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse import kotlinx.coroutines.suspendCancellableCoroutine @@ -61,54 +56,19 @@ internal class VisaCardScanHandler @Inject constructor( session = session, ) - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { val activationInput = VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet) val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput) return CompletionResult.Success(activationStatus) } - return context.deriveKey(wallet) - } - - private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult { - val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { - Timber.e("Failed to create derivation path while first scan") - - return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) - } - - val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath) - val derivationTaskResult = suspendCancellableCoroutine { continuation -> - derivationTask.run(session) { result -> - continuation.resume(result) - } - } - return handleDerivationResponse(derivationTaskResult) - } - - private suspend fun SessionContext.handleDerivationResponse( - result: CompletionResult, - ): CompletionResult { - return when (result) { - is CompletionResult.Success -> { - Timber.i("Start task for loading challenge for Visa wallet") - handleWalletAuthorization() - } - is CompletionResult.Failure -> { - CompletionResult.Failure(result.error) - } - } + return context.handleWalletAuthorization() } private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult { Timber.i("Started handling authorization using Visa wallet") - val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { - Timber.e("Failed to create derivation path while handling wallet authorization") - return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) - } - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { @@ -116,12 +76,7 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError) } - val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run { - Timber.e("Failed to find extended public key while handling wallet authorization") - return CompletionResult.Failure(VisaCardScanError.FailedToFindDerivedWalletKey.tangemError) - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(extendedPublicKey.publicKey) + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } @@ -132,9 +87,7 @@ internal class VisaCardScanHandler @Inject constructor( error("sign and get specific error to switch to card_id flow") visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) }.getOrElse { - Timber.i( - "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", - ) + Timber.i("Failed to get Access token for Wallet public key authorization. Authorizing using Card Pub key") return handleCardAuthorization( cardWalletAddress = walletAddress.value, ) @@ -142,7 +95,6 @@ internal class VisaCardScanHandler @Inject constructor( val signChallengeResult = signChallengeWithWallet( publicKey = wallet.publicKey, - derivationPath = derivationPath, nonce = challengeResponse.challenge, ) @@ -169,9 +121,7 @@ internal class VisaCardScanHandler @Inject constructor( val authorizationTokensResponse = runCatching { visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) }.getOrElse { - Timber.i( - "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", - ) + Timber.i("Failed to get Access token for Wallet public key authorization. Authorizing using Card Pub key") return handleCardAuthorization( cardWalletAddress = cardWalletAddress, ) @@ -271,13 +221,11 @@ internal class VisaCardScanHandler @Inject constructor( private suspend fun SessionContext.signChallengeWithWallet( publicKey: ByteArray, - derivationPath: DerivationPath, nonce: String, ): CompletionResult { val signHashCommand = SignHashCommand( hash = nonce.hexToBytes(), walletPublicKey = publicKey, - derivationPath = derivationPath, ) return suspendCancellableCoroutine { signHashCommand.run(session) { result -> diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index bf9bd888dd..6ddfd6c0d7 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -45,6 +45,8 @@ enum class VisaActivationError( AddressNotMatched(104003008), InconsistentRemoteState(104003009), FailedRemoteState(104003010), + VisaCardForApproval(104003011), + CardIdNotMatched(104003011), } object VisaAuthorizationAPIError : UniversalError { From 2689850df00af6f947fae022f2042fe182894093 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 17:16:17 +0500 Subject: [PATCH 3/4] Updated on 2026-08-14 --- .../tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt index 1566f4d125..a5941e042d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt @@ -55,7 +55,7 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi PrimaryButton( modifier = Modifier .padding(top = TangemTheme.dimens.spacing48) - .wrapContentWidth(), + .widthIn(min = TangemTheme.dimens.size158), text = stringResourceSafe(R.string.nft_collections_receive), onClick = state.onReceiveClick, ) From c8e19f4cd6cdf3aa32921d7b316e767b2736f5f8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 19 May 2025 17:16:47 +0500 Subject: [PATCH 4/4] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 9 +++++ .../ObserveAndClearNFTCacheIfNeedUseCase.kt | 38 +++++++++++++++++++ .../wallet/child/wallet/model/WalletModel.kt | 11 ++++++ .../implementors/MultiWalletContentLoader.kt | 3 ++ .../MultiWalletContentLoaderFactory.kt | 3 ++ .../subscribers/WalletNFTListSubscriber.kt | 15 +++++--- 6 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 26161f3d76..07c16b3c3f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -127,4 +127,13 @@ internal object NFTDomainModule { fun provideGetWalletNFTEnabledUseCase(walletsRepository: WalletsRepository): GetWalletNFTEnabledUseCase { return GetWalletNFTEnabledUseCase(walletsRepository) } + + @Provides + @Singleton + fun provideClearNFTCacheUseCase( + nftRepository: NFTRepository, + currenciesRepository: CurrenciesRepository, + ): ObserveAndClearNFTCacheIfNeedUseCase { + return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt new file mode 100644 index 0000000000..dec9562a2a --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/ObserveAndClearNFTCacheIfNeedUseCase.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.* + +class ObserveAndClearNFTCacheIfNeedUseCase( + private val nftRepository: NFTRepository, + private val currenciesRepository: CurrenciesRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow> = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .map { it.map(CryptoCurrency::network) } + .mapDiff { old, new -> + // calculate networks sets difference to determine which networks were removed + old.toSet() - new.toSet() + } + .distinctUntilChanged() + .onEach { removedNetworks -> + if (removedNetworks.isNotEmpty()) { + nftRepository.clearCache(userWalletId, removedNetworks.toList()) + } + } + + private fun Flow.mapDiff(diff: (old: T, new: T) -> R): Flow = flow { + var previous: T? = null + collect { current -> + val prev = previous + if (prev != null) { + emit(diff(prev, current)) + } + previous = current + } + } +} \ No newline at end of file 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 1fe0628b59..2360c86709 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 @@ -16,6 +16,7 @@ import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.ReferralDeepLink import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase @@ -84,6 +85,7 @@ internal class WalletModel @Inject constructor( private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val appRouter: AppRouter, private val routingFeatureToggle: RoutingFeatureToggle, + private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -94,6 +96,7 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() private val expressStatusJobHolder = JobHolder() + private val clearNFTCacheJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -224,6 +227,7 @@ internal class WalletModel @Inject constructor( } subscribeOnExpressTransactionsUpdates(selectedWallet) subscribeToScreenBackgroundState(selectedWallet) + observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -283,6 +287,13 @@ internal class WalletModel @Inject constructor( ) } + private fun observeAndClearNFTCacheIfNeedUseCase(selectedWallet: UserWallet) { + observeAndClearNFTCacheIfNeedUseCase + .invoke(selectedWallet.walletId) + .launchIn(modelScope) + .saveIn(clearNFTCacheJobHolder) + } + private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 62325f8bfe..007c74fcc6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -8,6 +8,7 @@ import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase @@ -47,6 +48,7 @@ internal class MultiWalletContentLoader( private val deepLinksRegistry: DeepLinksRegistry, private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, + private val currenciesRepository: CurrenciesRepository, private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletContentLoader(id = userWallet.walletId) { @@ -72,6 +74,7 @@ internal class MultiWalletContentLoader( stateHolder = stateHolder, walletsRepository = walletsRepository, clickIntents = clickIntents, + currenciesRepository = currenciesRepository, ).let(::add) } MultiWalletWarningsSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 44aa146b20..9cdb3f5767 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -8,6 +8,7 @@ import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase @@ -42,6 +43,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val walletsRepository: WalletsRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val routingFeatureToggle: RoutingFeatureToggle, + private val currenciesRepository: CurrenciesRepository, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -65,6 +67,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( walletsRepository = walletsRepository, getNFTCollectionsUseCase = getNFTCollectionsUseCase, routingFeatureToggle = routingFeatureToggle, + currenciesRepository = currenciesRepository, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt index 57ed2a8b6d..329dc0268c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -15,17 +16,21 @@ internal class WalletNFTListSubscriber( private val userWallet: UserWallet, private val stateHolder: WalletStateController, private val walletsRepository: WalletsRepository, + private val currenciesRepository: CurrenciesRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val clickIntents: WalletClickIntents, ) : WalletSubscriber() { @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> = walletsRepository - .nftEnabledStatus(userWallet.walletId) + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine( + walletsRepository.nftEnabledStatus(userWallet.walletId), + currenciesRepository.getWalletCurrenciesUpdates(userWallet.walletId), + ) { nftEnabled, currencies -> nftEnabled to currencies } .distinctUntilChanged() - .flatMapLatest { nftEnabled -> - // if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need - if (nftEnabled) { + .flatMapLatest { (nftEnabled, currencies) -> + // if NFT is enabled for this wallet and there are currencies, + // then start observing changes from store and apply transformer if need + if (nftEnabled && currencies.isNotEmpty()) { getNFTCollectionsUseCase(userWallet.walletId) .shareIn( scope = coroutineScope,