Updated on 2026-08-14
This commit is contained in:
commit
4f5b134c99
16 changed files with 221 additions and 124 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<VisaCardActivationResponse> {
|
||||
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<ExtendedPublicKey> {
|
||||
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<VisaCardActivationResponse> {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<VisaCardActivationStatus> {
|
||||
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<ExtendedPublicKey>,
|
||||
): CompletionResult<VisaCardActivationStatus> {
|
||||
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<VisaCardActivationStatus> {
|
||||
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<SignHashResponse> {
|
||||
val signHashCommand = SignHashCommand(
|
||||
hash = nonce.hexToBytes(),
|
||||
walletPublicKey = publicKey,
|
||||
derivationPath = derivationPath,
|
||||
)
|
||||
return suspendCancellableCoroutine {
|
||||
signHashCommand.run(session) { result ->
|
||||
|
|
|
|||
|
|
@ -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<Set<Network>> = 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 <T, R> Flow<T>.mapDiff(diff: (old: T, new: T) -> R): Flow<R> = flow {
|
||||
var previous: T? = null
|
||||
collect { current ->
|
||||
val prev = previous
|
||||
if (prev != null) {
|
||||
emit(diff(prev, current))
|
||||
}
|
||||
previous = current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,6 +45,8 @@ enum class VisaActivationError(
|
|||
AddressNotMatched(104003008),
|
||||
InconsistentRemoteState(104003009),
|
||||
FailedRemoteState(104003010),
|
||||
VisaCardForApproval(104003011),
|
||||
CardIdNotMatched(104003011),
|
||||
}
|
||||
|
||||
object VisaAuthorizationAPIError : UniversalError {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Unit>()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.walletconnect.transaction.entity.blockaid
|
||||
|
||||
internal data class TransactionCheckResultsUM(
|
||||
val estimatedWalletChanges: WcEstimatedWalletChangesUM,
|
||||
val notificationText: String? = null,
|
||||
)
|
||||
|
|
@ -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<TransactionCheckResultsUM> {
|
||||
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",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue