diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8d88a7037..d592ad2b98 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,6 +114,8 @@ dependencies { implementation(projects.domain.notifications) implementation(projects.domain.notifications.models) implementation(projects.domain.notifications.toggles) + implementation(projects.domain.swap.models) + implementation(projects.domain.swap) implementation(projects.common) implementation(projects.common.routing) @@ -160,6 +162,7 @@ dependencies { implementation(projects.data.quotes) implementation(projects.data.blockaid) implementation(projects.data.notifications) + implementation(projects.data.swap) /** Features */ implementation(projects.features.referral.impl) @@ -170,6 +173,8 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.data) + implementation(projects.features.swapV2.api) + implementation(projects.features.swapV2.impl) implementation(projects.features.tester.api) implementation(projects.features.tester.impl) implementation(projects.features.wallet.api) @@ -210,8 +215,8 @@ dependencies { implementation(projects.features.walletconnect.impl) implementation(projects.features.usedesk.api) implementation(projects.features.usedesk.impl) - implementation(projects.features.feeSelector.api) - implementation(projects.features.feeSelector.impl) + implementation(projects.features.hotWallet.api) + implementation(projects.features.hotWallet.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt index 9859e3e4be..28f8fdf2a6 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaAuthTokenStorage.kt @@ -3,11 +3,9 @@ package com.tangem.tap.data import android.content.Context import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.visa.model.VisaAuthTokens -import com.tangem.sdk.storage.AndroidSecureStorage -import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext @@ -21,11 +19,9 @@ internal class DefaultVisaAuthTokenStorage @Inject constructor( ) : VisaAuthTokenStorage { private val secureStorage by lazy { - AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "visa_auth_storage", - ), + AndroidSecureStorageV2( + appContext = applicationContext, + name = "visa_auth_storage", ) } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt index b87d3fbfdf..e6ad08f73d 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt @@ -3,11 +3,9 @@ package com.tangem.tap.data import android.content.Context import com.tangem.common.extensions.toByteArray import com.tangem.common.extensions.toInt -import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOtpData -import com.tangem.sdk.storage.AndroidSecureStorage -import com.tangem.sdk.storage.createEncryptedSharedPreferences +import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.withContext @@ -21,11 +19,9 @@ class DefaultVisaOTPStorage @Inject constructor( ) : VisaOTPStorage { private val secureStorage by lazy { - AndroidSecureStorage( - preferences = SecureStorage.createEncryptedSharedPreferences( - context = applicationContext, - storageName = "visa_otp_storage", - ), + AndroidSecureStorageV2( + appContext = applicationContext, + name = "visa_otp_storage", ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index c78eeae98f..a10bcc05e1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -5,7 +5,7 @@ import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -71,7 +71,7 @@ internal object ManageTokensDomainModule { derivationsRepository: DerivationsRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveManagedTokensUseCase { @@ -82,7 +82,7 @@ internal object ManageTokensDomainModule { derivationsRepository = derivationsRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 95b8accbf9..d23de84aaf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -6,8 +6,8 @@ import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository @@ -52,8 +52,8 @@ object MarketsDomainModule { @Provides @Singleton - fun provideGetTokenQuotesUseCase(singleQuoteSupplier: SingleQuoteSupplier): GetCurrencyQuotesUseCase { - return GetCurrencyQuotesUseCase(singleQuoteSupplier = singleQuoteSupplier) + fun provideGetTokenQuotesUseCase(singleQuoteStatusSupplier: SingleQuoteStatusSupplier): GetCurrencyQuotesUseCase { + return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier) } @Provides @@ -64,7 +64,7 @@ object MarketsDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveMarketTokensUseCase { @@ -74,7 +74,7 @@ object MarketsDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) 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 07c16b3c3f..5f525bc14c 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 @@ -3,8 +3,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.quotes.single.SingleQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -92,18 +92,18 @@ internal object NFTDomainModule { @Singleton fun provideGetNFTPriceUseCase( nftRepository: NFTRepository, - singleQuoteSupplier: SingleQuoteSupplier, + singleQuoteStatusSupplier: SingleQuoteStatusSupplier, ): GetNFTPriceUseCase { - return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier) + return GetNFTPriceUseCase(nftRepository, singleQuoteStatusSupplier) } @Provides @Singleton fun provideFetchNFTPriceUseCase( nftRepository: NFTRepository, - singleQuoteFetcher: SingleQuoteFetcher, + singleQuoteStatusFetcher: SingleQuoteStatusFetcher, ): FetchNFTPriceUseCase { - return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) + return FetchNFTPriceUseCase(nftRepository, singleQuoteStatusFetcher) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt index e94429b646..a392d9c30e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SwapDomainModule.kt @@ -1,12 +1,15 @@ package com.tangem.tap.di.domain +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase -import com.tangem.feature.swap.domain.api.SwapRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +import com.tangem.feature.swap.domain.api.SwapRepository as OldSwapRepository /** [REDACTED_AUTHOR] @@ -17,7 +20,19 @@ internal object SwapDomainModule { @Provides @Singleton - fun provideGetAvailablePairsUseCase(swapRepository: SwapRepository): GetAvailablePairsUseCase { + fun provideGetAvailablePairsUseCase(swapRepository: OldSwapRepository): GetAvailablePairsUseCase { return GetAvailablePairsUseCase(swapRepository = swapRepository) } + + @Provides + @Singleton + fun provideGetSwapSupportedPairsUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapErrorResolver: SwapErrorResolver, + ): GetSwapSupportedPairsUseCase { + return GetSwapSupportedPairsUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapErrorResolver = swapErrorResolver, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 936104ecf9..68ed536c3d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -8,9 +8,9 @@ import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.quotes.QuotesRepositoryV2 -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher @@ -42,7 +42,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): AddCryptoCurrenciesUseCase { @@ -50,7 +50,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -62,7 +62,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchTokenListUseCase { @@ -70,7 +70,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -161,7 +161,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCurrencyStatusUseCase { @@ -169,7 +169,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -181,7 +181,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCardTokenListUseCase { @@ -189,7 +189,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) @@ -346,11 +346,11 @@ internal object TokensDomainModule { @Singleton fun provideRefreshMultiCurrencyWalletQuotesUseCase( currenciesRepository: CurrenciesRepository, - multiQuoteFetcher: MultiQuoteFetcher, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, ): RefreshMultiCurrencyWalletQuotesUseCase { return RefreshMultiCurrencyWalletQuotesUseCase( currenciesRepository = currenciesRepository, - multiQuoteFetcher = multiQuoteFetcher, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, ) } @@ -368,27 +368,27 @@ internal object TokensDomainModule { fun provideBaseCurrenciesStatusesOperations( tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, - quotesRepositoryV2: QuotesRepositoryV2, + quotesRepository: QuotesRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, - singleQuoteSupplier: SingleQuoteSupplier, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, - quotesRepositoryV2 = quotesRepositoryV2, + quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, - singleQuoteSupplier = singleQuoteSupplier, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, @@ -400,27 +400,27 @@ internal object TokensDomainModule { fun provideBaseCurrencyStatusOperations( tokensFeatureToggles: TokensFeatureToggles, currenciesRepository: CurrenciesRepository, - quotesRepositoryV2: QuotesRepositoryV2, + quotesRepository: QuotesRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - multiQuoteFetcher: MultiQuoteFetcher, - singleQuoteSupplier: SingleQuoteSupplier, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + singleQuoteStatusSupplier: SingleQuoteStatusSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, - quotesRepositoryV2 = quotesRepositoryV2, + quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, multiNetworkStatusFetcher = multiNetworkStatusFetcher, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - multiQuoteFetcher = multiQuoteFetcher, - singleQuoteSupplier = singleQuoteSupplier, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 6fdd327d88..f2e8fb0312 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage +import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager @@ -80,6 +81,10 @@ internal object UserWalletsListManagerModule { context = applicationContext, storageName = "user_wallets_storage", ), + androidSecureStorageV2 = AndroidSecureStorageV2( + appContext = applicationContext, + name = "user_wallets_storage2", + ), ) val authenticatedStorage = AuthenticatedStorage( 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 46a8dadc5e..4d9a809e11 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 @@ -83,11 +83,14 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Requesting challenge for wallet authorization") - val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) - .getOrElse { - Timber.i("Failed to get Access token for Wallet public key authorization") - return CompletionResult.Failure(it.tangemError) - } + val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge( + cardId = card.cardId, + // This is the wallet public key, not the address and it's alright, as the API expects it in this format + cardWalletAddress = wallet.publicKey.toHexString(), + ).getOrElse { + Timber.i("Failed to get Access token for Wallet public key authorization") + return CompletionResult.Failure(it.tangemError) + } val signChallengeResult = signChallengeWithWallet( publicKey = wallet.publicKey, @@ -96,15 +99,16 @@ internal class VisaCardScanHandler @Inject constructor( return when (signChallengeResult) { is CompletionResult.Success -> { - val signature = signChallengeResult.data.cardSignature ?: run { - Timber.i("Failed to sign challenge with Wallet public key") - return CompletionResult.Failure(VisaCardScanError.FailedToSignChallenge.tangemError) - } + val signature = signChallengeResult.data.walletSignature + val salt = signChallengeResult.data.salt Timber.i("Challenge signed with Wallet public key") handleWalletAuthorizationTokens( cardWalletAddress = walletAddress.value, - signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()), + signedChallenge = challengeResponse.toSignedChallenge( + signedChallenge = signature.toHexString(), + salt = salt.toHexString(), + ), ) } is CompletionResult.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt index 85e57a4ae2..ade00b0024 100644 --- a/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/DefaultHomeComponent.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.utils.findActivity import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.api.HomeComponent import com.tangem.tap.features.home.compose.StoriesScreen +import com.tangem.tap.features.home.compose.StoriesScreenV2 import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.store @@ -57,12 +58,22 @@ internal class DefaultHomeComponent @AssistedInject constructor( val activity = LocalContext.current.findActivity() BackHandler(onBack = activity::finish) SystemBarsIconsDisposable(darkIcons = false) - StoriesScreen( - homeState = homeState, - onScanButtonClick = model::onScanClick, - onShopButtonClick = model::onShopClick, - onSearchTokensClick = model::onSearchClick, - ) + if (homeState.value.isV2StoriesEnabled) { + StoriesScreenV2( + homeState = homeState, + onCreateNewWalletButtonClick = model::onCreateNewWalletScreen, + onAddExistingWalletButtonClick = model::onAddExistingWalletScreen, + onScanButtonClick = model::onScanClick, + ) + } else { + StoriesScreen( + homeState = homeState, + onScanButtonClick = model::onScanClick, + onShopButtonClick = model::onShopClick, + onSearchTokensClick = model::onSearchClick, + ) + } + ChangeRootBackgroundColorEffect(Color(color = 0xFF010101)) } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 132cbd1773..8936e89b41 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -71,6 +71,14 @@ internal class HomeModel @Inject constructor( .launchIn(modelScope) } + fun onCreateNewWalletScreen() { + // TODO implement navigation to create new wallet + } + + fun onAddExistingWalletScreen() { + // TODO implement navigation to add existing wallet + } + fun onScanClick() { analyticsEventHandler.send(IntroductionProcess.ButtonScanCard()) scanCard() diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt new file mode 100644 index 0000000000..a17f707453 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesScreenV2.kt @@ -0,0 +1,252 @@ +@file:Suppress("MagicNumber") + +package com.tangem.tap.features.home.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags +import com.tangem.tap.features.home.compose.content.* +import com.tangem.tap.features.home.compose.views.HomeButtonsV2 +import com.tangem.tap.features.home.compose.views.StoriesProgressBar +import com.tangem.tap.features.home.redux.HomeState +import com.tangem.tap.features.home.redux.Stories +import com.tangem.wallet.R +import kotlin.math.max + +@Composable +internal fun StoriesScreenV2( + homeState: MutableState, + onCreateNewWalletButtonClick: () -> Unit, + onAddExistingWalletButtonClick: () -> Unit, + onScanButtonClick: () -> Unit, +) { + val state = homeState.value + + var currentStory by remember { mutableStateOf(state.firstStory) } + val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) + + val goToPreviousStory = remember(currentStory, currentStoryIndex) { + { currentStory = state.stories[max(0, currentStoryIndex - 1)] } + } + val goToNextStory = remember(currentStory, currentStoryIndex) { + { + currentStory = if (currentStoryIndex < state.stories.lastIndex) { + state.stories[currentStoryIndex + 1] + } else { + state.firstStory + } + } + } + + // todo refactor [REDACTED_TASK_KEY] + StoriesScreenContentV2( + modifier = Modifier + .fillMaxSize() + .testTag(TestTags.STORIES_SCREEN), + config = StoriesScreenContentV2Config( + storiesSize = state.stories.lastIndex, + currentStoryIndex = currentStoryIndex, + currentStory = currentStory, + isScanInProgress = homeState.value.scanInProgress, + onGoToPreviousStory = goToPreviousStory, + onGoToNextStory = goToNextStory, + onCreateNewWalletButtonClick = onCreateNewWalletButtonClick, + onAddExistingWalletButtonClick = onAddExistingWalletButtonClick, + onScanButtonClick = onScanButtonClick, + ), + ) +} + +@Deprecated("Use StoriesContainer from core/ui") +@Suppress("LongMethod") +@Composable +private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifier: Modifier = Modifier) { + var isPressed by remember { mutableStateOf(value = false) } + + val isPaused = isPressed || config.isScanInProgress + val currentStoryDuration = config.currentStory.duration + + Box( + modifier = modifier.background(Color(0xFF010101)), + ) { + Row( + modifier = Modifier.fillMaxSize(), + ) { + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(Unit) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + isPressed = true + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < 200) config.onGoToPreviousStory() + isPressed = false + }, + ) + }, + ) + Box( + Modifier + .weight(1f) + .fillMaxHeight() + .pointerInput(Unit) { + detectTapGestures( + onPress = { + val pressStartTime = System.currentTimeMillis() + isPressed = true + this.tryAwaitRelease() + val pressEndTime = System.currentTimeMillis() + val totalPressTime = pressEndTime - pressStartTime + if (totalPressTime < 200) config.onGoToNextStory() + isPressed = false + }, + ) + }, + ) + } + + Column( + modifier = Modifier + .statusBarsPadding() + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + StoriesProgressBar( + steps = config.storiesSize, + currentStep = config.currentStoryIndex, + stepDuration = currentStoryDuration, + paused = isPaused, + onStepFinish = config.onGoToNextStory, + ) + Image( + painter = painterResource(id = R.drawable.ic_tangem_logo), + contentDescription = null, + contentScale = ContentScale.FillHeight, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + ) + .height(TangemTheme.dimens.size18) + .align(Alignment.Start), + ) + when (config.currentStory) { + Stories.TangemIntro -> FirstStoriesContent( + isPaused = isPaused, + duration = currentStoryDuration, + ) + Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet() + Stories.UltraSecureBackup -> StoriesUltraSecureBackup( + isPaused = isPaused, + stepDuration = currentStoryDuration, + ) + Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration) + Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration) + Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration) + } + } + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(bottom = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing16) + .align(Alignment.BottomCenter) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + HomeButtonsV2( + modifier = Modifier.fillMaxWidth(), + btnScanStateInProgress = config.isScanInProgress, + onScanButtonClick = config.onScanButtonClick, + onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick, + onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick, + ) + } + } +} + +private data class StoriesScreenContentV2Config( + val storiesSize: Int, + val currentStoryIndex: Int, + val currentStory: Stories, + val isScanInProgress: Boolean, + val onGoToPreviousStory: () -> Unit = {}, + val onGoToNextStory: () -> Unit = {}, + val onCreateNewWalletButtonClick: () -> Unit = {}, + val onAddExistingWalletButtonClick: () -> Unit = {}, + val onScanButtonClick: () -> Unit = {}, +) + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun StoriesScreenContentV2Preview( + @PreviewParameter(StoriesScreenContentV2ConfigProvider::class) config: StoriesScreenContentV2Config, +) { + TangemThemePreview { + StoriesScreenContentV2(config = config) + } +} + +private class StoriesScreenContentV2ConfigProvider : CollectionPreviewParameterProvider( + collection = listOf( + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 0, + currentStory = Stories.TangemIntro, + isScanInProgress = true, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 1, + currentStory = Stories.RevolutionaryWallet, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 2, + currentStory = Stories.UltraSecureBackup, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 3, + currentStory = Stories.Currencies, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 4, + currentStory = Stories.Web3, + isScanInProgress = false, + ), + StoriesScreenContentV2Config( + storiesSize = 6, + currentStoryIndex = 5, + currentStory = Stories.WalletForEveryone, + isScanInProgress = false, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt new file mode 100644 index 0000000000..6b620da6dc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtonsV2.kt @@ -0,0 +1,124 @@ +package com.tangem.tap.features.home.compose.views + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TestTags +import com.tangem.wallet.R + +@Composable +internal fun HomeButtonsV2( + btnScanStateInProgress: Boolean, + onScanButtonClick: () -> Unit, + onCreateNewWalletButtonClick: () -> Unit, + onAddExistingWalletButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + CreateNewWalletButton( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON), + onClick = onCreateNewWalletButtonClick, + ) + AddExistingWalletButton( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON), + onClick = onAddExistingWalletButtonClick, + ) + ScanCardButton( + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON), + showProgress = btnScanStateInProgress, + onClick = onScanButtonClick, + ) + } +} + +@Composable +private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + StoriesButton( + modifier = modifier, + text = stringResourceSafe(id = R.string.home_button_create_new_wallet), + useDarkerColors = false, + onClick = onClick, + ) +} + +@Composable +private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + StoriesButton( + modifier = modifier, + text = stringResourceSafe(id = R.string.home_button_add_existing_wallet), + useDarkerColors = true, + onClick = onClick, + ) +} + +@Composable +private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + StoriesButton( + modifier = modifier, + text = stringResourceSafe(id = R.string.home_button_scan), + useDarkerColors = true, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24), + onClick = onClick, + showProgress = showProgress, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) { + TangemThemePreview { + Box( + modifier = Modifier.background(Color.Black), + ) { + HomeButtonsV2( + btnScanStateInProgress = state.btnScanStateInProgress, + onCreateNewWalletButtonClick = {}, + onAddExistingWalletButtonClick = {}, + onScanButtonClick = {}, + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + ) + } + } +} + +private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + HomeButtonsV2State( + btnScanStateInProgress = false, + ), + HomeButtonsV2State( + btnScanStateInProgress = true, + ), + ), +) + +private data class HomeButtonsV2State( + val btnScanStateInProgress: Boolean, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index b1d45779ba..d36ea27e67 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -7,6 +7,7 @@ import org.rekotlin.StateType // todo refactor [REDACTED_TASK_KEY] data class HomeState( val scanInProgress: Boolean = false, + val isV2StoriesEnabled: Boolean = false, val stories: ImmutableList = getRestrictedStories().toImmutableList(), ) : StateType { diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt index ac570f6d81..67ff49b2b2 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt @@ -1,14 +1,22 @@ package com.tangem.common.test.data.quote import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.datasource.local.quote.converter.QuoteConverter import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.extensions.orZero -fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): Quote { - return QuoteConverter(source = source).convert(value = mapOf(rawCurrencyId to this).entries.first()) +fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = StatusSource.ACTUAL): QuoteStatus { + return QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID(rawCurrencyId), + value = QuoteStatus.Data( + source = source, + fiatRate = price.orZero(), + priceChange = priceChange24h.orZero().movePointLeft(2), + ), + ) } -fun Pair.toDomain(source: StatusSource = StatusSource.ACTUAL): Quote { - return QuoteConverter(source = source).convert(value = mapOf(this).entries.first()) +fun Pair.toDomain(source: StatusSource = StatusSource.ACTUAL): QuoteStatus { + return second.toDomain(rawCurrencyId = first, source = source) } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 46dc027b89..0ab98cc318 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -62,5 +62,17 @@ { "name": "USEDESK_ENABLED", "version": "undefined" + }, + { + "name": "SEND_VIA_SWAP_ENABLED", + "version": "undefined" + }, + { + "name": "SWAP_REDESIGN_ENABLED", + "version": "undefined" + }, + { + "name": "SEND_REDESIGN_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt index 89f2f3db60..84040fc6a5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/Trace.kt @@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Trace( - @Json(name = "exposed") val exposed: Exposed, + @Json(name = "exposed") val exposed: Exposed?, @Json(name = "asset") val asset: NftAsset, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt index 08e86a0697..2e27aab60b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/blockaid/models/response/ValidationResponse.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class ValidationResponse( @Json(name = "status") val status: String, @Json(name = "result_type") val resultType: String, + @Json(name = "description") val description: String?, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt index 172c39e727..69c2f7a7d5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class GenerateNoneByCardWalletRequest( @Json(name = "auth_type") val authType: String = "card_wallet", @Json(name = "card_wallet_address") val cardWalletAddress: String, + @Json(name = "card_id") val cardId: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt index c80e97b0c9..df5d0c3935 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt @@ -8,4 +8,5 @@ data class GetAccessTokenByCardWalletRequest( @Json(name = "auth_type") val authType: String = "card_wallet", @Json(name = "session_id") val sessionId: String, @Json(name = "signature") val signature: String, + @Json(name = "salt") val salt: String, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt deleted file mode 100644 index 555eb268f9..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.datasource.local.quote.converter - -import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.orZero - -/** - * Converter from [QuotesResponse.Quote] to [Quote.Value] - * - * @property source status source - * -[REDACTED_AUTHOR] - */ -class QuoteConverter( - private val source: StatusSource, -) : - Converter, Quote.Value> { - - /** - * Secondary constructor - * - * @param isCached flag that determines whether the quote is a cache - */ - constructor(isCached: Boolean) : this( - source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, - ) - - override fun convert(value: Map.Entry): Quote.Value { - val (currencyId, quote) = value - - return Quote.Value( - rawCurrencyId = CryptoCurrency.RawID(currencyId), - fiatRate = quote.price.orZero(), - priceChange = quote.priceChange24h.orZero().movePointLeft(2), - source = source, - ) - } -} \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 11d55684c1..4b28892f04 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -383,6 +383,8 @@ Unlimited Order Tangem Scan Tangem + Create New Wallet + Add Existing Wallet to %s On %s network This information was generated with AI.\nTap here, if you find any errors. @@ -856,8 +858,12 @@ Recipient Not a valid address Ensure the receiving wallet address is on the %s network to avoid losing your tokens + send to %s + Ensure you %s network address, as errors may result in lost transfers Send to A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds + Memo / Destination Tag is a code separating transactions to a shared recipient in a crypto network. + Caution: Missing a memo may lead to fund loss. My wallets A way of measuring Bitcoin transaction fees. It indicates the number of the smallest Bitcoin unit (Satoshi) for each virtual byte in a transaction. The higher the number, the faster the transaction will be processed by miners. Satoshi / vByte @@ -885,7 +891,7 @@ Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name - Join staking with this token + Put your token to work The amount to stake must be at least %s Staking amount will be rounded to %1$s TRX due to network rules. Unstaking amount will be rounded to %1$s TRX due to network rules. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt index eed177c971..e5293e07ab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabel.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.audits import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -11,6 +12,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -30,41 +32,59 @@ import com.tangem.core.ui.res.TangemThemePreview fun AuditLabel(state: AuditLabelUM, modifier: Modifier = Modifier) { AuditLabelInternal( textReference = state.text, - textColor = getColorByType(state.type), - backgroundColor = getColorByType(state.type).copy(alpha = 0.1f), + type = state.type, modifier = modifier, ) } @Composable -private fun AuditLabelInternal( - textReference: TextReference, - textColor: Color, - backgroundColor: Color, - modifier: Modifier = Modifier, -) { +private fun AuditLabelInternal(textReference: TextReference, type: AuditLabelUM.Type, modifier: Modifier = Modifier) { Box( modifier = modifier.background( - color = backgroundColor, + color = getBackgroundColorByType(type), shape = TangemTheme.shapes.roundedCornersSmall2, ), ) { Text( text = textReference.resolveReference(), style = TangemTheme.typography.caption1, - color = textColor, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing4), + color = getTextColorByType(type), + modifier = Modifier.padding(getPaddingByType(type)), ) } } @ReadOnlyComposable @Composable -private fun getColorByType(type: AuditLabelUM.Type): Color { +private fun getTextColorByType(type: AuditLabelUM.Type): Color { return when (type) { AuditLabelUM.Type.Prohibition -> TangemTheme.colors.text.warning AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent + AuditLabelUM.Type.Info -> TangemTheme.colors.text.primary2 + } +} + +@ReadOnlyComposable +@Composable +private fun getBackgroundColorByType(type: AuditLabelUM.Type): Color { + return when (type) { + AuditLabelUM.Type.Prohibition -> TangemTheme.colors.text.warning.copy(alpha = 0.1f) + AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention.copy(alpha = 0.1f) + AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent.copy(alpha = 0.1f) + AuditLabelUM.Type.Info -> TangemTheme.colors.text.accent + } +} + +@ReadOnlyComposable +@Composable +private fun getPaddingByType(type: AuditLabelUM.Type): PaddingValues { + return when (type) { + AuditLabelUM.Type.Prohibition, + AuditLabelUM.Type.Warning, + AuditLabelUM.Type.Permit, + -> PaddingValues(horizontal = 4.dp) + AuditLabelUM.Type.Info -> PaddingValues(horizontal = 6.dp, vertical = 1.dp) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt index 25059b9594..4b1ed19772 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/audits/AuditLabelUM.kt @@ -22,5 +22,8 @@ data class AuditLabelUM(val text: TextReference, val type: Type) { /** Blue. Like, "trusted" */ Permit, + + /** Blue with white text. Like, "best rate" */ + Info, } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt new file mode 100644 index 0000000000..282f4a0618 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/badge/Badge.kt @@ -0,0 +1,74 @@ +package com.tangem.core.ui.components.badge + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Badge component + * + * @param iconRes icon drawable resource + * @param text text reference + * @param modifier composable modifier + * + * @see Figma + */ +@Composable +fun Badge(@DrawableRes iconRes: Int, text: TextReference, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier + .border(1.dp, TangemTheme.colors.stroke.primary, RoundedCornerShape(4.dp)) + .padding(horizontal = 4.dp), + ) { + Icon( + painter = rememberVectorPainter( + ImageVector.vectorResource(iconRes), + ), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.size(12.dp), + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Badge_Preview() { + TangemThemePreview { + Badge( + iconRes = R.drawable.ic_star_mini_24, + text = stringReference("4.9"), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt new file mode 100644 index 0000000000..ee6aa0bcdb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/badge/entity/BadgeUM.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.components.badge.entity + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +/** + * Badge component UI model + * + * @param iconRes icon drawable resource + * @param text text reference + */ +data class BadgeUM( + val text: TextReference, + @DrawableRes val iconRes: Int, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt index 961bca8869..46530d78b1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt @@ -16,6 +16,7 @@ enum class TangemButtonSize { RoundedAction, WideAction, TwoLines, + Small, } @Composable @@ -28,6 +29,7 @@ internal fun TangemButtonSize.toHeightDp(): Dp = when (this) { TangemButtonSize.RoundedAction, -> TangemTheme.dimens.size36 TangemButtonSize.WideAction -> TangemTheme.dimens.size40 + TangemButtonSize.Small -> 24.dp } @Composable @@ -40,6 +42,7 @@ internal fun TangemButtonSize.toShape(): Shape = when (this) { TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium TangemButtonSize.TwoLines -> TangemTheme.shapes.roundedCornersXMedium TangemButtonSize.RoundedAction -> TangemTheme.shapes.roundedCornersLarge + TangemButtonSize.Small -> TangemTheme.shapes.roundedCornersXMedium } @Composable @@ -48,6 +51,7 @@ internal fun TangemButtonSize.toIconPadding(): Dp = when (this) { TangemButtonSize.Default, TangemButtonSize.WideAction, TangemButtonSize.TwoLines, + TangemButtonSize.Small, -> TangemTheme.dimens.spacing4 TangemButtonSize.Text -> TangemTheme.dimens.spacing8 TangemButtonSize.Selector -> 0.dp @@ -100,11 +104,18 @@ internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition): start = horizontalPadding.first, end = horizontalPadding.second, ) + TangemButtonSize.Small -> PaddingValues( + top = TangemTheme.dimens.spacing4, + bottom = TangemTheme.dimens.spacing4, + start = horizontalPadding.first, + end = horizontalPadding.second, + ) } } @Composable @ReadOnlyComposable +@Suppress("CyclomaticComplexMethod") internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconPosition): Pair { return when (this) { TangemButtonSize.Default, @@ -124,5 +135,10 @@ internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconP is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing24 is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing24 to TangemTheme.dimens.spacing16 } + TangemButtonSize.Small -> when (icon) { + is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing12 to TangemTheme.dimens.spacing12 + is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing8 to TangemTheme.dimens.spacing12 + is TangemButtonIconPosition.End -> TangemTheme.dimens.spacing12 to TangemTheme.dimens.spacing8 + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt index bebd090e66..028be341c1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.res.TangemTheme /** @@ -32,7 +32,7 @@ fun FooterContainer( AnimatedVisibility(visible = footer != null) { val footerWrapped = remember(this) { requireNotNull(footer) } Text( - text = footerWrapped.resolveReference(), + text = footerWrapped.resolveAnnotatedReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt new file mode 100644 index 0000000000..bc6d664fb1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -0,0 +1,159 @@ +package com.tangem.core.ui.components.fields + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.utils.StringsSigns.PASSWORD_VISUAL_CHAR +import kotlin.random.Random + +@Composable +fun PinTextField( + value: String, + length: Int, + isPasswordVisual: Boolean, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val focusRequester = remember { FocusRequester() } + val textFieldValue = remember(value) { + TextFieldValue(value, selection = TextRange(value.length)) + } + + BasicTextField( + value = textFieldValue, + onValueChange = { + if (it.text.length <= length) { + onValueChange(it.text) + } + }, + modifier = modifier + .clickable { + focusRequester.requestFocus() + } + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + singleLine = true, + textStyle = TangemTheme.typography.body1, + maxLines = 1, + cursorBrush = SolidColor(TangemTheme.colors.text.primary1), + decorationBox = { innerTextField -> + Box(modifier = Modifier.width(0.dp)) { + innerTextField() + } + CellDecoration( + length = length, + isPasswordVisual = isPasswordVisual, + value = value, + ) + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun CellDecoration( + length: Int, + value: String, + modifier: Modifier = Modifier, + isPasswordVisual: Boolean = false, +) { + val textMeasurer = rememberTextMeasurer() + val width = textMeasurer.measure("0") + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + repeat(length) { + val char = if (it < value.length) { + if (isPasswordVisual) PASSWORD_VISUAL_CHAR.toString() else value[it].toString() + } else { + "" + } + + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = char, + transitionSpec = { + ( + fadeIn(animationSpec = tween(220, delayMillis = 90)) + + slideInVertically(animationSpec = tween(330, delayMillis = 0)) + ) + .togetherWith( + fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)), + ) + }, + ) { text -> + Text( + modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), + text = text, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + lineHeight = 48.sp, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + TangemThemePreview { + var text by remember { mutableStateOf("123") } + + Column { + PinTextField( + value = text, + onValueChange = { text = it }, + isPasswordVisual = false, + length = 6, + ) + + Row { + Button(onClick = { text += Random.nextInt(0, 10).toString() }) { + Text("Add Character") + } + Button(onClick = { text = text.dropLast(1) }) { + Text("Delete") + } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt index 5a92d21899..63903ea854 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowEnterInfoAmount.kt @@ -141,7 +141,7 @@ fun InputRowEnterInfoAmountV2( ) Icon( modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_union_16), + painter = painterResource(R.drawable.ic_token_info_24), tint = TangemTheme.colors.icon.informative, contentDescription = null, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt index 8d5363d3cc..8991b68840 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowRecipient.kt @@ -2,6 +2,10 @@ package com.tangem.core.ui.components.inputrow import android.content.res.Configuration import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.small.TangemIconButton import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.components.icons.identicon.IdentIcon import com.tangem.core.ui.components.inputrow.inner.CrossIcon @@ -25,24 +30,29 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION import kotlinx.coroutines.delay /** - * [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4) + * * [Design Reference](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4) * - * @param title title reference - * @param value recipient address - * @param placeholder placeholder - * @param onValueChange callback for value change - * @param onPasteClick callback for paste - * @param modifier composable modifier - * @param singleLine is single line text - * @param error error text - * @param isError is error flag - * @param showDivider show divider + * @param title The title text reference to display above the input field + * @param value The current recipient address value to display in the input field + * @param placeholder The placeholder text reference to show when the input is empty + * @param onValueChange Callback invoked when the input value changes, receives the new string value + * @param onPasteClick Callback invoked when the paste button is clicked, receives the pasted string + * @param onQrCodeClick Callback invoked when the QR code scan button is clicked + * @param modifier Optional modifier to apply to the composable + * @param singleLine Whether the text field should be constrained to a single line (default: false) + * @param error Optional error text reference to display when validation fails + * @param isError Whether the input is in an error state, affects styling and title color + * @param showDivider Whether to show a divider below the component (default: false) + * @param isLoading Whether to show a loading indicator instead of the identicon (default: false) + * @param isValuePasted Whether the current value was pasted, affects visual feedback (default: false) * - * @see InputRowRecipientDefault for readonly version + * @see InputRowRecipientDefault for a read-only version of this component */ +@Suppress("LongParameterList", "LongMethod") @Composable fun InputRowRecipient( title: TextReference, @@ -50,6 +60,8 @@ fun InputRowRecipient( placeholder: TextReference, onValueChange: (String) -> Unit, onPasteClick: (String) -> Unit, + onQrCodeClick: () -> Unit, + isRedesignEnabled: Boolean, modifier: Modifier = Modifier, singleLine: Boolean = false, error: TextReference? = null, @@ -109,18 +121,55 @@ fun InputRowRecipient( .padding(start = TangemTheme.dimens.spacing8), ) } - PasteButton( - isPasteButtonVisible = value.isBlank(), - onClick = onPasteClick, + Row( modifier = Modifier - .align(CenterEnd) - .padding(start = TangemTheme.dimens.spacing8), - ) + .align(CenterEnd), + ) { + if (isRedesignEnabled) { + QrButton( + visible = !singleLine, + onQrCodeClick = onQrCodeClick, + ) + } + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + backgroundColorEnabled = if (isRedesignEnabled) { + TangemTheme.colors.button.secondary + } else { + TangemTheme.colors.button.primary + }, + textColor = if (isRedesignEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.primary2 + }, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8), + ) + } } } } } +@Composable +private fun QrButton(visible: Boolean, onQrCodeClick: () -> Unit, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = visible, + enter = fadeIn(), + exit = fadeOut(animationSpec = tween(DEFAULT_ANIMATION_DURATION)), + modifier = modifier, + ) { + TangemIconButton( + iconRes = R.drawable.ic_scan_16, + onClick = onQrCodeClick, + background = TangemTheme.colors.button.secondary, + iconTint = TangemTheme.colors.text.primary1, + ) + } +} + @Composable private fun RowScope.InputIcon(isLoading: Boolean, value: String) { var isLoadingProxy by remember { mutableStateOf(isLoading) } @@ -172,7 +221,9 @@ private fun InputRowRecipientPreview( showDivider = true, onValueChange = {}, onPasteClick = {}, + onQrCodeClick = {}, modifier = Modifier.background(TangemTheme.colors.background.primary), + isRedesignEnabled = false, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index ef23a4babc..d816b39eb6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalHapticFeedback @@ -34,14 +35,21 @@ import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION * @param modifier composable modifier */ @Composable -fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { +fun PasteButton( + isPasteButtonVisible: Boolean, + onClick: (String) -> Unit, + modifier: Modifier = Modifier, + backgroundColorEnabled: Color = TangemTheme.colors.button.primary, + backgroundColorDisabled: Color = TangemTheme.colors.button.secondary, + textColor: Color = TangemTheme.colors.text.primary2, +) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty() val color = if (isPasteEnabled) { - TangemTheme.colors.button.primary + backgroundColorEnabled } else { - TangemTheme.colors.button.secondary + backgroundColorDisabled } AnimatedVisibility( visible = isPasteButtonVisible, @@ -53,7 +61,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi Text( text = stringResourceSafe(R.string.common_paste), style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary2, + color = textColor, modifier = Modifier .background( color = color, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt new file mode 100644 index 0000000000..1083b56912 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderChooseCrypto.kt @@ -0,0 +1,323 @@ +package com.tangem.core.ui.components.provider + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +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 androidx.compose.ui.util.fastForEach +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.audits.AuditLabel +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.badge.Badge +import com.tangem.core.ui.components.badge.entity.BadgeUM +import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +/** + * Provider Row - Choose Crypto component + * + * @param providerChooseUM component ui model + * @param onClick click listener + * @param modifier composable modifier + * + * @see Figma + */ +@Composable +@Suppress("DestructuringDeclarationWithTooManyEntries") +fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + ConstraintLayout( + modifier = modifier + .background(TangemTheme.colors.background.action) + .clip(RoundedCornerShape(14.dp)) + .selectedBorder(isSelected = providerChooseUM.isSelected) + .clickable(enabled = true, onClick = onClick) + .fillMaxWidth() + .padding(all = 12.dp), + ) { + val (iconRef, titleRef, badgeRef, infoRef, labelRef) = createRefs() + val titleEndRef = createStartBarrier(infoRef, labelRef) + + IconContent( + iconUrl = providerChooseUM.iconUrl, + modifier = Modifier + .constrainAs(iconRef) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + ) + TitleContent( + providerChooseUM = providerChooseUM, + modifier = Modifier.constrainAs(titleRef) { + start.linkTo(iconRef.end, 12.dp) + end.linkTo(titleEndRef, 4.dp) + top.linkTo(parent.top) + bottom.linkTo(badgeRef.top) + width = Dimension.fillToConstraints + }, + ) + BadgeContent( + providerChooseUM = providerChooseUM, + modifier = Modifier.constrainAs(badgeRef) { + start.linkTo(iconRef.end, 12.dp) + end.linkTo(labelRef.start, 4.dp) + top.linkTo(titleRef.bottom, 4.dp) + bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints + visibility = if (providerChooseUM.badgeList.isEmpty()) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + InfoContent( + text = providerChooseUM.infoText, + modifier = Modifier.constrainAs(infoRef) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(labelRef.top, 4.dp) + }, + ) + LabelContent( + labelUM = providerChooseUM.labelUM, + modifier = Modifier.constrainAs(labelRef) { + end.linkTo(parent.end) + top.linkTo(infoRef.bottom) + bottom.linkTo(parent.bottom) + visibility = if (providerChooseUM.labelUM == null) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + } +} + +@Composable +private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) { + SubcomposeAsyncImage( + modifier = modifier + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) + .background(TangemColorPalette.Light1), + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + contentDescription = null, + ) +} + +@Composable +private fun TitleContent(providerChooseUM: ProviderChooseUM, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + Text( + text = providerChooseUM.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + ) + Text( + text = providerChooseUM.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + ) + } +} + +@Composable +private fun InfoContent(text: TextReference, modifier: Modifier = Modifier) { + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} + +@Composable +private fun BadgeContent(providerChooseUM: ProviderChooseUM, modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + providerChooseUM.badgeList.fastForEach { badge -> + Badge( + iconRes = badge.iconRes, + text = badge.text, + ) + } + } +} + +@Composable +private fun LabelContent(labelUM: ProviderChooseUM.LabelUM?, modifier: Modifier = Modifier) { + Box( + modifier = modifier, + ) { + when (labelUM) { + is ProviderChooseUM.LabelUM.Info -> AuditLabel(labelUM.auditLabelUM) + is ProviderChooseUM.LabelUM.Text -> Text( + text = labelUM.text.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + ) + null -> Unit + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun ProviderChooseCrypto_Preview( + @PreviewParameter(ProviderChooseCryptoPreviewProvider::class) params: ProviderChooseUM, +) { + TangemThemePreview { + ProviderChooseCrypto( + providerChooseUM = params, + onClick = {}, + ) + } +} + +private class ProviderChooseCryptoPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ProviderChooseUM( + title = stringReference("ChangeHero"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 800,00 POL"), + iconUrl = "", + badgeList = persistentListOf( + BadgeUM( + text = stringReference("4.9"), + iconRes = R.drawable.ic_star_outline_24, + ), + BadgeUM( + text = stringReference("5 mins"), + iconRes = R.drawable.ic_speed_24, + ), + BadgeUM( + text = stringReference("$3.45"), + iconRes = R.drawable.ic_gas_24, + ), + ), + isSelected = true, + labelUM = ProviderChooseUM.LabelUM.Info( + auditLabelUM = AuditLabelUM( + text = resourceReference(R.string.express_provider_best_rate), + type = AuditLabelUM.Type.Info, + ), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 799,12 POL"), + iconUrl = "", + badgeList = persistentListOf( + BadgeUM( + text = stringReference("4.9"), + iconRes = R.drawable.ic_star_outline_24, + ), + BadgeUM( + text = stringReference("5 mins"), + iconRes = R.drawable.ic_speed_24, + ), + BadgeUM( + text = stringReference("$3.45"), + iconRes = R.drawable.ic_gas_24, + ), + ), + isSelected = false, + labelUM = ProviderChooseUM.LabelUM.Text( + text = stringReference("–0.2%"), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 799,12 POL"), + iconUrl = "", + badgeList = persistentListOf(), + isSelected = false, + labelUM = ProviderChooseUM.LabelUM.Text( + text = stringReference("–0.2%"), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 POL"), + iconUrl = "", + badgeList = persistentListOf(), + isSelected = false, + labelUM = ProviderChooseUM.LabelUM.Text( + text = stringReference("–0.2%"), + ), + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 POL"), + iconUrl = "", + badgeList = persistentListOf(), + isSelected = false, + labelUM = null, + ), + ProviderChooseUM( + title = stringReference("Changelly"), + subtitle = stringReference("CEX"), + infoText = stringReference("1 POL"), + iconUrl = "", + badgeList = persistentListOf( + BadgeUM( + text = stringReference("4.9"), + iconRes = R.drawable.ic_star_outline_24, + ), + BadgeUM( + text = stringReference("5 mins"), + iconRes = R.drawable.ic_speed_24, + ), + BadgeUM( + text = stringReference("$3.45"), + iconRes = R.drawable.ic_gas_24, + ), + ), + isSelected = false, + labelUM = null, + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt new file mode 100644 index 0000000000..b3a9392c87 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/entity/ProviderChooseUM.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.provider.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.badge.entity.BadgeUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +/** + * Provider Row - Choose Crypto component UI Model + * + * @property title title text + * @property subtitle subtitle text + * @property infoText info text + * @property iconUrl icon url + * @property isSelected indicated whether component is selected (border outline) + * @property badgeList list of badge components + * @property labelUM label component (whether AuditLabel or text) + */ +data class ProviderChooseUM( + val title: TextReference, + val subtitle: TextReference, + val infoText: TextReference, + val iconUrl: String, + val isSelected: Boolean, + val badgeList: ImmutableList, + val labelUM: LabelUM?, +) { + @Immutable + sealed class LabelUM { + data class Info( + val auditLabelUM: AuditLabelUM, + ) : LabelUM() + + data class Text( + val text: TextReference, + ) : LabelUM() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt index c510835242..f1e49e308f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -60,4 +60,40 @@ fun AnnotatedString.Builder.appendSpace() = append(" ") fun AnnotatedString.Builder.appendColored(text: String, color: Color) = withStyle(SpanStyle(color = color)) { append(text) +} + +/** + * Appends text from a template string to the AnnotatedString.Builder, replacing a placeholder (default "%s") + * with custom styled content provided by a lambda. The lambda allows you to insert styled or complex content + * (e.g., colored, bold, or annotated text) at the placeholder position. + * + * If the placeholder is not found in the template, the function does nothing. + * + * Example usage: + * builder.appendWithStyledPlaceholder( + * template = "Ensure you %s network address, as errors may result in lost transfers" + * ) { + * withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)) { + * append("Ethereum") + * } + * } + * + * @param template The template string containing the placeholder to be replaced. + * @param placeholder The placeholder string to be replaced by styled content. Default is "%s". + * @param styledContent Lambda to build the styled content to insert at the placeholder position. + */ +fun AnnotatedString.Builder.appendWithStyledPlaceholder( + template: String, + placeholder: String = "%s", + styledContent: AnnotatedString.Builder.() -> Unit, +) { + val index = template.indexOf(placeholder) + if (index < 0) return + // Append before placeholder + if (index > 0) append(template.substring(0, index)) + // Append styled content + styledContent() + // Append after placeholder + val afterIndex = index + placeholder.length + if (afterIndex < template.length) append(template.substring(afterIndex)) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt index a014910b24..fc74ccf197 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/ModifierExt.kt @@ -1,12 +1,21 @@ package com.tangem.core.ui.extensions import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.MultipleClickPreventer /** @@ -38,4 +47,51 @@ fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier): } else { this } -} \ No newline at end of file +} + +/** + * Conditionally applies a modifier based on a boolean condition. + */ +@Composable +fun Modifier.conditionalCompose( + condition: Boolean, + modifier: @Composable Modifier.() -> Modifier = { Modifier }, + otherModifier: @Composable Modifier.() -> Modifier = { this }, +): Modifier { + return if (condition) { + then(modifier(Modifier)) + } else { + otherModifier() + } +} + +/** + * Border with accent outline when selected + */ +@Composable +fun Modifier.selectedBorder( + isSelected: Boolean, + width: Dp = 2.5.dp, + color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + radius: Dp = 16.dp, +) = conditionalCompose( + condition = isSelected, + modifier = { + border( + width = width, + color = color, + shape = RoundedCornerShape(radius), + ) + .padding(2.5.dp) + .border( + width = 1.dp, + color = TangemTheme.colors.text.accent, + shape = RoundedCornerShape(radius - 2.dp), + ) + .clip(RoundedCornerShape(radius - 2.dp)) + }, + otherModifier = { + padding(2.dp) + .clip(RoundedCornerShape(radius - 2.dp)) + }, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt index d7de6de18e..feca4f686c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TestTags.kt @@ -4,6 +4,8 @@ object TestTags { const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER" const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON" const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON" + const val STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON = "STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON" + const val STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON = "STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON" const val MAIN_SCREEN = "MAIN_SCREEN_CONTAINER" const val MAIN_SCREEN_MORE_BUTTON = "MAIN_SCREEN_MORE_BUTTON" diff --git a/core/ui/src/main/res/drawable/ic_fee_new_24.xml b/core/ui/src/main/res/drawable/ic_fee_new_24.xml index a644c488cf..435ad051d4 100644 --- a/core/ui/src/main/res/drawable/ic_fee_new_24.xml +++ b/core/ui/src/main/res/drawable/ic_fee_new_24.xml @@ -1,21 +1,27 @@ + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportWidth="24" + android:viewportHeight="24"> + + android:fillColor="#0099FF" + android:fillType="evenOdd" + android:pathData="M12,3.547C11.407,3.547 10.866,3.701 10.283,3.956C9.721,4.201 9.073,4.562 8.271,5.008L5.073,6.788C4.611,7.045 4.214,7.374 3.939,7.825C3.662,8.278 3.547,8.793 3.547,9.357L3.547,9.411C3.546,9.602 3.546,9.825 3.599,10.027C3.733,10.53 4.055,10.86 4.448,11.035C4.663,11.13 4.887,11.175 5.088,11.194V16.66C4.167,17.188 3.547,18.181 3.547,19.318C3.547,19.945 4.055,20.453 4.682,20.453H11.23C11.644,20.453 11.98,20.117 11.98,19.703C11.98,19.289 11.644,18.953 11.23,18.953H5.09C5.254,18.267 5.872,17.757 6.608,17.757H11.23C11.644,17.757 11.98,17.421 11.98,17.007C11.98,16.593 11.644,16.257 11.23,16.257H9.669V11.21H18.572C18.834,11.21 19.205,11.19 19.553,11.035C19.946,10.86 20.268,10.53 20.401,10.027C20.455,9.825 20.454,9.602 20.454,9.411L20.454,9.357C20.454,8.793 20.339,8.278 20.062,7.825C19.787,7.374 19.39,7.045 18.928,6.788L15.73,5.008C14.928,4.562 14.28,4.201 13.718,3.956C13.134,3.701 12.594,3.547 12,3.547ZM8.968,6.337C9.81,5.868 10.395,5.543 10.883,5.33C11.356,5.124 11.685,5.047 12,5.047C12.316,5.047 12.645,5.124 13.118,5.33C13.606,5.543 14.191,5.868 15.033,6.337L18.198,8.099C18.506,8.269 18.679,8.437 18.782,8.606C18.883,8.772 18.954,9 18.954,9.357C18.954,9.489 18.954,9.563 18.951,9.617C18.95,9.635 18.949,9.646 18.949,9.652C18.948,9.657 18.946,9.66 18.945,9.663L18.942,9.664C18.923,9.673 18.884,9.686 18.817,9.695C18.75,9.705 18.67,9.71 18.572,9.71H5.429C5.331,9.71 5.251,9.705 5.184,9.695C5.116,9.686 5.078,9.673 5.059,9.664L5.056,9.663C5.055,9.66 5.053,9.657 5.052,9.652C5.051,9.646 5.051,9.635 5.05,9.617C5.047,9.563 5.047,9.489 5.047,9.357C5.047,9 5.118,8.772 5.219,8.606C5.322,8.437 5.495,8.269 5.802,8.099L8.968,6.337ZM6.608,16.257L6.588,16.257V11.21H8.169V16.257H6.608Z" /> + + android:fillColor="#0099FF" + android:pathData="M13.396,18.788C13.103,19.081 13.103,19.556 13.396,19.849C13.689,20.142 14.164,20.142 14.457,19.849L19.849,14.456C20.142,14.163 20.142,13.689 19.849,13.396C19.556,13.103 19.081,13.103 18.788,13.396L13.396,18.788Z" /> + + android:fillColor="#0099FF" + android:fillType="evenOdd" + android:pathData="M13.176,14.503C13.176,13.77 13.77,13.176 14.504,13.176C15.237,13.176 15.832,13.77 15.832,14.503C15.832,15.237 15.237,15.831 14.504,15.831C13.77,15.831 13.176,15.237 13.176,14.503ZM14.504,14.676C14.599,14.676 14.676,14.599 14.676,14.503C14.676,14.408 14.599,14.331 14.504,14.331C14.409,14.331 14.332,14.408 14.332,14.503C14.332,14.599 14.409,14.676 14.504,14.676Z" /> + + android:fillColor="#0099FF" + android:fillType="evenOdd" + android:pathData="M18.741,17.412C18.008,17.412 17.413,18.007 17.413,18.74C17.413,19.473 18.008,20.068 18.741,20.068C19.474,20.068 20.069,19.473 20.069,18.74C20.069,18.007 19.474,17.412 18.741,17.412ZM18.913,18.74C18.913,18.835 18.836,18.912 18.741,18.912C18.646,18.912 18.569,18.835 18.569,18.74C18.569,18.645 18.646,18.568 18.741,18.568C18.836,18.568 18.913,18.645 18.913,18.74Z" /> + diff --git a/core/ui/src/main/res/drawable/ic_gas_24.xml b/core/ui/src/main/res/drawable/ic_gas_24.xml new file mode 100644 index 0000000000..278945f9fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_gas_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_scan_16.xml b/core/ui/src/main/res/drawable/ic_scan_16.xml new file mode 100644 index 0000000000..16a9aa2ffb --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_scan_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_speed_24.xml b/core/ui/src/main/res/drawable/ic_speed_24.xml new file mode 100644 index 0000000000..8e4390a831 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_speed_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_star_outline_24.xml b/core/ui/src/main/res/drawable/ic_star_outline_24.xml new file mode 100644 index 0000000000..116e8fdc03 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_star_outline_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_token_info_24.xml b/core/ui/src/main/res/drawable/ic_token_info_24.xml new file mode 100644 index 0000000000..87c467d679 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_token_info_24.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_union_16.xml b/core/ui/src/main/res/drawable/ic_union_16.xml deleted file mode 100644 index 3281f5e0ff..0000000000 --- a/core/ui/src/main/res/drawable/ic_union_16.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 7d53361c16..36945de43e 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -13,4 +13,5 @@ object StringsSigns { const val NON_BREAKING_SPACE = '\u00A0' const val PERCENT = "%" const val THREE_STARS = "\u2217\u2217\u2217" + const val PASSWORD_VISUAL_CHAR = '\u2022' } \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index 64daa2aaf6..e245687538 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -39,6 +39,7 @@ internal object BlockAidMapper { } else { mapSimulationSuccessResult(from.simulation.accountSummary) }, + description = from.validation.description, ) } diff --git a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt index 6d83fc64db..1980d63605 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -46,7 +46,7 @@ class BlockAidMapperTest { spenders = mapOf("spender" to spenderDetails), ) val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse( @@ -79,7 +79,7 @@ class BlockAidMapperTest { outTransfer = listOf(Transfer(value = "1.5", rawValue = "0x2")), ) val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse( @@ -105,7 +105,7 @@ class BlockAidMapperTest { @Test fun `when response error validation rhen returns failed to validate`() { val response = TransactionScanResponse( - validation = ValidationResponse(status = "Error", resultType = "Benign"), + validation = ValidationResponse(status = "Error", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), @@ -120,7 +120,7 @@ class BlockAidMapperTest { @Test fun `when response not benign then returns validation unsafe`() { val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Phishing"), + validation = ValidationResponse(status = "Success", resultType = "Phishing", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), @@ -134,7 +134,7 @@ class BlockAidMapperTest { @Test fun `when response simulation not success then returns simulation failed ro simulate`() { val response = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Error", accountSummary = AccountSummaryResponse(emptyList(), emptyList(), null), @@ -148,7 +148,7 @@ class BlockAidMapperTest { @Test fun `when response simulation is empty then returns failed to simulate`() { val txResponse = TransactionScanResponse( - validation = ValidationResponse(status = "Success", resultType = "Benign"), + validation = ValidationResponse(status = "Success", resultType = "Benign", description = ""), simulation = SimulationResponse( status = "Success", accountSummary = AccountSummaryResponse( diff --git a/features/fee-selector/api/.gitignore b/data/express/.gitignore similarity index 100% rename from features/fee-selector/api/.gitignore rename to data/express/.gitignore diff --git a/data/express/build.gradle.kts b/data/express/build.gradle.kts new file mode 100644 index 0000000000..f9a4304009 --- /dev/null +++ b/data/express/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.data.express" +} + +dependencies { + + /** Core */ + implementation(projects.core.datasource) + implementation(projects.core.utils) + + /** Data */ + implementation(projects.data.common) + + /** Domain */ + implementation(projects.domain.express.models) + implementation(projects.domain.express) + implementation(projects.domain.wallets.models) + + /** Other */ + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt new file mode 100644 index 0000000000..4963d92a82 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -0,0 +1,36 @@ +package com.tangem.data.express + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.express.converter.ExpressProviderConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.wallets.models.UserWallet +import timber.log.Timber + +internal class DefaultExpressRepository( + private val tangemExpressApi: TangemExpressApi, + private val appPreferencesStore: AppPreferencesStore, +) : ExpressRepository { + + override suspend fun getProviders(userWallet: UserWallet): List { + return safeApiCall( + call = { + tangemExpressApi.getProviders( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).getOrThrow().map(ExpressProviderConverter()::convert) + }, + onError = { + Timber.w(it, "Unable to fetch express providers") + throw it + }, + ) + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt b/data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt new file mode 100644 index 0000000000..b191da795b --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/converter/ExpressErrorConverter.kt @@ -0,0 +1,91 @@ +package com.tangem.data.express.converter + +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.domain.express.models.ExpressError +import com.tangem.utils.converter.Converter +import com.tangem.datasource.api.express.models.response.ExpressError as ExpressErrorDTO + +class ExpressErrorConverter( + private val jsonAdapter: JsonAdapter, +) : Converter { + + @Suppress("MagicNumber", "CyclomaticComplexMethod") + override fun convert(value: String): ExpressError { + try { + val error: ExpressErrorDTO = jsonAdapter.fromJson(value)?.error ?: return ExpressError.UnknownError + + return when (error.code) { + 2010 -> ExpressError.BadRequest(code = error.code) + 2020 -> ExpressError.Forbidden(code = error.code) + + 2200 -> ExpressError.InternalError(code = error.code) + 2210 -> ExpressError.ProviderNotFoundError(code = error.code) + 2220 -> ExpressError.ProviderNotActiveError(code = error.code) + 2230 -> ExpressError.ProviderNotAvailableError(code = error.code) + 2231 -> ExpressError.ProviderInternalError(code = error.code) + 2240 -> ExpressError.ExchangeNotPossibleError(code = error.code) + 2250 -> error.toTooSmallAmountError() + 2251 -> error.toTooBigAmountError() + 2260 -> error.toNotEnoughAllowanceError() + 2270 -> ExpressError.NotEnoughBalanceError(code = error.code) + 2280 -> ExpressError.InvalidAddressError(code = error.code) + 2290 -> error.toInvalidFromDecimalsError() + + 2320 -> error.toProviderDifferentAmountError() + else -> ExpressError.DataError(error.code, error.description) + } + } catch (e: Exception) { + return ExpressError.UnknownError + } + } + + private fun ExpressErrorDTO.toTooSmallAmountError(): ExpressError { + val minAmount = value?.minAmount ?: return ExpressError.DataError(code, description) + val decimals = value?.decimals ?: return ExpressError.DataError(code, description) + val requiredAmount = minAmount.toBigDecimalOrNull()?.movePointLeft(decimals) + ?: return ExpressError.DataError(code, description) + + return ExpressError.AmountError.TooSmallError(code = code, amount = requiredAmount) + } + + private fun ExpressErrorDTO.toTooBigAmountError(): ExpressError { + val maxAmount = value?.maxAmount ?: return ExpressError.DataError(code, description) + val decimals = value?.decimals ?: return ExpressError.DataError(code, description) + val requiredAmount = maxAmount.toBigDecimalOrNull()?.movePointLeft(decimals) + ?: return ExpressError.DataError(code, description) + + return ExpressError.AmountError.TooBigError(code = code, amount = requiredAmount) + } + + private fun ExpressErrorDTO.toNotEnoughAllowanceError(): ExpressError { + val currentAllowance = value?.currentAllowance ?: return ExpressError.DataError(code, description) + + return ExpressError.AmountError.NotEnoughAllowanceError(code = code, amount = currentAllowance) + } + + private fun ExpressErrorDTO.toInvalidFromDecimalsError(): ExpressError { + val receivedFromDecimals = value?.receivedFromDecimals ?: return ExpressError.DataError(code, description) + val expressFromDecimals = value?.expressFromDecimals ?: return ExpressError.DataError(code, description) + + return ExpressError.InvalidFromDecimalsError( + code = code, + receivedFromDecimals = receivedFromDecimals, + expressFromDecimals = expressFromDecimals, + ) + } + + private fun ExpressErrorDTO.toProviderDifferentAmountError(): ExpressError { + val decimals = value?.decimals ?: return ExpressError.DataError(code, description) + val fromAmount = value?.fromAmount?.toBigDecimalOrNull() ?: return ExpressError.DataError(code, description) + val fromAmountProvider = value?.fromAmountProvider?.toBigDecimalOrNull() + ?: return ExpressError.DataError(code, description) + + return ExpressError.ProviderDifferentAmountError( + code = code, + decimals = decimals, + fromAmount = fromAmount.movePointLeft(decimals), + fromProviderAmount = fromAmountProvider.movePointLeft(decimals), + ) + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt new file mode 100644 index 0000000000..36c5e88460 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.express.converter + +import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.api.express.models.response.ExchangeProviderType +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.utils.converter.Converter + +internal class ExpressProviderConverter : Converter { + + override fun convert(value: ExchangeProvider): ExpressProvider { + return ExpressProvider( + providerId = value.id, + rateTypes = emptyList(), + name = value.name, + type = convertExchangeType(value.type), + imageLarge = value.imageLargeUrl, + termsOfUse = value.termsOfUse, + privacyPolicy = value.privacyPolicy, + isRecommended = value.isRecommended, + slippage = value.slippage, + ) + } + + private fun convertExchangeType(type: ExchangeProviderType): ExpressProviderType { + return when (type) { + ExchangeProviderType.DEX -> ExpressProviderType.DEX + ExchangeProviderType.CEX -> ExpressProviderType.CEX + ExchangeProviderType.DEX_BRIDGE -> ExpressProviderType.DEX_BRIDGE + ExchangeProviderType.ONRAMP -> ExpressProviderType.ONRAMP + } + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt new file mode 100644 index 0000000000..75bfe50bf5 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt @@ -0,0 +1,43 @@ +package com.tangem.data.express.di + +import com.squareup.moshi.Moshi +import com.tangem.data.express.DefaultExpressRepository +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.data.express.error.DefaultExpressErrorResolver +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressErrorResolver +import com.tangem.domain.express.ExpressRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ExpressDataModule { + + @Provides + @Singleton + fun provideExpressErrorResolver(@NetworkMoshi moshi: Moshi): ExpressErrorResolver { + val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) + return DefaultExpressErrorResolver( + ExpressErrorConverter(jsonAdapter), + ) + } + + @Provides + @Singleton + fun provideExpressRepository( + tangemExpressApi: TangemExpressApi, + appPreferencesStore: AppPreferencesStore, + ): ExpressRepository { + return DefaultExpressRepository( + tangemExpressApi = tangemExpressApi, + appPreferencesStore = appPreferencesStore, + ) + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt b/data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt new file mode 100644 index 0000000000..96ca3d07a8 --- /dev/null +++ b/data/express/src/main/java/com/tangem/data/express/error/DefaultExpressErrorResolver.kt @@ -0,0 +1,20 @@ +package com.tangem.data.express.error + +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.express.ExpressErrorResolver +import com.tangem.domain.express.models.ExpressError + +internal class DefaultExpressErrorResolver( + private val expressErrorConverter: ExpressErrorConverter, +) : ExpressErrorResolver { + + override fun resolve(throwable: Throwable): ExpressError { + return when (throwable) { + is ApiResponseError.HttpException -> { + expressErrorConverter.convert(throwable.errorBody.orEmpty()) + } + else -> ExpressError.UnknownError + } + } +} \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 9bb7985754..7f412108e3 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -80,7 +80,7 @@ internal class DefaultNetworksStatusesStore( ifNotFound: (Network.ID) -> SimpleNetworkStatus?, ) { if (networks.isEmpty()) { - Timber.d("Nothing to update: networks is empty") + Timber.d("Nothing to update: networks are empty") return } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt index a0cadbe035..2acf7a68bf 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -9,9 +9,9 @@ import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.model.Quote import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -40,7 +40,7 @@ internal class HotCryptoCurrencyConverter( return HotCryptoCurrency( cryptoCurrency = currency.setupIconUrl(id = rawId.value), - quote = createQuote( + quoteStatus = createQuote( fiatRate = value.currentPrice, priceChange = value.priceChangePercentage, rawCurrencyId = rawId, @@ -89,16 +89,18 @@ internal class HotCryptoCurrencyConverter( fiatRate: BigDecimal?, priceChange: BigDecimal?, rawCurrencyId: CryptoCurrency.RawID, - ): Quote { + ): QuoteStatus { return if (fiatRate != null && priceChange != null) { - Quote.Value( + QuoteStatus( rawCurrencyId = rawCurrencyId, - fiatRate = fiatRate, - priceChange = priceChange.movePointLeft(2), - source = StatusSource.ACTUAL, // It doesn't matter + value = QuoteStatus.Data( + fiatRate = fiatRate, + priceChange = priceChange.movePointLeft(2), + source = StatusSource.ACTUAL, // It doesn't matter + ), ) } else { - Quote.Empty(rawCurrencyId) + QuoteStatus(rawCurrencyId = rawCurrencyId) } } diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index 368d9b3bef..2549fee133 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -9,29 +9,50 @@ android { namespace = "com.tangem.data.quotes" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { + // region Project - Core implementation(projects.core.datasource) - implementation(projects.core.utils) + api(projects.core.utils) + // endregion + // region Project - Data implementation(projects.data.common) - implementation(projects.data.tokens) + // endregion - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.models) - implementation(projects.domain.tokens.models) - implementation(projects.domain.quotes) - implementation(projects.domain.wallets.models) + // region Project - Domain + api(projects.domain.models) + api(projects.domain.quotes) + // endregion + // region Project - Libs + implementation(projects.libs.blockchainSdk) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Tangem SDKs + implementation(tangemDeps.blockchain) + // endregion + + // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) implementation(deps.timber) + // endregion - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - + // region Tests testImplementation(deps.test.coroutine) - testImplementation(deps.test.junit) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) testImplementation(projects.common.test) + // endregion } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt deleted file mode 100644 index 616b9cebc6..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/DefaultQuotesRepositoryV2.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.data.quotes - -import com.tangem.data.quotes.store.QuotesStoreV2 -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.QuotesRepositoryV2 -import com.tangem.domain.tokens.model.Quote -import javax.inject.Inject - -/** - * Default implementation of [QuotesRepositoryV2] - * - * @property quotesStore quotes store - * -[REDACTED_AUTHOR] - */ -internal class DefaultQuotesRepositoryV2 @Inject constructor( - private val quotesStore: QuotesStoreV2, -) : QuotesRepositoryV2 { - - override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? { - return quotesStore.getAllSyncOrNull()?.mapTo(hashSetOf()) { - it.takeIf { it.rawCurrencyId in currenciesIds } ?: Quote.Empty(it.rawCurrencyId) - } - } -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt b/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt new file mode 100644 index 0000000000..6fe470b39a --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.quotes.converter + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero + +/** + * Converter from [QuotesResponse.Quote] to [QuoteStatus] + * + * @property source status source + * +[REDACTED_AUTHOR] + */ +internal class QuoteStatusConverter( + private val source: StatusSource, +) : Converter, QuoteStatus> { + + override fun convert(value: Map.Entry): QuoteStatus { + val (currencyId, quote) = value + + return QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID(currencyId), + value = QuoteStatus.Data( + source = source, + fiatRate = quote.price.orZero(), + priceChange = quote.priceChange24h.orZero().movePointLeft(2), + ), + ) + } +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt deleted file mode 100644 index 1bdef12d69..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteFetcherModule.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.data.quotes.di - -import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher -import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater -import com.tangem.data.quotes.single.DefaultSingleQuoteFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.multi.MultiQuoteUpdater -import com.tangem.domain.quotes.single.SingleQuoteFetcher -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface QuoteFetcherModule { - - @Binds - @Singleton - fun bindMultiQuoteFetcher(impl: DefaultMultiQuoteFetcher): MultiQuoteFetcher - - @Binds - @Singleton - fun bindMultiQuoteUpdater(impl: DefaultMultiQuoteUpdater): MultiQuoteUpdater - - @Binds - @Singleton - fun bindSingleQuoteFetcher(impl: DefaultSingleQuoteFetcher): SingleQuoteFetcher -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt deleted file mode 100644 index 0fc3d0faed..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteProducerFactoryModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.data.quotes.di - -import com.tangem.data.quotes.single.DefaultSingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteProducer -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface QuoteProducerFactoryModule { - - @Binds - @Singleton - fun bindSingleQuoteProducerFactory(impl: DefaultSingleQuoteProducer.Factory): SingleQuoteProducer.Factory -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt new file mode 100644 index 0000000000..8f425800fe --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher +import com.tangem.data.quotes.single.DefaultSingleQuoteStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface QuoteStatusFetcherModule { + + @Binds + @Singleton + fun bindMultiQuoteStatusFetcher(impl: DefaultMultiQuoteStatusFetcher): MultiQuoteStatusFetcher + + @Binds + @Singleton + fun bindSingleQuoteStatusFetcher(impl: DefaultSingleQuoteStatusFetcher): SingleQuoteStatusFetcher +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt new file mode 100644 index 0000000000..a6b653ba03 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusProducerFactoryModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.quotes.di + +import com.tangem.data.quotes.single.DefaultSingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface QuoteStatusProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleQuoteStatusProducerFactory( + impl: DefaultSingleQuoteStatusProducer.Factory, + ): SingleQuoteStatusProducer.Factory +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusSupplierModule.kt similarity index 53% rename from data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusSupplierModule.kt index c31b91a7c7..135f3b3460 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteSupplierModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuoteStatusSupplierModule.kt @@ -1,7 +1,7 @@ package com.tangem.data.quotes.di -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -10,12 +10,12 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object QuoteSupplierModule { +internal object QuoteStatusSupplierModule { @Provides @Singleton - fun provideSingleQuoteSupplier(factory: SingleQuoteProducer.Factory): SingleQuoteSupplier { - return object : SingleQuoteSupplier( + fun provideSingleQuoteStatusSupplier(factory: SingleQuoteStatusProducer.Factory): SingleQuoteStatusSupplier { + return object : SingleQuoteStatusSupplier( factory = factory, keyCreator = { "single_quote_${it.rawCurrencyId.value}" }, ) {} diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index 58803a1a56..0cf87628bb 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -4,15 +4,19 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi -import com.tangem.data.quotes.DefaultQuotesRepositoryV2 -import com.tangem.data.quotes.store.DefaultQuotesStoreV2 -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.multi.DefaultMultiQuoteUpdater +import com.tangem.data.quotes.repository.DefaultQuotesRepository +import com.tangem.data.quotes.store.DefaultQuotesStatusesStore +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.multi.MultiQuoteUpdater import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -33,8 +37,8 @@ internal object QuotesDataModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, - ): QuotesStoreV2 { - return DefaultQuotesStoreV2( + ): QuotesStatusesStore { + return DefaultQuotesStatusesStore( runtimeStore = RuntimeSharedStore(), persistenceDataStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( @@ -51,5 +55,23 @@ internal object QuotesDataModule { @Singleton @Provides - fun providesQuotesRepositoryV2(impl: DefaultQuotesRepositoryV2): QuotesRepositoryV2 = impl + fun providesQuotesRepository(quotesStatusesStore: QuotesStatusesStore): QuotesRepository { + return DefaultQuotesRepository(quotesStatusesStore = quotesStatusesStore) + } + + @Singleton + @Provides + fun bindMultiQuoteUpdater( + appCurrencyResponseStore: AppCurrencyResponseStore, + quotesStatusesStore: QuotesStatusesStore, + multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + dispatchers: CoroutineDispatcherProvider, + ): MultiQuoteUpdater { + return DefaultMultiQuoteUpdater( + appCurrencyResponseStore = appCurrencyResponseStore, + quotesStatusesStore = quotesStatusesStore, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt similarity index 64% rename from data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index cfa6b766f0..c605e43574 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -2,13 +2,15 @@ package com.tangem.data.quotes.multi import arrow.core.Either import com.tangem.data.common.api.safeApiCallWithTimeout -import com.tangem.data.quotes.store.QuotesStoreV2 -import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.data.quotes.store.setSourceAsCache +import com.tangem.data.quotes.store.setSourceAsOnlyCache +import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber @@ -16,34 +18,32 @@ import javax.inject.Inject import javax.inject.Singleton /** - * Default implementation of [MultiQuoteFetcher] + * Default implementation of [MultiQuoteStatusFetcher] * * @property tangemTechApi tangemTech api * @property appCurrencyResponseStore app currency response store - * @property quotesStore quotes store + * @property quotesStatusesStore quotes store * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Singleton -internal class DefaultMultiQuoteFetcher @Inject constructor( +internal class DefaultMultiQuoteStatusFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStoreV2, + private val quotesStatusesStore: QuotesStatusesStore, private val dispatchers: CoroutineDispatcherProvider, -) : MultiQuoteFetcher { +) : MultiQuoteStatusFetcher { - private val quotesUnsupportedCurrenciesAdapter = QuotesUnsupportedCurrenciesIdAdapter() - - override suspend fun invoke(params: MultiQuoteFetcher.Params) = Either.catchOn(dispatchers.default) { + override suspend fun invoke(params: MultiQuoteStatusFetcher.Params) = Either.catchOn(dispatchers.default) { if (params.currenciesIds.isEmpty()) { Timber.d("No currencies to fetch quotes for") return@catchOn } - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStatusesStore.setSourceAsCache(currenciesIds = params.currenciesIds) - val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( + val replacementIdsResult = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( currenciesIds = params.currenciesIds.mapTo( destination = hashSetOf(), transform = CryptoCurrency.RawID::value, @@ -62,19 +62,19 @@ internal class DefaultMultiQuoteFetcher @Inject constructor( onError = { error -> throw error }, ) - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + val updatedResponse = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( response = response, filteredIds = replacementIdsResult.idsFiltered, ) - quotesStore.storeActual(values = updatedResponse.quotes) + quotesStatusesStore.store(values = updatedResponse.quotes) } .onLeft { Timber.e(it) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } - private suspend fun getAppCurrencyId(params: MultiQuoteFetcher.Params): String { + private suspend fun getAppCurrencyId(params: MultiQuoteStatusFetcher.Params): String { val appCurrencyId = params.appCurrencyId ?: appCurrencyResponseStore.getSyncOrNull()?.id diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index 06e492857e..73e2446946 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -2,12 +2,12 @@ package com.tangem.data.quotes.multi import androidx.annotation.VisibleForTesting import arrow.core.left -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater -import com.tangem.domain.tokens.model.Quote import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -17,24 +17,21 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton /** * Default implementation of [MultiQuoteUpdater] which updates quotes when the app currency changes * * @property appCurrencyResponseStore app currency response store - * @property quotesStore quotes store - * @property multiQuoteFetcher multi quote fetcher + * @property quotesStatusesStore quotes store + * @property multiQuoteStatusFetcher multi quote status fetcher * @param dispatchers dispatchers * [REDACTED_AUTHOR] */ -@Singleton -internal class DefaultMultiQuoteUpdater @Inject constructor( +internal class DefaultMultiQuoteUpdater( private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val quotesStore: QuotesStoreV2, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val quotesStatusesStore: QuotesStatusesStore, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, dispatchers: CoroutineDispatcherProvider, ) : MultiQuoteUpdater { @@ -60,11 +57,14 @@ internal class DefaultMultiQuoteUpdater @Inject constructor( .distinctUntilChanged() .filterNotNull() .mapLatest { appCurrency -> - val currenciesIds = quotesStore.getAllSyncOrNull().orEmpty() - .mapTo(destination = hashSetOf(), transform = Quote::rawCurrencyId) + val currenciesIds = quotesStatusesStore.getAllSyncOrNull().orEmpty() + .mapTo(destination = hashSetOf(), transform = QuoteStatus::rawCurrencyId) - multiQuoteFetcher( - params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrency.id), + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( + currenciesIds = currenciesIds, + appCurrencyId = appCurrency.id, + ), ) .onLeft(Timber::e) } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt new file mode 100644 index 0000000000..d170b82c5c --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -0,0 +1,34 @@ +package com.tangem.data.quotes.repository + +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.QuotesRepository +import timber.log.Timber + +/** + * Default implementation of [QuotesRepository] + * + * @property quotesStatusesStore quotes statuses store + * +[REDACTED_AUTHOR] + */ +internal class DefaultQuotesRepository( + private val quotesStatusesStore: QuotesStatusesStore, +) : QuotesRepository { + + override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set { + if (currenciesIds.isEmpty()) { + Timber.e("currenciesIds are empty") + return emptySet() + } + + val storedQuotes = quotesStatusesStore.getAllSyncOrNull() + ?.filter { it.rawCurrencyId in currenciesIds } + + return currenciesIds.mapTo(hashSetOf()) { currencyId -> + storedQuotes?.firstOrNull { it.rawCurrencyId == currencyId } + ?: QuoteStatus(rawCurrencyId = currencyId) + } + } +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt deleted file mode 100644 index 9cd56dacc2..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcher.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.data.quotes.single - -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteFetcher -import javax.inject.Inject - -internal class DefaultSingleQuoteFetcher @Inject constructor( - private val multiQuoteFetcher: MultiQuoteFetcher, -) : SingleQuoteFetcher { - - override suspend fun invoke(params: SingleQuoteFetcher.Params) = multiQuoteFetcher.invoke( - MultiQuoteFetcher.Params( - currenciesIds = setOf(params.rawCurrencyId), - appCurrencyId = params.appCurrencyId, - ), - ) -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt deleted file mode 100644 index d8450243b6..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.data.quotes.single - -import com.tangem.data.quotes.store.QuotesStoreV2 -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull - -/** - * Default implementation of [SingleQuoteProducer] - * - * @property params params - * @property quotesStore quotes store - */ -internal class DefaultSingleQuoteProducer @AssistedInject constructor( - @Assisted val params: SingleQuoteProducer.Params, - private val quotesStore: QuotesStoreV2, - private val dispatchers: CoroutineDispatcherProvider, -) : SingleQuoteProducer { - - override val fallback: Quote = Quote.Empty(rawCurrencyId = params.rawCurrencyId) - - override fun produce(): Flow { - return quotesStore.get() - .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } - .distinctUntilChanged() - .flowOn(dispatchers.default) - } - - @AssistedFactory - interface Factory : SingleQuoteProducer.Factory { - override fun create(params: SingleQuoteProducer.Params): DefaultSingleQuoteProducer - } -} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt new file mode 100644 index 0000000000..44dc7157d8 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcher.kt @@ -0,0 +1,22 @@ +package com.tangem.data.quotes.single + +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher +import javax.inject.Inject + +/** + * Default implementation of [SingleQuoteStatusFetcher] + * + * @property multiQuoteStatusFetcher fetcher of quotes statuses + */ +internal class DefaultSingleQuoteStatusFetcher @Inject constructor( + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, +) : SingleQuoteStatusFetcher { + + override suspend fun invoke(params: SingleQuoteStatusFetcher.Params) = multiQuoteStatusFetcher.invoke( + MultiQuoteStatusFetcher.Params( + currenciesIds = setOf(params.rawCurrencyId), + appCurrencyId = params.appCurrencyId, + ), + ) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt new file mode 100644 index 0000000000..692d5cb24a --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducer.kt @@ -0,0 +1,40 @@ +package com.tangem.data.quotes.single + +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.mapNotNull + +/** + * Default implementation of [SingleQuoteStatusProducer] + * + * @property params params + * @property quotesStatusesStore quotes store + */ +internal class DefaultSingleQuoteStatusProducer @AssistedInject constructor( + @Assisted val params: SingleQuoteStatusProducer.Params, + private val quotesStatusesStore: QuotesStatusesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SingleQuoteStatusProducer { + + override val fallback: QuoteStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId) + + override fun produce(): Flow { + return quotesStatusesStore.get() + .mapNotNull { quotes -> quotes.firstOrNull { it.rawCurrencyId == params.rawCurrencyId } } + .distinctUntilChanged() + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : SingleQuoteStatusProducer.Factory { + override fun create(params: SingleQuoteStatusProducer.Params): DefaultSingleQuoteStatusProducer + } +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt similarity index 54% rename from data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index ccad728a92..52b464efa0 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStoreV2.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt @@ -1,12 +1,12 @@ package com.tangem.data.quotes.store import androidx.datastore.core.DataStore +import com.tangem.data.quotes.converter.QuoteStatusConverter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.quote.converter.QuoteConverter import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.CoroutineScope @@ -15,21 +15,22 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import timber.log.Timber internal typealias CurrencyIdWithQuote = Map /** - * Default implementation of [QuotesStoreV2] + * Default implementation of [QuotesStatusesStore] * * @property runtimeStore runtime store * @property persistenceDataStore persistence store * @param dispatchers dispatchers */ -internal class DefaultQuotesStoreV2( - private val runtimeStore: RuntimeSharedStore>, +internal class DefaultQuotesStatusesStore( + private val runtimeStore: RuntimeSharedStore>, private val persistenceDataStore: DataStore, dispatchers: CoroutineDispatcherProvider, -) : QuotesStoreV2 { +) : QuotesStatusesStore { private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) @@ -40,62 +41,68 @@ internal class DefaultQuotesStoreV2( if (cachedStatuses.isNullOrEmpty()) return@launch runtimeStore.store( - value = QuoteConverter(isCached = true).convertSet(input = cachedStatuses.entries), + value = QuoteStatusConverter(source = StatusSource.CACHE).convertSet(input = cachedStatuses.entries), ) } } - override fun get(): Flow> = runtimeStore.get() + override fun get(): Flow> = runtimeStore.get() - override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() + override suspend fun getAllSyncOrNull(): Set? = runtimeStore.getSyncOrNull() - override suspend fun refresh(currenciesIds: Set) { - updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.CACHE) - } - - override suspend fun storeActual(values: Map) { - coroutineScope { - launch { - val quotes = QuoteConverter(isCached = false).convertSet(input = values.entries) - storeInRuntimeStore(values = quotes) - } - launch { storeInPersistenceStore(values = values) } - } - } - - override suspend fun storeError(currenciesIds: Set) { - updateStatusSourceInRuntime( - currenciesIds = currenciesIds, - ifNotFound = Quote::Empty, - source = StatusSource.ONLY_CACHE, + override suspend fun updateStatusSource( + currencyId: CryptoCurrency.RawID, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, + ) { + updateStatusSource( + currenciesIds = setOf(currencyId), + source = source, + ifNotFound = ifNotFound, ) } - private suspend fun updateStatusSourceInRuntime( + override suspend fun updateStatusSource( currenciesIds: Set, - ifNotFound: (CryptoCurrency.RawID) -> Quote? = { null }, source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, ) { + if (currenciesIds.isEmpty()) { + Timber.d("Nothing to update: currencies ids are empty") + return + } + runtimeStore.update(default = emptySet()) { stored -> val updatedQuotes = currenciesIds.mapNotNullTo(hashSetOf()) { id -> val quote = stored.firstOrNull { it.rawCurrencyId == id } ?: ifNotFound(id) ?: return@mapNotNullTo null - quote.copySealed(source = source) + quote.copy(value = quote.value.copySealed(source = source)) } stored.addOrReplace(items = updatedQuotes) { old, new -> old.rawCurrencyId == new.rawCurrencyId } } } - private suspend fun storeInRuntimeStore(values: Set) { - runtimeStore.update(default = emptySet()) { saved -> - saved.addOrReplace(items = values) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + override suspend fun store(values: CurrencyIdWithQuote) { + if (values.isEmpty()) return + + coroutineScope { + launch { storeInRuntime(values = values) } + launch { storeInPersistence(values = values) } } } - private suspend fun storeInPersistenceStore(values: Map) { + private suspend fun storeInRuntime(values: CurrencyIdWithQuote) { + val quotes = QuoteStatusConverter(source = StatusSource.ACTUAL).convertSet(input = values.entries) + + runtimeStore.update(default = emptySet()) { saved -> + saved.addOrReplace(items = quotes) { prev, new -> prev.rawCurrencyId == new.rawCurrencyId } + } + } + + private suspend fun storeInPersistence(values: CurrencyIdWithQuote) { persistenceDataStore.updateData { storedQuotes -> storedQuotes + values } } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt new file mode 100644 index 0000000000..a9dfe1fac2 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStore.kt @@ -0,0 +1,46 @@ +package com.tangem.data.quotes.store + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import kotlinx.coroutines.flow.Flow + +/** Store of [QuoteStatus]'es set */ +internal interface QuotesStatusesStore { + + /** Get flow of quotes */ + fun get(): Flow> + + /** Get all quotes synchronously or null */ + suspend fun getAllSyncOrNull(): Set? + + /** + * Update [source] of [QuoteStatus] by [currencyId]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + currencyId: CryptoCurrency.RawID, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + ) + + /** + * Update [source] of [QuoteStatus]es by [currenciesIds]. + * If the status is not found, create a new one by [ifNotFound]. + */ + suspend fun updateStatusSource( + currenciesIds: Set, + source: StatusSource, + ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + ) + + /** + * Store quotes statuses + * + * @param values map of currency ids and quotes + * + * See complex methods in `QuotesStatusesStoreExt`. + */ + suspend fun store(values: Map) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt new file mode 100644 index 0000000000..eb860d2de9 --- /dev/null +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStatusesStoreExt.kt @@ -0,0 +1,22 @@ +package com.tangem.data.quotes.store + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus + +/** Set [StatusSource] of quotes statuses as [StatusSource.CACHE] for [currenciesIds] */ +internal suspend fun QuotesStatusesStore.setSourceAsCache(currenciesIds: Set) { + updateStatusSource(currenciesIds = currenciesIds, source = StatusSource.CACHE) +} + +/** + * Set [StatusSource] of quotes statuses as [StatusSource.ONLY_CACHE] for [currenciesIds]. + * If the stored status is not found, store a default [QuoteStatus.Empty] status. + */ +internal suspend fun QuotesStatusesStore.setSourceAsOnlyCache(currenciesIds: Set) { + updateStatusSource( + currenciesIds = currenciesIds, + source = StatusSource.ONLY_CACHE, + ifNotFound = ::QuoteStatus, + ) +} \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt deleted file mode 100644 index 1fb13a9d5e..0000000000 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/QuotesStoreV2.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.data.quotes.store - -import com.tangem.datasource.api.tangemTech.models.QuotesResponse -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote -import kotlinx.coroutines.flow.Flow - -/** Store of [Quote]'es set */ -internal interface QuotesStoreV2 { - - /** Get flow of quotes */ - fun get(): Flow> - - /** Get all quotes synchronously or null */ - suspend fun getAllSyncOrNull(): Set? - - /** Refresh status of [currenciesIds] */ - suspend fun refresh(currenciesIds: Set) - - /** Store actual map of currency ids and quotes [values] */ - suspend fun storeActual(values: Map) - - /** Store error for [currenciesIds] */ - suspend fun storeError(currenciesIds: Set) -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt similarity index 70% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt rename to data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index bf7779378a..72c9a1f66c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -1,23 +1,27 @@ -package com.tangem.data.tokens.utils +package com.tangem.data.quotes.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.api.tangemTech.models.QuotesResponse -/** - * Adapter to replace unsupported currencies for quotes request if it necessary - */ -class QuotesUnsupportedCurrenciesIdAdapter { +/** Adapter to replace unsupported currencies for quotes request if it necessary */ +internal object QuotesUnsupportedCurrenciesIdAdapter { - /** - * Replaces unsupported currencies id to it replacements for request - */ + /** Ethereum coin ID */ + private val ethCoinId = Blockchain.Ethereum.toCoinId() + + /** Map that contains unsupported currencies and their replacement for request */ + private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } + + /** Replaces unsupported currencies id [currenciesIds] to it replacements for request */ fun replaceUnsupportedCurrencies(currenciesIds: Set): ReplacementResult { val idsForRequest = mutableSetOf() val idsFiltered = mutableSetOf() + currenciesIds.forEach { currencyId -> val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[currencyId] + if (replacementId != null) { idsFiltered.add(currencyId) idsForRequest.add(replacementId) @@ -25,16 +29,17 @@ class QuotesUnsupportedCurrenciesIdAdapter { idsForRequest.add(currencyId) } } + return ReplacementResult(idsForRequest = idsForRequest, idsFiltered = idsFiltered) } - /** - * Recover previously replaced currencies to response that uses in application - */ + /** Recover previously replaced currencies [filteredIds] to [response] that uses in application */ fun getResponseWithUnsupportedCurrencies(response: QuotesResponse, filteredIds: Set): QuotesResponse { val updatedQuotes = mutableMapOf() + filteredIds.forEach { filteredId -> val replacementId = UNSUPPORTED_IDS_WITH_REPLACEMENTS[filteredId] + if (replacementId != null) { val quoteReplacement = response.quotes[replacementId] if (quoteReplacement != null) { @@ -42,16 +47,9 @@ class QuotesUnsupportedCurrenciesIdAdapter { } } } + return response.copy(quotes = updatedQuotes + response.quotes) } data class ReplacementResult(val idsForRequest: Set, val idsFiltered: Set) - - companion object { - /** - * Map that contains unsupported currencies and their replacement for request - */ - private val ethCoinId = Blockchain.Ethereum.toCoinId() - private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = l2BlockchainsCoinIds.associateWith { ethCoinId } - } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt new file mode 100644 index 0000000000..e6cb1e498c --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt @@ -0,0 +1,118 @@ +package com.tangem.data.quotes.converter + +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.data.quote.toDomain +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuoteStatusConverterTest { + + private val ethQuoteDM = "ETH" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) + + @ParameterizedTest + @ProvideTestModels + fun convert(model: ConvertTestModel) { + // Act + val actual = QuoteStatusConverter(source = model.source).convert(value = model.value) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + ConvertTestModel( + source = StatusSource.CACHE, + value = createEntry(value = ethQuoteDM), + expected = ethQuoteDM.toDomain(source = StatusSource.CACHE), + ), + ConvertTestModel( + source = StatusSource.ONLY_CACHE, + value = createEntry(value = ethQuoteDM), + expected = ethQuoteDM.toDomain(source = StatusSource.ONLY_CACHE), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry(value = ethQuoteDM), + expected = ethQuoteDM.toDomain(source = StatusSource.ACTUAL), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry( + value = "ETH" to QuotesResponse.Quote( + price = null, + priceChange24h = null, + priceChange1w = null, + priceChange30d = null, + ), + ), + expected = QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID("ETH"), + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal("0.00"), + ), + ), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry( + value = "ETH" to QuotesResponse.Quote( + price = null, + priceChange24h = null, + priceChange1w = BigDecimal.ZERO, + priceChange30d = BigDecimal.ZERO, + ), + ), + expected = QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID("ETH"), + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal("0.00"), + ), + ), + ), + ConvertTestModel( + source = StatusSource.ACTUAL, + value = createEntry( + value = "ETH" to QuotesResponse.Quote( + price = BigDecimal.ONE, + priceChange24h = BigDecimal.ONE, + priceChange1w = null, + priceChange30d = null, + ), + ), + expected = QuoteStatus( + rawCurrencyId = CryptoCurrency.RawID("ETH"), + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal("0.01"), + ), + ), + ), + ) + + private fun createEntry(value: Pair): Map.Entry { + return mapOf(value).entries.first() + } + + data class ConvertTestModel( + val source: StatusSource, + val value: Map.Entry, + val expected: QuoteStatus, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt similarity index 70% rename from data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index 63fec8ab17..4db5f02c4f 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -2,7 +2,9 @@ package com.tangem.data.quotes.multi import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.data.quotes.store.setSourceAsCache +import com.tangem.data.quotes.store.setSourceAsOnlyCache import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -10,7 +12,7 @@ import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify @@ -23,22 +25,22 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -internal class DefaultMultiQuoteFetcherTest { +internal class DefaultMultiQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) - private val fetcher = DefaultMultiQuoteFetcher( + private val fetcher = DefaultMultiQuoteStatusFetcher( tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, - quotesStore = quotesStore, + quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `fetch quotes successfully`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -50,17 +52,17 @@ internal class DefaultMultiQuoteFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -68,16 +70,16 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes successfully if currenciesIds from params is empty`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = null) val actual = fetcher(params) coVerify(inverse = true) { - quotesStore.refresh(currenciesIds = any()) + quotesStore.setSourceAsCache(currenciesIds = any()) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) - quotesStore.storeError(currenciesIds = any()) + quotesStore.store(values = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -86,7 +88,7 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes successfully if appCurrencyId from params is not null`() = runTest { val appCurrencyId = "usd" - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) val coinIds = "BTC,ETH" coEvery { @@ -96,16 +98,16 @@ internal class DefaultMultiQuoteFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -114,19 +116,19 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes failure because appCurrencyId from params is blank`() = runTest { val appCurrencyId = "" - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = appCurrencyId) val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -137,7 +139,7 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes failure because api request failed`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -150,17 +152,17 @@ internal class DefaultMultiQuoteFetcherTest { val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -168,21 +170,21 @@ internal class DefaultMultiQuoteFetcherTest { @Test fun `fetch quotes failure because app currency not found`() = runTest { - val params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) + val params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null val actual = fetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsCache(currenciesIds = params.currenciesIds) appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = params.currenciesIds) + quotesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt similarity index 79% rename from data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt index f14962f52b..9d47ec8857 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdaterTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusUpdaterTest.kt @@ -3,10 +3,10 @@ package com.tangem.data.quotes.multi import arrow.core.right import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.flow.* @@ -16,16 +16,16 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class DefaultMultiQuoteUpdaterTest { +internal class DefaultMultiQuoteStatusUpdaterTest { private val appCurrencyResponseStore: AppCurrencyResponseStore = mockk() - private val quotesStore: QuotesStoreV2 = mockk() - private val multiQuoteFetcher: MultiQuoteFetcher = mockk() + private val quotesStore: QuotesStatusesStore = mockk() + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiQuoteUpdater = DefaultMultiQuoteUpdater( appCurrencyResponseStore = appCurrencyResponseStore, - quotesStore = quotesStore, - multiQuoteFetcher = multiQuoteFetcher, + quotesStatusesStore = quotesStore, + multiQuoteStatusFetcher = multiQuoteStatusFetcher, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -36,8 +36,8 @@ internal class DefaultMultiQuoteUpdaterTest { every { appCurrencyResponseStore.get() } returns appCurrencyFlow coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) - coEvery { multiQuoteFetcher(params) } returns Unit.right() + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteStatusFetcher(params) } returns Unit.right() val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() @@ -48,7 +48,7 @@ internal class DefaultMultiQuoteUpdaterTest { coVerifyOrder { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(params) + multiQuoteStatusFetcher(params) } } @@ -59,8 +59,8 @@ internal class DefaultMultiQuoteUpdaterTest { every { appCurrencyResponseStore.get() } returns appCurrencyFlow coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) - coEvery { multiQuoteFetcher(params) } returns Unit.right() + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteStatusFetcher(params) } returns Unit.right() val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() @@ -71,7 +71,7 @@ internal class DefaultMultiQuoteUpdaterTest { coVerifyOrder { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(params) + multiQuoteStatusFetcher(params) } } @@ -90,7 +90,7 @@ internal class DefaultMultiQuoteUpdaterTest { coVerify(inverse = true) { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(any()) + multiQuoteStatusFetcher(any()) } } @@ -111,8 +111,8 @@ internal class DefaultMultiQuoteUpdaterTest { every { appCurrencyResponseStore.get() } returns appCurrencyFlow coEvery { quotesStore.getAllSyncOrNull() } returns emptySet() - val params = MultiQuoteFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) - coEvery { multiQuoteFetcher(params) } returns Unit.right() + val params = MultiQuoteStatusFetcher.Params(currenciesIds = emptySet(), appCurrencyId = usdAppCurrency.id) + coEvery { multiQuoteStatusFetcher(params) } returns Unit.right() val actual = multiQuoteUpdater.getMultiQuoteUpdatesFlow() @@ -126,7 +126,7 @@ internal class DefaultMultiQuoteUpdaterTest { coVerify(inverse = true) { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(any()) + multiQuoteStatusFetcher(any()) } innerFlow.emit(value = true) @@ -136,7 +136,7 @@ internal class DefaultMultiQuoteUpdaterTest { coVerifyOrder { quotesStore.getAllSyncOrNull() - multiQuoteFetcher(params) + multiQuoteStatusFetcher(params) } } diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt new file mode 100644 index 0000000000..07c29dad8f --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt @@ -0,0 +1,108 @@ +package com.tangem.data.quotes.repository + +import com.google.common.truth.Truth +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.QuotesRepository +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultQuotesRepositoryTest { + + private val quotesStatusesStore = mockk() + private val repository: QuotesRepository = DefaultQuotesRepository(quotesStatusesStore = quotesStatusesStore) + + @BeforeEach + fun resetMocks() { + clearMocks(quotesStatusesStore) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetMultiQuoteSyncOrNull { + + private val btcRawId = CryptoCurrency.RawID(value = "BTC") + private val ethRawId = CryptoCurrency.RawID(value = "ETH") + + private val ethQuote = QuoteStatus( + rawCurrencyId = ethRawId, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal.ZERO, + ), + ) + + @ParameterizedTest + @ProvideTestModels + fun getMultiQuoteSyncOrNull(model: GetMultiQuoteSyncOrNullModel) = runTest { + // Arrange + coEvery { quotesStatusesStore.getAllSyncOrNull() } returns model.initialStore + + // Act + val actual = repository.getMultiQuoteSyncOrNull(currenciesIds = model.currencyIds) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetMultiQuoteSyncOrNullModel( + initialStore = null, + currencyIds = emptySet(), + expected = emptySet(), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = null, + currencyIds = setOf(ethRawId), + expected = setOf(QuoteStatus(rawCurrencyId = ethRawId)), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = emptySet(), + currencyIds = emptySet(), + expected = emptySet(), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = emptySet(), + currencyIds = setOf(ethRawId), + expected = setOf(QuoteStatus(rawCurrencyId = ethRawId)), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = setOf(ethQuote), + currencyIds = setOf(ethRawId), + expected = setOf(ethQuote), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = setOf(QuoteStatus(rawCurrencyId = btcRawId)), + currencyIds = setOf(ethRawId), + expected = setOf(QuoteStatus(rawCurrencyId = ethRawId)), + ), + GetMultiQuoteSyncOrNullModel( + initialStore = setOf(ethQuote, QuoteStatus(rawCurrencyId = btcRawId)), + currencyIds = setOf(ethRawId), + expected = setOf(ethQuote), + ), + ) + } + + data class GetMultiQuoteSyncOrNullModel( + val initialStore: Set?, + val currencyIds: Set, + val expected: Set?, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt similarity index 68% rename from data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt index a16f80b3d7..245d0364b9 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusFetcherTest.kt @@ -2,8 +2,10 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.data.quotes.multi.DefaultMultiQuoteFetcher -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.multi.DefaultMultiQuoteStatusFetcher +import com.tangem.data.quotes.store.QuotesStatusesStore +import com.tangem.data.quotes.store.setSourceAsCache +import com.tangem.data.quotes.store.setSourceAsOnlyCache import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -11,34 +13,34 @@ import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifyOrder import io.mockk.mockk import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.Test import java.math.BigDecimal -internal class DefaultSingleQuoteFetcherTest { +internal class DefaultSingleQuoteStatusFetcherTest { private val tangemTechApi = mockk(relaxed = true) private val appCurrencyResponseStore = mockk(relaxed = true) - private val quotesStore = mockk(relaxed = true) + private val quotesStore = mockk(relaxed = true) - private val multiFetcher = DefaultMultiQuoteFetcher( + private val multiFetcher = DefaultMultiQuoteStatusFetcher( tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, - quotesStore = quotesStore, + quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) - private val singleFetcher = DefaultSingleQuoteFetcher(multiFetcher) + private val singleFetcher = DefaultSingleQuoteStatusFetcher(multiFetcher) @Test fun `fetch single quote successfully`() = runTest { - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -50,14 +52,14 @@ internal class DefaultSingleQuoteFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(params.rawCurrencyId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(params.rawCurrencyId)) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } coVerify(inverse = true) { - quotesStore.storeError(currenciesIds = any()) + quotesStore.setSourceAsOnlyCache(currenciesIds = any()) } Truth.assertThat(actual.isRight()).isTrue() @@ -66,7 +68,7 @@ internal class DefaultSingleQuoteFetcherTest { @Test fun `fetch single quote successfully if appCurrencyId from params is not null`() = runTest { val appCurrencyId = "usd" - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) val coinIds = "BTC" coEvery { @@ -76,9 +78,9 @@ internal class DefaultSingleQuoteFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeActual(values = successResponse.quotes) + quotesStore.store(values = successResponse.quotes) } Truth.assertThat(actual.isRight()).isTrue() @@ -87,13 +89,13 @@ internal class DefaultSingleQuoteFetcherTest { @Test fun `fetch single quote failure because appCurrencyId from params is blank`() = runTest { val appCurrencyId = "" - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = appCurrencyId) val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } Truth.assertThat(actual.isLeft()).isTrue() @@ -104,7 +106,7 @@ internal class DefaultSingleQuoteFetcherTest { @Test fun `fetch single quote failure because api request failed`() = runTest { - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns usdAppCurrency @@ -117,14 +119,14 @@ internal class DefaultSingleQuoteFetcherTest { val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) appCurrencyResponseStore.getSyncOrNull() tangemTechApi.getQuotes(currencyId = "usd", coinIds = coinIds) - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } coVerify(inverse = true) { - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() @@ -132,21 +134,21 @@ internal class DefaultSingleQuoteFetcherTest { @Test fun `fetch single quote failure because app currency not found`() = runTest { - val params = SingleQuoteFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) + val params = SingleQuoteStatusFetcher.Params(rawCurrencyId = currenciesId, appCurrencyId = null) coEvery { appCurrencyResponseStore.getSyncOrNull() } returns null val actual = singleFetcher(params) coVerifyOrder { - quotesStore.refresh(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsCache(currenciesIds = setOf(currenciesId)) appCurrencyResponseStore.getSyncOrNull() - quotesStore.storeError(currenciesIds = setOf(currenciesId)) + quotesStore.setSourceAsOnlyCache(currenciesIds = setOf(currenciesId)) } coVerify(inverse = true) { tangemTechApi.getQuotes(currencyId = any(), coinIds = any()) - quotesStore.storeActual(values = any()) + quotesStore.store(values = any()) } Truth.assertThat(actual.isLeft()).isTrue() diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt similarity index 73% rename from data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt rename to data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index ce3be77d28..ef5fccaf7f 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -2,11 +2,11 @@ package com.tangem.data.quotes.single import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.quotes.store.QuotesStoreV2 +import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.every import io.mockk.mockk @@ -19,27 +19,27 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -internal class DefaultSingleQuoteProducerTest { +internal class DefaultSingleQuoteStatusProducerTest { - private val params = SingleQuoteProducer.Params( + private val params = SingleQuoteStatusProducer.Params( rawCurrencyId = CryptoCurrency.RawID(value = "BTC"), ) - private val quotesStore = mockk() + private val quotesStore = mockk() - private val producer = DefaultSingleQuoteProducer( + private val producer = DefaultSingleQuoteStatusProducer( params = params, - quotesStore = quotesStore, + quotesStatusesStore = quotesStore, dispatchers = TestingCoroutineDispatcherProvider(), ) @Test fun `test that flow is mapped for network from params`() = runTest { - val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId) val storeQuote = flowOf( setOf( status, - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), ), ) @@ -57,7 +57,7 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test that flow is updated if quote is updated`() = runTest { - val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) every { quotesStore.get() } returns storeQuote @@ -66,7 +66,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } // first emit - val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId) storeQuote.emit(value = setOf(status)) val values1 = getEmittedValues(flow = actual) @@ -75,11 +75,13 @@ internal class DefaultSingleQuoteProducerTest { Truth.assertThat(values1).isEqualTo(listOf(status)) // second emit - val updatedStatus = Quote.Value( + val updatedStatus = QuoteStatus( rawCurrencyId = params.rawCurrencyId, - fiatRate = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), ) storeQuote.emit(value = setOf(updatedStatus)) @@ -91,7 +93,7 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test that flow is filtered the same status`() = runTest { - val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + val storeQuote = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) every { quotesStore.get() } returns storeQuote @@ -100,7 +102,7 @@ internal class DefaultSingleQuoteProducerTest { verify { quotesStore.get() } // first emit - val status = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val status = QuoteStatus(rawCurrencyId = params.rawCurrencyId) storeQuote.emit(value = setOf(status)) val values1 = getEmittedValues(flow = actual) @@ -120,11 +122,13 @@ internal class DefaultSingleQuoteProducerTest { @Test fun `test if flow throws exception`() = runTest { val exception = IllegalStateException() - val status = Quote.Value( + val status = QuoteStatus( rawCurrencyId = params.rawCurrencyId, - fiatRate = BigDecimal.ONE, - priceChange = BigDecimal.ZERO, - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + source = StatusSource.ACTUAL, + ), ) val innerFlow = MutableStateFlow(value = false) @@ -146,7 +150,7 @@ internal class DefaultSingleQuoteProducerTest { val values1 = getEmittedValues(flow = actual) Truth.assertThat(values1.size).isEqualTo(1) - val fallbackStatus = Quote.Empty(rawCurrencyId = params.rawCurrencyId) + val fallbackStatus = QuoteStatus(rawCurrencyId = params.rawCurrencyId) Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) innerFlow.emit(value = true) @@ -160,7 +164,7 @@ internal class DefaultSingleQuoteProducerTest { fun `test if flow doesn't contain network from params`() = runTest { val storeFlow = flowOf( setOf( - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), + QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), ), ) diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt new file mode 100644 index 0000000000..93b3bb2077 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreExtTest.kt @@ -0,0 +1,133 @@ +package com.tangem.data.quotes.store + +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.data.quote.toDomain +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal +import kotlin.properties.Delegates + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesStatusesStoreExtTest { + + private var runtimeStore: RuntimeSharedStore> by Delegates.notNull() + private var persistenceStore: MockStateDataStore by Delegates.notNull() + private var store: DefaultQuotesStatusesStore by Delegates.notNull() + + private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + private val btcQuote = btcQuoteDM.toDomain() + + @BeforeEach + fun resetMocks() { + runtimeStore = RuntimeSharedStore() + persistenceStore = MockStateDataStore(default = emptyMap()) + + store = DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetSourceAsCache { + + @ParameterizedTest + @ProvideTestModels + fun setSourceAsCache(model: SetSourceTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.setSourceAsCache(model.currenciesIds) + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + SetSourceTestModel( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = emptySet(), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.CACHE)), + ), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetSourceAsOnlyCache { + + @ParameterizedTest + @ProvideTestModels + fun setSourceAsOnlyCache(model: SetSourceTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.setSourceAsOnlyCache(model.currenciesIds) + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + SetSourceTestModel( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(QuoteStatus(btcQuote.rawCurrencyId)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + ), + SetSourceTestModel( + initialRuntime = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + currenciesIds = setOf(btcQuote.rawCurrencyId), + expected = setOf(btcQuoteDM.toDomain(source = StatusSource.ONLY_CACHE)), + ), + ) + } + + data class SetSourceTestModel( + val initialRuntime: Set?, + val currenciesIds: Set, + val expected: Set?, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt new file mode 100644 index 0000000000..b9b3c6ee60 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStatusesStoreTest.kt @@ -0,0 +1,565 @@ +package com.tangem.data.quotes.store + +import androidx.datastore.core.DataStore +import com.google.common.truth.Truth +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.data.quote.toDomain +import com.tangem.common.test.datastore.MockStateDataStore +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal +import kotlin.properties.Delegates + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesStatusesStoreTest { + + private var runtimeStore: RuntimeSharedStore> by Delegates.notNull() + private var persistenceStore: MockStateDataStore by Delegates.notNull() + private var store: DefaultQuotesStatusesStore by Delegates.notNull() + + // region Data models + private val btcQuoteDM = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) + private val ethQuoteDM = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) + // endregion + + // region Domain models + private val btcQuote = btcQuoteDM.toDomain() + private val ethQuote = ethQuoteDM.toDomain() + private val adaEmptyQuote = QuoteStatus(rawCurrencyId = CryptoCurrency.RawID(value = "ADA")) + // endregion + + @BeforeEach + fun resetMocks() { + runtimeStore = RuntimeSharedStore() + persistenceStore = MockStateDataStore(default = emptyMap()) + + store = DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Initialization { + + @Test + fun `initialization if cache store is empty`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore: DataStore = mockk() + + every { persistenceStore.data } returns emptyFlow() + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `initialization if cache store contains empty map`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = null + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `initialization if cache store is not empty`() = runTest { + // Arrange + val runtimeStore = RuntimeSharedStore>() + val persistenceStore = MockStateDataStore(default = emptyMap()) + + persistenceStore.updateData { + it.toMutableMap().apply { + this += btcQuoteDM + this += ethQuoteDM + } + } + + // Act + DefaultQuotesStatusesStore( + runtimeStore = runtimeStore, + persistenceDataStore = persistenceStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + val actual = runtimeStore.getSyncOrNull() + + // Assert + val expected = setOf( + btcQuoteDM.toDomain(source = StatusSource.CACHE), + ethQuoteDM.toDomain(source = StatusSource.CACHE), + ) + + Truth.assertThat(actual).isEqualTo(expected) + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Get { + + @ParameterizedTest + @ProvideTestModels + fun get(model: GetTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + val actual = getEmittedValues(flow = store.get()) + + // Assert + val expected = listOfNotNull(model.expected) + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetTestModel(initialRuntime = null, expected = null), + GetTestModel(initialRuntime = emptySet(), expected = emptySet()), + GetTestModel(initialRuntime = setOf(btcQuote, ethQuote), expected = setOf(btcQuote, ethQuote)), + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetAllSyncOrNull { + + @ParameterizedTest + @ProvideTestModels + fun getAllSyncOrNull(model: GetTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetTestModel(initialRuntime = null, expected = null), + GetTestModel(initialRuntime = emptySet(), expected = emptySet()), + GetTestModel(initialRuntime = setOf(btcQuote, ethQuote), expected = setOf(btcQuote, ethQuote)), + ) + } + + data class GetTestModel(val initialRuntime: Set?, val expected: Set?) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SingleUpdateStatusSource { + + @ParameterizedTest + @ProvideTestModels + fun updateStatusSource(model: UpdateStatusSourceModel.Single) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.updateStatusSource( + currencyId = model.currencyId, + source = model.source, + ifNotFound = model.ifNotFound, + ) + + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // region runtime store is null + UpdateStatusSourceModel.Single( + initialRuntime = null, + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Single( + initialRuntime = null, + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store is empty + UpdateStatusSourceModel.Single( + initialRuntime = emptySet(), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Single( + initialRuntime = emptySet(), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store contains statuses + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote, adaEmptyQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.CACHE)), + ethQuote, + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ONLY_CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.ONLY_CACHE)), + ethQuote, + ), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote, ethQuote), + currencyId = btcQuote.rawCurrencyId, + source = StatusSource.ACTUAL, + expected = setOf(btcQuote, ethQuote), + ), + UpdateStatusSourceModel.Single( + initialRuntime = setOf(btcQuote), + currencyId = adaEmptyQuote.rawCurrencyId, + source = StatusSource.ACTUAL, + ifNotFound = ::QuoteStatus, + expected = setOf(btcQuote, adaEmptyQuote), + ), + // endregion + ) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class MultiUpdateStatusSource { + + @ParameterizedTest + @ProvideTestModels + fun updateStatusSource(model: UpdateStatusSourceModel.Multi) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + // Act + store.updateStatusSource( + currenciesIds = model.currenciesIds, + source = model.source, + ifNotFound = model.ifNotFound, + ) + + val actual = store.getAllSyncOrNull() + + // Assert + val expected = model.expected + + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + // region runtime store is null + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(), + source = StatusSource.CACHE, // never-mind + expected = null, + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf( + btcQuote.rawCurrencyId, + CryptoCurrency.RawID(value = "ETH"), + ), + source = StatusSource.ACTUAL, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = null, + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store is empty + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(), + source = StatusSource.CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf( + btcQuote.rawCurrencyId, + CryptoCurrency.RawID(value = "ETH"), + ), + source = StatusSource.ACTUAL, // never-mind + expected = emptySet(), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = emptySet(), + currenciesIds = setOf(btcQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, // never-mind + ifNotFound = ::QuoteStatus, + expected = setOf( + QuoteStatus(rawCurrencyId = btcQuote.rawCurrencyId), + ), + ), + // endregion + + // region runtime store contains statuses + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.CACHE)), + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ONLY_CACHE, + expected = setOf( + btcQuote.copy(value = btcQuote.value.copySealed(source = StatusSource.ONLY_CACHE)), + adaEmptyQuote, + ), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote, adaEmptyQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + expected = setOf(btcQuote, adaEmptyQuote), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + expected = setOf(btcQuote), + ), + UpdateStatusSourceModel.Multi( + initialRuntime = setOf(btcQuote), + currenciesIds = setOf(btcQuote.rawCurrencyId, adaEmptyQuote.rawCurrencyId), + source = StatusSource.ACTUAL, + ifNotFound = ::QuoteStatus, + expected = setOf(btcQuote, adaEmptyQuote), + ), + // endregion + ) + } + + sealed interface UpdateStatusSourceModel { + + val initialRuntime: Set? + val source: StatusSource + val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? + val expected: Set? + + data class Single( + override val initialRuntime: Set?, + val currencyId: CryptoCurrency.RawID, + override val source: StatusSource, + override val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + override val expected: Set?, + ) : UpdateStatusSourceModel + + data class Multi( + override val initialRuntime: Set?, + val currenciesIds: Set, + override val source: StatusSource, + override val ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus? = { null }, + override val expected: Set?, + ) : UpdateStatusSourceModel + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Store { + + @ParameterizedTest + @ProvideTestModels + fun store(model: StoreTestModel) = runTest { + // Arrange + if (model.initialRuntime != null) { + runtimeStore.store(value = model.initialRuntime) + } + + if (model.initialPersistence != null) { + persistenceStore.updateData { model.initialPersistence } + } + + // Act + store.store(values = model.values) + + val runtimeActual = runtimeStore.getSyncOrNull() + val persistenceActual = getEmittedValues(persistenceStore.data) + + // Assert + val runtimeExpected = model.runtimeExpected + val persistenceExpected = listOf(model.persistenceExpected) + + Truth.assertThat(runtimeActual).isEqualTo(runtimeExpected) + Truth.assertThat(persistenceActual).isEqualTo(persistenceExpected) + } + + private fun provideTestModels() = listOf( + // region stores are null + StoreTestModel( + initialRuntime = null, + initialPersistence = null, + values = emptyMap(), + runtimeExpected = null, + persistenceExpected = emptyMap(), + ), + StoreTestModel( + initialRuntime = null, + initialPersistence = null, + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + // endregion + + // region persistence store is null + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = null, + values = emptyMap(), + runtimeExpected = setOf(btcQuote), + persistenceExpected = emptyMap(), + ), + StoreTestModel( + initialRuntime = setOf(ethQuote), + initialPersistence = null, + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(ethQuote, btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + // endregion + + // region runtime store is null + StoreTestModel( + initialRuntime = null, + initialPersistence = mapOf(btcQuoteDM), + values = emptyMap(), + runtimeExpected = null, + persistenceExpected = mapOf(btcQuoteDM), + ), + StoreTestModel( + initialRuntime = null, + initialPersistence = mapOf(ethQuoteDM), + values = mapOf(btcQuoteDM), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM), + ), + // endregion + + // region stores contain data + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = mapOf(btcQuoteDM), + values = emptyMap(), + runtimeExpected = setOf(btcQuote), + persistenceExpected = mapOf(btcQuoteDM), + ), + StoreTestModel( + initialRuntime = setOf(btcQuote), + initialPersistence = mapOf(btcQuoteDM), + values = mapOf(ethQuoteDM), + runtimeExpected = setOf(ethQuote, btcQuote), + persistenceExpected = mapOf(ethQuoteDM, btcQuoteDM), + ), + // endregion + ) + } + + data class StoreTestModel( + val initialRuntime: Set?, + val initialPersistence: CurrencyIdWithQuote?, + val values: CurrencyIdWithQuote, + val persistenceExpected: CurrencyIdWithQuote?, + val runtimeExpected: Set?, + ) +} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt deleted file mode 100644 index d5a517f5e2..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreGetMethodTest.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.tangem.data.quotes.store - -import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.common.test.data.quote.toDomain -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.common.test.utils.getEmittedValues -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -internal class QuotesStoreGetMethodTest { - - private val runtimeStore = RuntimeSharedStore>() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `test get if runtime store is empty`() = runTest { - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(emptyList>()) - } - - @Test - fun `test get if runtime store contains empty set`() = runTest { - runtimeStore.store(value = emptySet()) - - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values).isEqualTo(listOf(emptySet())) - } - - @Test - fun `test get if runtime store is not empty`() = runTest { - val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) - val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) - - runtimeStore.store(value = setOf(btcQuote.toDomain(), ethQuote.toDomain())) - - val actual = store.get() - - val values = getEmittedValues(flow = actual) - - Truth.assertThat(values.size).isEqualTo(1) - Truth.assertThat(values).isEqualTo(listOf(setOf(btcQuote.toDomain(), ethQuote.toDomain()))) - } - - @Test - fun `test getAllSyncOrNull if runtime store is empty`() = runTest { - val actual = store.getAllSyncOrNull() - - Truth.assertThat(actual).isEqualTo(null) - } - - @Test - fun `test getAllSyncOrNull if runtime store contains empty set`() = runTest { - runtimeStore.store(value = emptySet()) - - val actual = store.getAllSyncOrNull() - - Truth.assertThat(actual).isEqualTo(emptySet()) - } - - @Test - fun `test getAllSyncOrNull if runtime store is not empty`() = runTest { - val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) - val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) - - runtimeStore.store(value = setOf(btcQuote.toDomain(), ethQuote.toDomain())) - - val actual = store.getAllSyncOrNull() - - val expected = setOf(btcQuote.toDomain(), ethQuote.toDomain()) - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt deleted file mode 100644 index 2452a66004..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreInitializationTest.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.data.quotes.store - -import androidx.datastore.core.DataStore -import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.common.test.data.quote.toDomain -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.StatusSource -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -internal class QuotesStoreInitializationTest { - - @Test - fun `test initialization if cache store is empty`() = runTest { - val runtimeStore = RuntimeSharedStore>() - val persistenceStore: DataStore = mockk() - - every { persistenceStore.data } returns emptyFlow() - - DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null) - } - - @Test - fun `test initialization if cache store contains empty map`() = runTest { - val runtimeStore = RuntimeSharedStore>() - val persistenceStore = MockStateDataStore(default = emptyMap()) - - DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(null) - } - - @Test - fun `test initialization if cache store is not empty`() = runTest { - val runtimeStore = RuntimeSharedStore>() - val persistenceStore = MockStateDataStore(default = emptyMap()) - - val btcQuote = "BTC" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ZERO) - val ethQuote = "ETH" to MockQuoteResponseFactory.createSinglePrice(BigDecimal.ONE) - - persistenceStore.updateData { - it.toMutableMap().apply { - this += btcQuote - this += ethQuote - } - } - - DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - val expected = setOf( - btcQuote.toDomain(source = StatusSource.CACHE), - ethQuote.toDomain(source = StatusSource.CACHE), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt deleted file mode 100644 index e935753dcf..0000000000 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.data.quotes.store - -import com.google.common.truth.Truth -import com.tangem.common.test.data.quote.MockQuoteResponseFactory -import com.tangem.common.test.data.quote.toDomain -import com.tangem.common.test.datastore.MockStateDataStore -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.test.runTest -import org.junit.Test -import java.math.BigDecimal - -/** -[REDACTED_AUTHOR] - */ -internal class QuotesStoreUpdateMethodsTest { - - private val runtimeStore = RuntimeSharedStore>() - private val persistenceStore = MockStateDataStore(default = emptyMap()) - - private val store = DefaultQuotesStoreV2( - runtimeStore = runtimeStore, - persistenceDataStore = persistenceStore, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `refresh if runtime store is empty`() = runTest { - val currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ) - - store.refresh(currenciesIds = currenciesIds) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet()) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `refresh if runtime store contains quote with this id`() = runTest { - val quote = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) - .toDomain(rawCurrencyId = "BTC", source = StatusSource.ACTUAL) - - runtimeStore.store(value = setOf(quote)) - - store.refresh(currenciesIds = setOf(quote.rawCurrencyId)) - - val runtimeExpected = setOf(quote.copySealed(source = StatusSource.CACHE)) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store actual if runtime and cache stores contain quotes with this id`() = runTest { - val prevStatus = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) - - runtimeStore.store( - value = setOf( - prevStatus.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), - ), - ) - - persistenceStore.updateData { - it.toMutableMap().apply { - put("BTC", prevStatus) - } - } - - val newStatus = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.TEN) - - store.storeActual(values = mapOf("BTC" to newStatus)) - - val runtimeExpected = setOf( - newStatus.toDomain(rawCurrencyId = "BTC", source = StatusSource.ACTUAL), - ) - val persistenceExpected = mapOf("BTC" to newStatus) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(persistenceExpected) - } - - @Test - fun `store error if runtime store is empty`() = runTest { - val currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ) - - store.storeError(currenciesIds = currenciesIds) - - val runtimeExpected = currenciesIds.map(Quote::Empty).toSet() - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } - - @Test - fun `store error if runtime store contains status with this network`() = runTest { - val status = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE) - - runtimeStore.store( - value = setOf( - status.toDomain(rawCurrencyId = "BTC", source = StatusSource.CACHE), - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), - ), - ) - - store.storeError( - currenciesIds = setOf( - CryptoCurrency.RawID(value = "BTC"), - CryptoCurrency.RawID(value = "ETH"), - ), - ) - - val runtimeExpected = setOf( - status.toDomain(rawCurrencyId = "BTC", source = StatusSource.ONLY_CACHE), - Quote.Empty(rawCurrencyId = CryptoCurrency.RawID(value = "ETH")), - ) - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) - Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) - } -} \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt new file mode 100644 index 0000000000..3a4f3abb14 --- /dev/null +++ b/data/quotes/src/test/java/com/tangem/data/quotes/utils/QuotesUnsupportedCurrenciesIdAdapterTest.kt @@ -0,0 +1,144 @@ +package com.tangem.data.quotes.utils + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.compatibility.l2BlockchainsCoinIds +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.common.test.data.quote.MockQuoteResponseFactory +import com.tangem.common.test.utils.ProvideTestModels +import com.tangem.data.quotes.utils.QuotesUnsupportedCurrenciesIdAdapter.ReplacementResult +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.datasource.api.tangemTech.models.QuotesResponse.Quote +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class QuotesUnsupportedCurrenciesIdAdapterTest { + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceUnsupportedCurrencies { + + @ParameterizedTest + @ProvideTestModels + fun replaceUnsupportedCurrencies(model: ReplaceUnsupportedCurrenciesModel) { + // Act + val actual = QuotesUnsupportedCurrenciesIdAdapter.replaceUnsupportedCurrencies( + currenciesIds = model.currenciesIds, + ) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + ReplaceUnsupportedCurrenciesModel( + currenciesIds = l2BlockchainsCoinIds.toSet(), + expected = ReplacementResult( + idsForRequest = l2BlockchainsCoinIds.mapTo(hashSetOf()) { + Blockchain.Ethereum.toCoinId() + }, + idsFiltered = l2BlockchainsCoinIds.toSet(), + ), + ), + ReplaceUnsupportedCurrenciesModel( + currenciesIds = notL2BlockchainsCoinIds, + expected = ReplacementResult(idsForRequest = notL2BlockchainsCoinIds, idsFiltered = emptySet()), + ), + ReplaceUnsupportedCurrenciesModel( + currenciesIds = blockchainCoinIds, + expected = ReplacementResult( + idsForRequest = Blockchain.entries + .filterNot(Blockchain::isTestnet) + .mapTo(hashSetOf()) { + if (it.isL2EthereumNetwork()) { + Blockchain.Ethereum.toCoinId() + } else { + it.toCoinId() + } + }, + idsFiltered = l2BlockchainsCoinIds.toSet(), + ), + ), + ) + } + + data class ReplaceUnsupportedCurrenciesModel( + val currenciesIds: Set, + val expected: ReplacementResult, + ) + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetResponseWithUnsupportedCurrencies { + + private val zeroQuote = MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ZERO) + private val ethQuote = "ethereum" to zeroQuote + + @ParameterizedTest + @ProvideTestModels + fun getResponseWithUnsupportedCurrencies(model: GetResponseWithUnsupportedCurrenciesModel) { + // Act + val actual = QuotesUnsupportedCurrenciesIdAdapter.getResponseWithUnsupportedCurrencies( + response = model.response, + filteredIds = model.filteredIds, + ) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetResponseWithUnsupportedCurrenciesModel( + response = QuotesResponse(quotes = emptyMap()), + filteredIds = emptySet(), + expected = QuotesResponse(quotes = emptyMap()), + ), + GetResponseWithUnsupportedCurrenciesModel( + response = createQuotesResponse(ethQuote), + filteredIds = emptySet(), + expected = createQuotesResponse(ethQuote), + ), + GetResponseWithUnsupportedCurrenciesModel( + response = createQuotesResponse( + "arbitrum-one" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ethQuote, + ), + filteredIds = blockchainCoinIds, + expected = createQuotesResponse( + *l2BlockchainsCoinIds.map { it to zeroQuote }.toTypedArray(), + "arbitrum-one" to MockQuoteResponseFactory.createSinglePrice(value = BigDecimal.ONE), + ethQuote, + ), + ), + ) + + private fun createQuotesResponse(vararg quotes: Pair): QuotesResponse { + return QuotesResponse(quotes = quotes.toMap()) + } + } + + data class GetResponseWithUnsupportedCurrenciesModel( + val response: QuotesResponse, + val filteredIds: Set, + val expected: QuotesResponse, + ) + + private companion object { + + val blockchainCoinIds = Blockchain.entries + .filterNot(Blockchain::isTestnet) + .mapTo(destination = hashSetOf(), transform = Blockchain::toCoinId) + + val notL2BlockchainsCoinIds = Blockchain.entries + .filterNot { it.isTestnet() || it.isL2EthereumNetwork() } + .mapTo(hashSetOf()) { it.toCoinId() } + } +} \ No newline at end of file diff --git a/features/fee-selector/impl/.gitignore b/data/swap/.gitignore similarity index 100% rename from features/fee-selector/impl/.gitignore rename to data/swap/.gitignore diff --git a/data/swap/build.gradle.kts b/data/swap/build.gradle.kts new file mode 100644 index 0000000000..26eb11f40e --- /dev/null +++ b/data/swap/build.gradle.kts @@ -0,0 +1,54 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.data.swap" +} + +dependencies { + /** Core */ + implementation(projects.core.datasource) + implementation(projects.core.utils) + + /** Data */ + implementation(projects.data.common) + implementation(projects.data.express) + + /** Domain */ + implementation(projects.domain.express.models) + implementation(projects.domain.express) + implementation(projects.domain.swap.models) + implementation(projects.domain.swap) + implementation(projects.domain.wallets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + + /** Tangem SDK */ + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Libs */ + implementation(projects.libs.blockchainSdk) + + /** Other */ + implementation(deps.androidx.datastore) + implementation(deps.jodatime) + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt new file mode 100644 index 0000000000..8827a8bc7c --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt @@ -0,0 +1,19 @@ +package com.tangem.data.swap + +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.SwapErrorResolver + +internal class DefaultSwapErrorResolver( + private val expressErrorConverter: ExpressErrorConverter, +) : SwapErrorResolver { + override fun resolve(throwable: Throwable): ExpressError { + return when (throwable) { + is ApiResponseError.HttpException -> { + expressErrorConverter.convert(throwable.errorBody.orEmpty()) + } + else -> ExpressError.UnknownError + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt new file mode 100644 index 0000000000..1931f346f2 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -0,0 +1,200 @@ +package com.tangem.data.swap + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.swap.converter.TokenInfoConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class DefaultSwapRepositoryV2 @Inject constructor( + private val tangemExpressApi: TangemExpressApi, + private val expressRepository: ExpressRepository, + private val coroutineDispatcher: CoroutineDispatcherProvider, + private val appPreferencesStore: AppPreferencesStore, + private val currencyStatusOperations: BaseCurrencyStatusOperations, +) : SwapRepositoryV2 { + + private val tokenInfoConverter = TokenInfoConverter() + override suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ): List = withContext(coroutineDispatcher.io) { + val cryptoCurrencyList = cryptoCurrencyStatusList.map { it.currency } + + val allPairs = getPairsInternal( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + val providers = expressRepository.getProviders(userWallet = userWallet) + val mappedProviders = providers.associateBy(ExpressProvider::providerId) + + allPairs.map { pair -> + async { + val statusFrom = cryptoCurrencyStatusList + .firstOrNull { + it.currency.getContractAddress() == pair.from.contractAddress && + it.currency.network.backendId == pair.from.network + } + val statusTo = cryptoCurrencyStatusList + .firstOrNull { + it.currency.getContractAddress() == pair.to.contractAddress && + it.currency.network.backendId == pair.to.network + } + + if (statusFrom != null && statusTo != null) { + SwapPairModel( + from = statusFrom, + to = statusTo, + providers = pair.providers.mapNotNull { + mappedProviders[it.providerId] + }, + ) + } else { + null + } + } + }.awaitAll().filterNotNull() + } + + override suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ): List = withContext(coroutineDispatcher.io) { + val allPairs = getPairsInternal( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + allPairs.map { pair -> + async { + val statusFromDeferred = async { + cryptoCurrencyList + .firstOrNull { + it.getContractAddress() == pair.from.contractAddress && + it.network.backendId == pair.from.network + } + } + val statusToDeferred = async { + cryptoCurrencyList + .firstOrNull { + it.getContractAddress() == pair.to.contractAddress && + it.network.backendId == pair.to.network + } + } + + createPairModelOnly( + currencyFrom = statusFromDeferred.await(), + currencyTo = statusToDeferred.await(), + userWalletId = userWallet.walletId, + ) + } + }.awaitAll().filterNotNull() + } + + private suspend fun CoroutineScope.getPairsInternal( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ) = awaitAll( + // original pairs + async { + invokePairRequest( + userWallet = userWallet, + from = arrayListOf(initialCurrency), + to = cryptoCurrencyList, + ) + }, + // reversed pairs + async { + invokePairRequest( + userWallet = userWallet, + from = cryptoCurrencyList, + to = arrayListOf(initialCurrency), + ) + }, + ).flatten() + + private suspend fun invokePairRequest( + userWallet: UserWallet, + from: List, + to: List, + ) = safeApiCall( + call = { + tangemExpressApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = PairsRequestBody( + from = tokenInfoConverter.convertList(from), + to = tokenInfoConverter.convertList(to), + ), + ).getOrThrow() + }, + onError = { + Timber.w(it, "Unable to get pairs") + throw it + }, + ) + + private suspend fun CoroutineScope.createPairModelOnly( + currencyFrom: CryptoCurrency?, + currencyTo: CryptoCurrency?, + userWalletId: UserWalletId, + ): SwapPairModel? { + return if (currencyFrom != null && currencyTo != null) { + val statusFrom = currencyStatusOperations.getCurrencyStatusSync( + userWalletId = userWalletId, + cryptoCurrencyId = currencyFrom.id, + ).getOrNull() + val statusTo = currencyStatusOperations.getCurrencyStatusSync( + userWalletId = userWalletId, + cryptoCurrencyId = currencyTo.id, + ).getOrNull() + + if (statusFrom != null && statusTo != null) { + SwapPairModel( + from = statusFrom, + to = statusTo, + providers = emptyList(), + ) + } else { + null + } + } else { + null + } + } + + private fun CryptoCurrency.getContractAddress(): String { + return when (this) { + is CryptoCurrency.Token -> this.contractAddress + is CryptoCurrency.Coin -> "0" + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt new file mode 100644 index 0000000000..04e3203f28 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.swap.converter + +import com.tangem.datasource.api.express.models.request.LeastTokenInfo +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.TokenInfo +import com.tangem.utils.converter.Converter + +class TokenInfoConverter : Converter { + + override fun convert(value: CryptoCurrency): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = value.network.backendId, + ) + } + + fun convert(value: TokenInfo): LeastTokenInfo { + return LeastTokenInfo( + contractAddress = value.contractAddress, + network = value.network, + ) + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt new file mode 100644 index 0000000000..0ee1971b25 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -0,0 +1,52 @@ +package com.tangem.data.swap.di + +import com.squareup.moshi.Moshi +import com.tangem.data.express.converter.ExpressErrorConverter +import com.tangem.data.swap.DefaultSwapErrorResolver +import com.tangem.data.swap.DefaultSwapRepositoryV2 +import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.express.ExpressRepository +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SwapDataModule { + + @Provides + @Singleton + fun provideSwapErrorResolver(@NetworkMoshi moshi: Moshi): SwapErrorResolver { + val jsonAdapter = moshi.adapter(ExpressErrorResponse::class.java) + return DefaultSwapErrorResolver( + ExpressErrorConverter(jsonAdapter), + ) + } + + @Provides + @Singleton + fun provideSwapRepository( + tangemExpressApi: TangemExpressApi, + expressRepository: ExpressRepository, + coroutineDispatcher: CoroutineDispatcherProvider, + appPreferencesStore: AppPreferencesStore, + currencyStatusOperations: BaseCurrencyStatusOperations, + ): SwapRepositoryV2 { + return DefaultSwapRepositoryV2( + tangemExpressApi = tangemExpressApi, + expressRepository = expressRepository, + coroutineDispatcher = coroutineDispatcher, + appPreferencesStore = appPreferencesStore, + currencyStatusOperations = currencyStatusOperations, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index a0b6429f32..b0f2b5e284 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -47,12 +47,14 @@ internal class DefaultVisaAuthRepository @Inject constructor( } override suspend fun getCardWalletAuthChallenge( + cardId: String, cardWalletAddress: String, ): Either = withContext(dispatchers.io) { request { visaAuthApi.generateNonceByCardWallet( GenerateNoneByCardWalletRequest( cardWalletAddress = cardWalletAddress, + cardId = cardId, ), ).getOrThrow() }.map { response -> @@ -82,6 +84,7 @@ internal class DefaultVisaAuthRepository @Inject constructor( GetAccessTokenByCardWalletRequest( sessionId = signedChallenge.challenge.session.sessionId, signature = signedChallenge.signature, + salt = signedChallenge.salt, ), ).getOrThrow() } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index f5ef109750..9a4e8e8d7c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -119,7 +119,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emitAll(delegate.invoke(transactionData)) } - override suspend fun getAmount(): Amount? { + override fun getAmount(): Amount? { return approvalAmount } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt index e372f35b8a..2726cb8389 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTransactionUseCase.kt @@ -127,7 +127,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( return dAppFee } - override suspend fun getAmount(): Amount? { + override fun getAmount(): Amount? { return approvalAmount } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt index fdffc8a26d..a2fc3d4224 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -49,7 +49,9 @@ internal object WcEthTxHelper { .hexToBigDecimal() .movePointLeft(blockchain.decimals()) - val callData = CompiledSmartContractCallData(txParams.data.removePrefix(HEX_PREFIX).hexToBytes()) + val callData = txParams.data?.removePrefix(HEX_PREFIX)?.hexToBytes()?.let { + CompiledSmartContractCallData(it) + } return TransactionData.Uncompiled( amount = Amount(value, blockchain), fee = dAppFee, @@ -62,10 +64,10 @@ internal object WcEthTxHelper { ) } - fun getApprovedAmount(txData: String, result: CheckTransactionResult): ApprovedAmount? { + fun getApprovedAmount(txData: String?, result: CheckTransactionResult): ApprovedAmount? { val approvalMethodId = ApprovalERC20TokenCallData("", null).methodId - val isApprovalWcMethod = txData.startsWith(approvalMethodId) - if (!isApprovalWcMethod) return null + val isApprovalWcMethod = txData?.startsWith(approvalMethodId) + if (isApprovalWcMethod != true) return null val simulation = result.simulation as? SimulationResult.Success ?: return null val approves = (simulation.data as? SimulationData.Approve)?.approvedAmounts diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index 7208eea4c4..808d399dab 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -28,7 +28,7 @@ internal class AssociateNetworksDelegate( } } - @Throws(WcPairError.UnsupportedNetworks::class) + @Throws(WcPairError.UnsupportedBlockchains::class) suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() @@ -74,7 +74,7 @@ internal class AssociateNetworksDelegate( notAdded.add(wcNetwork) } } - if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedNetworks(unknownRequired) + if (unknownRequired.isNotEmpty()) throw WcPairError.UnsupportedBlockchains(unknownRequired) return ProposalNetwork( wallet = wallet, missingRequired = missingRequired, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index fc52444c3b..77b7c18e93 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -56,7 +56,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( // check unsupported dApps, just local constant for now, finish if unsupported if (sdkSessionProposal.name in unsupportedDApps) { Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") - val error = WcPairState.Error(WcPairError.UnsupportedDApp) + val error = WcPairState.Error(WcPairError.UnsupportedDomain) emit(error) return@flow } @@ -104,7 +104,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ) newSession }.onLeft { - analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.message)) + analytics.send(WcAnalyticEvents.DAppConnectionFailed(it.code)) Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") } emit(WcPairState.Approving.Result(sessionForApprove, either)) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 54f931d604..1a72488035 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -27,7 +27,11 @@ internal class WcPairSdkDelegate : WcSdkObserver { val pairCall = async { sdkPair(url) } val proposal = async { proposalCallback() } - pairCall.await().map { proposal.await() } + pairCall.await().onLeft { + proposal.cancel() + return@coroutineScope it.left() + } + proposal.await().right() } suspend fun approve( @@ -40,11 +44,14 @@ internal class WcPairSdkDelegate : WcSdkObserver { val approveCall = async { sdkApprove(sessionApprove) } val approveCallback = async { approveCallback() } approveCall.await() - .onLeft { return@coroutineScope it.left() } + .onLeft { + approveCallback.cancel() + return@coroutineScope it.left() + } when (val result = approveCallback.await()) { is Wallet.Model.SettledSessionResponse.Result -> result.right() is Wallet.Model.SettledSessionResponse.Error -> - WcPairError.ExternalApprovalError(result.errorMessage).left() + WcPairError.ApprovalFailed(result.errorMessage).left() } } @@ -77,7 +84,7 @@ internal class WcPairSdkDelegate : WcSdkObserver { WalletKit.approveSession( params = sessionApprove, onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toPairError()) }, + onError = { continuation.resume(it.throwable.toApproveError()) }, ) } } @@ -87,10 +94,20 @@ internal class WcPairSdkDelegate : WcSdkObserver { WalletKit.pair( params = Wallet.Params.Pair(uri), onSuccess = { continuation.resume(Unit.right()) }, - onError = { continuation.resume(it.throwable.toPairError()) }, + onError = { continuation.resume(it.throwable.toPairError().left()) }, ) } } - private fun Throwable.toPairError() = WcPairError.ExternalApprovalError(this.localizedMessage.orEmpty()).left() + private fun Throwable.toPairError() = when { + pairingExpiredMessages.any { message.orEmpty().contains(it) } -> WcPairError.UriAlreadyUsed(message.orEmpty()) + else -> WcPairError.PairingFailed(this.localizedMessage.orEmpty()) + } + + private fun Throwable.toApproveError() = WcPairError.ApprovalFailed(this.localizedMessage.orEmpty()).left() + + companion object { + // com.reown.android.pairing.engine.domain.PairingEngine.pair + private val pairingExpiredMessages = listOf("Pairing URI expired", "Pairing expired") + } } \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index bd8e58f43b..e493e6b602 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -196,7 +196,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `success pair and reject unsupported dApp`() = runTest { coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() - val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) + val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDomain) val useCase = useCaseFactory() useCase.invoke().test { @@ -211,7 +211,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `complete on pair error`() = runTest { - val error = WcPairError.ExternalApprovalError("error") + val error = WcPairError.PairingFailed("error") coEvery { sdkDelegate.pair(url) } returns error.left() val errorState = WcPairState.Error(error) @@ -229,7 +229,7 @@ internal class DefaultWcPairUseCaseTest { @Test fun `complete on approve error`() = runTest { val approveLoading = WcPairState.Approving.Loading(sessionForApprove) - val error = WcPairError.ExternalApprovalError("error").left() + val error = WcPairError.ApprovalFailed("error").left() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.approve(sdkApprove) } returns error coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } diff --git a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt index d7244d6023..6080ac8921 100644 --- a/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt +++ b/domain/blockaid/models/src/main/kotlin/com/domain/blockaid/models/transaction/CheckTransactionResult.kt @@ -8,5 +8,6 @@ package com.domain.blockaid.models.transaction */ data class CheckTransactionResult( val validation: ValidationResult, + val description: String? = null, val simulation: SimulationResult, ) \ No newline at end of file diff --git a/domain/express/.gitignore b/domain/express/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/express/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/express/build.gradle.kts b/domain/express/build.gradle.kts new file mode 100644 index 0000000000..b5a4095dac --- /dev/null +++ b/domain/express/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.express.models" +} + +dependencies { + /** Core */ + implementation(projects.core.utils) + implementation(projects.core.datasource) + + /** Domain */ + implementation(projects.domain.express.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + + /** Other */ + implementation(deps.moshi.adapters) +} \ No newline at end of file diff --git a/domain/express/models/.gitignore b/domain/express/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/express/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/express/models/build.gradle.kts b/domain/express/models/build.gradle.kts new file mode 100644 index 0000000000..a7e2520b52 --- /dev/null +++ b/domain/express/models/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + /** Domain */ + implementation(projects.domain.tokens.models) + + /* Other */ + implementation(deps.moshi.adapters) +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt new file mode 100644 index 0000000000..7d153e9bf1 --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressError.kt @@ -0,0 +1,64 @@ +package com.tangem.domain.express.models + +import java.math.BigDecimal + +sealed class ExpressError { + + abstract val code: Int + + data class DataError( + override val code: Int, + val description: String?, + ) : ExpressError() + + data class BadRequest(override val code: Int) : ExpressError() + + data class Forbidden(override val code: Int) : ExpressError() + + data class InternalError(override val code: Int) : ExpressError() + + data class ProviderNotFoundError(override val code: Int) : ExpressError() + + data class ProviderNotActiveError(override val code: Int) : ExpressError() + + data class ProviderNotAvailableError(override val code: Int) : ExpressError() + + data class ProviderInternalError(override val code: Int) : ExpressError() + + data class ExchangeNotPossibleError(override val code: Int) : ExpressError() + + data class NotEnoughBalanceError(override val code: Int) : ExpressError() + + data class InvalidAddressError(override val code: Int) : ExpressError() + + sealed class AmountError : ExpressError() { + abstract val amount: BigDecimal + + data class TooSmallError(override val code: Int, override val amount: BigDecimal) : AmountError() + data class TooBigError(override val code: Int, override val amount: BigDecimal) : AmountError() + data class NotEnoughAllowanceError(override val code: Int, override val amount: BigDecimal) : AmountError() + } + + data class ProviderDifferentAmountError( + override val code: Int, + val fromAmount: BigDecimal, + val fromProviderAmount: BigDecimal, + val decimals: Int, + ) : ExpressError() + + data class InvalidFromDecimalsError( + override val code: Int, + val receivedFromDecimals: Int, + val expressFromDecimals: Int, + ) : ExpressError() + + data class InvalidSignatureError(override val code: Int = 990) : ExpressError() + + data class InvalidRequestIdError(override val code: Int = 991) : ExpressError() + + data class InvalidPayoutAddressError(override val code: Int = 992) : ExpressError() + + data object UnknownError : ExpressError() { + override val code: Int = -1 + } +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt new file mode 100644 index 0000000000..9a26b89580 --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.express.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.math.BigDecimal + +/** + * Express provider data model + * + * @property providerId provider id + * @property rateTypes supported rate types + * @property name provider name + * @property type provider type + * @property imageLarge provider large logo image + * @property termsOfUse terms of use link + * @property privacyPolicy privacy policy link + * @property isRecommended flag that indicates if this provider is recommended + * @property slippage provider slippage + * + * Uses to store transaction data in datastore, when extends - should always add default value + * to support backward compatibility + */ +@JsonClass(generateAdapter = true) +data class ExpressProvider( + @Json(name = "providerId") + val providerId: String, + @Json(name = "rateTypes") + val rateTypes: List = emptyList(), + @Json(name = "name") + val name: String, + @Json(name = "type") + val type: ExpressProviderType, + @Json(name = "imageLarge") + val imageLarge: String, + @Json(name = "termsOfUse") + val termsOfUse: String?, + @Json(name = "privacyPolicy") + val privacyPolicy: String?, + @Json(name = "isRecommended") + val isRecommended: Boolean = false, + @Json(name = "slippage") + val slippage: BigDecimal?, +) \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt new file mode 100644 index 0000000000..3c77da80fb --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProviderType.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.express.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Express provider type + * + * @property typeName provider type name + */ +@JsonClass(generateAdapter = false) +enum class ExpressProviderType(val typeName: String) { + @Json(name = "DEX") + DEX(typeName = "DEX"), + + @Json(name = "CEX") + CEX(typeName = "CEX"), + + @Json(name = "DEX_BRIDGE") + DEX_BRIDGE(typeName = "DEX/Bridge"), + + @Json(name = "onramp") + ONRAMP(typeName = "ONRAMP"), +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt new file mode 100644 index 0000000000..4b9efd00b0 --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressRateType.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.express.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Rate type. + * + * Current implementation contains only float type, fixed will be supported later. + */ +@JsonClass(generateAdapter = false) +enum class ExpressRateType { + @Json(name = "FLOAT") + Float, + + @Json(name = "FIXED") + Fixed, +} \ No newline at end of file diff --git a/domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt b/domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt new file mode 100644 index 0000000000..05e981033d --- /dev/null +++ b/domain/express/src/main/java/com/tangem/domain/express/ExpressErrorResolver.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.express + +import com.tangem.domain.express.models.ExpressError + +/** + * Common express error resolver + */ +interface ExpressErrorResolver { + + fun resolve(throwable: Throwable): ExpressError +} \ No newline at end of file diff --git a/domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt b/domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt new file mode 100644 index 0000000000..d7c6518b46 --- /dev/null +++ b/domain/express/src/main/java/com/tangem/domain/express/ExpressRepository.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.express + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.wallets.models.UserWallet + +/** + * Express repository + */ +interface ExpressRepository { + + /** + * Returns list of express providers + * + * @param userWallet selected user wallet info + */ + suspend fun getProviders(userWallet: UserWallet): List +} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt index b2cedcf6e1..0d15310592 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt @@ -8,7 +8,7 @@ import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -24,7 +24,7 @@ class SaveManagedTokensUseCase( private val derivationsRepository: DerivationsRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -126,8 +126,8 @@ class SaveManagedTokensUseCase( } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, appCurrencyId = null, ), diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt index d146df5090..ec0e1717c6 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetCurrencyQuotesUseCase.kt @@ -4,29 +4,29 @@ import arrow.core.None import arrow.core.Option import arrow.core.toOption import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map class GetCurrencyQuotesUseCase( - private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, ) { // TODO apply interval parameter [REDACTED_TASK_KEY] operator fun invoke( currencyID: CryptoCurrency.ID, interval: PriceChangeInterval, refresh: Boolean, - ): Flow> { + ): Flow> { val rawId = currencyID.rawCurrencyId ?: return flowOf(None) - return singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + return singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawId), ) - .map { (it as? Quote.Value).toOption() } + .map { (it.value as? QuoteStatus.Data).toOption() } .catch { emit(None) } } } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt index 95750fda93..96a60f0cc0 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt @@ -6,7 +6,7 @@ import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles @@ -29,7 +29,7 @@ class SaveMarketTokensUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -107,8 +107,8 @@ class SaveMarketTokensUseCase( } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, appCurrencyId = null, ), diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt new file mode 100644 index 0000000000..1aad20046d --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt @@ -0,0 +1,69 @@ +package com.tangem.domain.models.quote + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigDecimal + +/** + * Represents the status of a specific currency quote + * + * @property rawCurrencyId the unique identifier of the cryptocurrency for which the financial information is provided + * @property value the specific status value + */ +data class QuoteStatus(val rawCurrencyId: CryptoCurrency.RawID, val value: Value) { + + /** Constructor for creating the [Empty] status of a specific currency quote */ + constructor(rawCurrencyId: CryptoCurrency.RawID) : this(rawCurrencyId = rawCurrencyId, value = Empty) + + /** Represents the various possible statuses of a quote */ + sealed interface Value { + + /** Status source */ + val source: StatusSource + + fun copySealed(source: StatusSource): Value { + return when (this) { + is Empty -> this + is Data -> copy(source = source) + } + } + } + + /** Represents unknown financial information for a specific cryptocurrency */ + data object Empty : Value { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and + * price change. + * + * @property source status source + * @property fiatRate the current fiat exchange rate for the cryptocurrency + * @property priceChange the price change for the cryptocurrency + */ + data class Data( + override val source: StatusSource, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, + ) : Value +} + +/** Applies the given [function] if [QuoteStatus.Value] is [QuoteStatus.Data] or does nothing */ +inline fun QuoteStatus.mapData(function: QuoteStatus.Data.() -> Unit): QuoteStatus { + if (value is QuoteStatus.Data) value.function() + + return this +} + +/** + * Applies the given [onData] or [onEmpty] functions depending on [QuoteStatus.Data] or [QuoteStatus.Empty] + * + * @param T type of result + */ +inline fun QuoteStatus.fold(onData: QuoteStatus.Data.() -> T, onEmpty: QuoteStatus.Empty.() -> T): T { + return when (value) { + is QuoteStatus.Data -> value.onData() + is QuoteStatus.Empty -> value.onEmpty() + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt index eaf7a5cdea..1193977eb9 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTPriceUseCase.kt @@ -3,11 +3,11 @@ package com.tangem.domain.nft import arrow.core.Either import com.tangem.domain.models.network.Network import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher class FetchNFTPriceUseCase( private val nftRepository: NFTRepository, - private val singleQuoteFetcher: SingleQuoteFetcher, + private val singleQuoteStatusFetcher: SingleQuoteStatusFetcher, ) { suspend operator fun invoke(network: Network, appCurrencyId: String?): Either { @@ -15,8 +15,8 @@ class FetchNFTPriceUseCase( val nftCurrency = nftRepository.getNFTCurrency(network) val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") - singleQuoteFetcher( - params = SingleQuoteFetcher.Params( + singleQuoteStatusFetcher( + params = SingleQuoteStatusFetcher.Params( rawCurrencyId = rawId, appCurrencyId = appCurrencyId, ), diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt index 4dc44a7590..a728ce7b73 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTPriceUseCase.kt @@ -1,19 +1,19 @@ package com.tangem.domain.nft import arrow.core.Either +import com.tangem.domain.models.quote.fold import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map class GetNFTPriceUseCase( private val nftRepository: NFTRepository, - private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, ) { suspend operator fun invoke(userWalletId: UserWalletId, nftAsset: NFTAsset): Either> { @@ -21,8 +21,8 @@ class GetNFTPriceUseCase( val nftCurrency = nftRepository.getNFTCurrency(nftAsset.network) val rawId = nftCurrency.id.rawCurrencyId ?: error("Invalid nft currency id") - singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = rawId), + singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawId), ).map { quote -> val nftPrice = nftRepository.getNFTSalePrice( userWalletId = userWalletId, @@ -31,12 +31,15 @@ class GetNFTPriceUseCase( assetId = nftAsset.id, ) - val quoteValue = quote as? Quote.Value - if (nftPrice !is NFTSalePrice.Value) { nftPrice } else { - nftPrice.copy(fiatValue = quoteValue?.fiatRate?.multiply(nftPrice.value)) + nftPrice.copy( + fiatValue = quote.fold( + onData = { fiatRate.multiply(nftPrice.value) }, + onEmpty = { null }, + ), + ) } } } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt index 389cc866e3..4d8415b915 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/HotCryptoCurrency.kt @@ -1,17 +1,17 @@ package com.tangem.domain.onramp.model import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus /** * Hot crypto currency * * @property cryptoCurrency crypto currency - * @property quote quote + * @property quoteStatus quote status * [REDACTED_AUTHOR] */ data class HotCryptoCurrency( val cryptoCurrency: CryptoCurrency, - val quote: Quote, + val quoteStatus: QuoteStatus, ) \ No newline at end of file diff --git a/domain/quotes/build.gradle.kts b/domain/quotes/build.gradle.kts index cc097e7300..27f51399d9 100644 --- a/domain/quotes/build.gradle.kts +++ b/domain/quotes/build.gradle.kts @@ -6,5 +6,4 @@ plugins { dependencies { api(projects.domain.core) api(projects.domain.models) - api(projects.domain.tokens.models) } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt similarity index 69% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt index f9d517cd42..9431c2b3f2 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepositoryV2.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt @@ -1,15 +1,15 @@ package com.tangem.domain.quotes import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus /** * Quotes repository * [REDACTED_AUTHOR] */ -interface QuotesRepositoryV2 { +interface QuotesRepository { /** Get quotes by [currenciesIds] synchronously or null */ - suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? + suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteStatusFetcher.kt similarity index 75% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteStatusFetcher.kt index d75234b93e..4f2ad37795 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteFetcher.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/multi/MultiQuoteStatusFetcher.kt @@ -4,11 +4,11 @@ import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.currency.CryptoCurrency /** - * Fetcher of quotes + * Fetcher of quotes statuses for specified list of [CryptoCurrency.RawID] * [REDACTED_AUTHOR] */ -interface MultiQuoteFetcher : FlowFetcher { +interface MultiQuoteStatusFetcher : FlowFetcher { /** * Params diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusFetcher.kt similarity index 61% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusFetcher.kt index 5b315bc6f8..062c864638 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteFetcher.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusFetcher.kt @@ -4,16 +4,17 @@ import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.currency.CryptoCurrency /** - * Fetcher of quote for [CryptoCurrency.RawID] + * Fetcher of quote for specified [CryptoCurrency.RawID] * [REDACTED_AUTHOR] */ -interface SingleQuoteFetcher : FlowFetcher { +interface SingleQuoteStatusFetcher : FlowFetcher { /** * Params * * @property rawCurrencyId crypto currency id + * @property appCurrencyId app currency id, if null then selected app currency will be used */ data class Params( val rawCurrencyId: CryptoCurrency.RawID, diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusProducer.kt similarity index 72% rename from domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt rename to domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusProducer.kt index c7fd024e28..81ff530551 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteProducer.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusProducer.kt @@ -2,16 +2,16 @@ package com.tangem.domain.quotes.single import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus /** * Producer of quote [CryptoCurrency.RawID] * [REDACTED_AUTHOR] */ -interface SingleQuoteProducer : FlowProducer { +interface SingleQuoteStatusProducer : FlowProducer { data class Params(val rawCurrencyId: CryptoCurrency.RawID) - interface Factory : FlowProducer.Factory + interface Factory : FlowProducer.Factory } \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt new file mode 100644 index 0000000000..a203be8468 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteStatusSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.quotes.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.quote.QuoteStatus + +/** + * Supplier of quote [SingleQuoteStatusProducer.Params] + * + * @property factory factory for creating [SingleQuoteStatusProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleQuoteStatusSupplier( + override val factory: SingleQuoteStatusProducer.Factory, + override val keyCreator: (SingleQuoteStatusProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt deleted file mode 100644 index e65dbb492d..0000000000 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/single/SingleQuoteSupplier.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.domain.quotes.single - -import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.tokens.model.Quote - -/** - * Supplier of quote [SingleQuoteProducer.Params] - * - * @property factory factory for creating [SingleQuoteProducer] - * @property keyCreator key creator - * -[REDACTED_AUTHOR] - */ -abstract class SingleQuoteSupplier( - override val factory: SingleQuoteProducer.Factory, - override val keyCreator: (SingleQuoteProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/swap/.gitignore b/domain/swap/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/swap/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/swap/build.gradle.kts b/domain/swap/build.gradle.kts new file mode 100644 index 0000000000..c0231df3f5 --- /dev/null +++ b/domain/swap/build.gradle.kts @@ -0,0 +1,31 @@ +import org.gradle.kotlin.dsl.projects + +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.swap" +} + + +dependencies { + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.express.models) + implementation(projects.domain.swap.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) + + /** Util */ + implementation(projects.core.utils) + + /** Other */ + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + +} \ No newline at end of file diff --git a/domain/swap/models/.gitignore b/domain/swap/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/swap/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/swap/models/build.gradle.kts b/domain/swap/models/build.gradle.kts new file mode 100644 index 0000000000..0b8ed0f521 --- /dev/null +++ b/domain/swap/models/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.swap.models" +} + +dependencies { + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.express.models) + implementation(projects.domain.tokens.models) + + /** Core */ + implementation(projects.core.datasource) + + /** Other */ + implementation(deps.jodatime) + implementation(deps.moshi) +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt new file mode 100644 index 0000000000..6a1f16be09 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +data class SwapCurrencies( + val fromGroup: SwapCurrenciesGroup, + val toGroup: SwapCurrenciesGroup, +) + +data class SwapCurrenciesGroup( + val available: List, + val unavailable: List, + val afterSearch: Boolean, +) + +data class SwapCryptoCurrency( + val currencyStatus: CryptoCurrencyStatus, + val providers: List, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt new file mode 100644 index 0000000000..5e8e0479f9 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapPairModel.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.swap.models + +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +/** + * Domain layer representation of SwapPair data network model. + * + * @property from Short information about the token we want to change + * @property to Short information about the token we want to exchange for + * @property providers Exchange providers + */ +data class SwapPairModel( + val from: CryptoCurrencyStatus, + val to: CryptoCurrencyStatus, + val providers: List, +) \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt new file mode 100644 index 0000000000..9bd09cc984 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/TokenInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.swap.models + +data class TokenInfo( + val contractAddress: String, + val network: String, +) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt new file mode 100644 index 0000000000..b9f75cb5c1 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapErrorResolver.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.swap + +import com.tangem.domain.express.models.ExpressError + +interface SwapErrorResolver { + fun resolve(throwable: Throwable): ExpressError +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt new file mode 100644 index 0000000000..b2c768cdc7 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.swap + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +interface SwapRepositoryV2 { + + /** + * Express swap pairs, both direct and reversed + * + * @param userWallet selected user wallet + * @param initialCurrency currency being swapped (either to or from) + * @param cryptoCurrencyStatusList list of currencies might be swapped + */ + suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyStatusList: List, + ): List + + /** Express getPairs request variant without providers request */ + suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ): List +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt new file mode 100644 index 0000000000..1954bc4f85 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -0,0 +1,85 @@ +package com.tangem.domain.swap.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapCryptoCurrency +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapCurrenciesGroup +import com.tangem.domain.swap.models.SwapPairModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +/** + * Returns pais + */ +class GetSwapSupportedPairsUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapErrorResolver: SwapErrorResolver, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + initialCurrency: CryptoCurrency, + cryptoCurrencyList: List, + ) = Either.catch { + val pairs = swapRepositoryV2.getPairsOnly( + userWallet = userWallet, + initialCurrency = initialCurrency, + cryptoCurrencyList = cryptoCurrencyList, + ) + + val filteredOutInitial = cryptoCurrencyList.filterNot { it.id == initialCurrency.id } + + val fromGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.from }, + groupingCurrency = { it.to }, + cryptoCurrencyList = filteredOutInitial, + ) + val toGroup = pairs.groupPairs( + initialCurrency = initialCurrency, + filteringCurrency = { it.to }, + groupingCurrency = { it.from }, + cryptoCurrencyList = filteredOutInitial, + ) + + SwapCurrencies( + fromGroup = fromGroup, + toGroup = toGroup, + ) + }.mapLeft(swapErrorResolver::resolve) + + private fun List.groupPairs( + initialCurrency: CryptoCurrency, + filteringCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + groupingCurrency: (SwapPairModel) -> CryptoCurrencyStatus, + cryptoCurrencyList: List, + ): SwapCurrenciesGroup { + val availableCryptoCurrencies = filter { pair -> filteringCurrency(pair).currency.id == initialCurrency.id } + // Search available to swap currency + .filter { pair -> + cryptoCurrencyList.any { currencyStatus -> + currencyStatus.id == pair.to.currency.id + } + }.map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } + + val unavailableCryptoCurrencies = + cryptoCurrencyList - availableCryptoCurrencies.map { it.currencyStatus.currency }.toSet() + + return SwapCurrenciesGroup( + available = availableCryptoCurrencies, + unavailable = unavailableCryptoCurrencies.map { currency -> + SwapCryptoCurrency( + CryptoCurrencyStatus( + currency = currency, + value = CryptoCurrencyStatus.Loading, // todo select token unavailable + ), + emptyList(), + ) + }, + afterSearch = false, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt deleted file mode 100644 index a92dfb0ab3..0000000000 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.domain.tokens.model - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.currency.CryptoCurrency -import java.math.BigDecimal - -sealed interface Quote { - - val rawCurrencyId: CryptoCurrency.RawID - - fun copySealed(source: StatusSource): Quote { - return when (this) { - is Empty -> this - is Value -> copy(source = source) - } - } - - /** - * Represents unknown financial information for a specific cryptocurrency. - * - * @property rawCurrencyId The raw cryptocurrency ID. - */ - data class Empty(override val rawCurrencyId: CryptoCurrency.RawID) : Quote - - /** - * Represents financial information for a specific cryptocurrency, including its fiat exchange rate and price change. - * - * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. - * @property fiatRate The current fiat exchange rate for the cryptocurrency. - * @property priceChange The price change for the cryptocurrency. - * @property source source of data - */ - data class Value( - override val rawCurrencyId: CryptoCurrency.RawID, - val fiatRate: BigDecimal, - val priceChange: BigDecimal, - val source: StatusSource, - ) : Quote -} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt index 9c088e4fce..3e8451b1ed 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Token.kt @@ -1,12 +1,14 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.quote.QuoteStatus + /** * Represents a domain model for a token (as part of a general list of tokens). * * @property id The unique identifier of the token. * @property networks List of networks associated with the token. * @property isAvailable Indicates whether this token is supported in Tangem app. - * @property quote Equivalent prices for the token. + * @property quoteStatus Equivalent prices for the token. * @property name The name of the token. * @property symbol The brief name of the token, e.g., "BTC". * @property iconUrl URL of the token's icon. @@ -15,7 +17,7 @@ data class Token( val id: String, val networks: List, val isAvailable: Boolean, - val quote: Quote?, + val quoteStatus: QuoteStatus?, val name: String, val symbol: String, val iconUrl: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt index bbb70459eb..290a8de379 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,7 +28,7 @@ class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -162,8 +162,8 @@ class AddCryptoCurrenciesUseCase( } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 8d69b8ab21..2d45549280 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError @@ -22,7 +22,7 @@ class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -82,8 +82,8 @@ class FetchCardTokenListUseCase( } private suspend fun fetchQuotes(currenciesIds: Set) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params(currenciesIds = currenciesIds, appCurrencyId = null), ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 5a6a9fa889..d890e9269e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -7,7 +7,7 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.single.SingleNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError @@ -30,7 +30,7 @@ class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -123,8 +123,8 @@ class FetchCurrencyStatusUseCase( } private suspend fun Raise.fetchQuote(currencyId: CryptoCurrency.ID) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = setOfNotNull(currencyId.rawCurrencyId), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 782de0af6f..7ca78e63e9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -9,7 +9,7 @@ import arrow.core.toNonEmptyListOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError @@ -32,7 +32,7 @@ class FetchTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -111,8 +111,8 @@ class FetchTokenListUseCase( } private suspend fun Raise.fetchQuotes(currenciesIds: Set) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet(), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt index 446ba616af..e9b9d3925b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -5,7 +5,7 @@ import arrow.core.getOrElse import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.multi.MultiQuoteFetcher +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.error.QuotesError import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -15,7 +15,7 @@ import kotlinx.coroutines.coroutineScope class RefreshMultiCurrencyWalletQuotesUseCase( private val currenciesRepository: CurrenciesRepository, - private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { @@ -45,8 +45,8 @@ class RefreshMultiCurrencyWalletQuotesUseCase( } private suspend fun fetchQuotes(currenciesIds: Set) { - multiQuoteFetcher( - params = MultiQuoteFetcher.Params( + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params( currenciesIds = currenciesIds.mapNotNullTo(hashSetOf(), CryptoCurrency.ID::rawCurrencyId), appCurrencyId = null, ), diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 6e8d810651..09a6e9bbe8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -9,13 +9,14 @@ import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.quotes.QuotesRepositoryV2 -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -23,7 +24,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -41,18 +41,18 @@ import kotlinx.coroutines.flow.* @Suppress("LargeClass", "LongParameterList") abstract class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, - private val quotesRepositoryV2: QuotesRepositoryV2, + private val quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) { protected val currencyStatusProxyCreator = CurrencyStatusProxyCreator(stakingRepository) - protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> + protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> protected abstract fun getNetworksStatuses( userWalletId: UserWalletId, @@ -120,7 +120,7 @@ abstract class BaseCurrencyStatusOperations( combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = maybeQuote, + maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, maybeYieldBalance = maybeYieldBalance, ) @@ -129,7 +129,7 @@ abstract class BaseCurrencyStatusOperations( combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = maybeQuote, + maybeQuoteStatus = maybeQuote, maybeNetworkStatus = maybeNetworkStatus, maybeYieldBalance = null, ) @@ -191,7 +191,7 @@ abstract class BaseCurrencyStatusOperations( } val quote = cryptoCurrencyId.rawCurrencyId?.let { rawId -> - singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = rawId)) + singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(rawCurrencyId = rawId)) .firstOrNull() } ?.right() @@ -210,7 +210,7 @@ abstract class BaseCurrencyStatusOperations( return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = quote, + maybeQuoteStatus = quote, maybeNetworkStatus = networkStatuses, maybeYieldBalance = yieldBalances, ) @@ -257,7 +257,7 @@ abstract class BaseCurrencyStatusOperations( val (_, currenciesIds) = getIds(nonEmptyCurrencies) val rawIds = currenciesIds.mapNotNull { it.rawCurrencyId }.toSet() - val quotes = quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds)?.right() + val quotes = quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds)?.right() val networkStatuses = multiNetworkStatusSupplier( params = MultiNetworkStatusProducer.Params(userWalletId = userWalletId), @@ -287,7 +287,7 @@ abstract class BaseCurrencyStatusOperations( ) val quotes = currency.id.rawCurrencyId?.let { - singleQuoteSupplier(params = SingleQuoteProducer.Params(rawCurrencyId = it)) + singleQuoteStatusSupplier(params = SingleQuoteStatusProducer.Params(rawCurrencyId = it)) .firstOrNull() } ?.right() @@ -303,7 +303,7 @@ abstract class BaseCurrencyStatusOperations( return currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - maybeQuote = quotes, + maybeQuoteStatus = quotes, maybeNetworkStatus = networkStatus, maybeYieldBalance = yieldBalances, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index d29b49e8be..fc57cf580f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -13,15 +13,16 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusProducer import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.quotes.QuotesRepositoryV2 -import com.tangem.domain.quotes.multi.MultiQuoteFetcher -import com.tangem.domain.quotes.single.SingleQuoteProducer -import com.tangem.domain.quotes.single.SingleQuoteSupplier +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher +import com.tangem.domain.quotes.single.SingleQuoteStatusProducer +import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher @@ -31,7 +32,6 @@ import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.extractAddress @@ -44,25 +44,25 @@ import kotlinx.coroutines.flow.* @Suppress("LongParameterList", "LargeClass") class CachedCurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, - quotesRepositoryV2: QuotesRepositoryV2, + quotesRepository: QuotesRepository, private val stakingRepository: StakingRepository, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val multiQuoteFetcher: MultiQuoteFetcher, - private val singleQuoteSupplier: SingleQuoteSupplier, + private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, + private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, - quotesRepositoryV2 = quotesRepositoryV2, + quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteSupplier = singleQuoteSupplier, + singleQuoteStatusSupplier = singleQuoteStatusSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, tokensFeatureToggles = tokensFeatureToggles, ) { @@ -123,7 +123,7 @@ class CachedCurrenciesStatusesOperations( val (networks, currenciesIds) = getIds(currencies) fun createCurrenciesStatuses( - maybeQuotes: Either>?, + maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, maybeYieldBalances: Either?, isUpdating: Boolean, @@ -203,8 +203,8 @@ class CachedCurrenciesStatusesOperations( async { val rawCurrenciesIds = currenciesIds.mapNotNullTo(mutableSetOf()) { it.rawCurrencyId } - multiQuoteFetcher( - params = MultiQuoteFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), + multiQuoteStatusFetcher( + params = MultiQuoteStatusFetcher.Params(currenciesIds = rawCurrenciesIds, appCurrencyId = null), ) }, async { @@ -226,7 +226,7 @@ class CachedCurrenciesStatusesOperations( private fun createCurrenciesStatuses( currencies: NonEmptyList, - maybeQuotes: Either>?, + maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, maybeYieldBalances: Either?, isUpdating: Boolean, @@ -252,7 +252,7 @@ class CachedCurrenciesStatusesOperations( val currencyStatus = currencyStatusProxyCreator.createCurrencyStatus( currency = currency, - quote = quote, + quoteStatus = quote, networkStatus = networkStatus, yieldBalance = yieldBalance, ignoreQuote = quotesRetrievingFailed, @@ -286,7 +286,7 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { + private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return getQuotesUpdates( rawCurrencyIds = tokensIds.mapNotNullTo( destination = hashSetOf(), @@ -295,11 +295,11 @@ class CachedCurrenciesStatusesOperations( ) } - override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = id), + override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { + return singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = id), ) - .map>> { setOf(it).right() } + .map>> { setOf(it).right() } .distinctUntilChanged() } @@ -367,14 +367,16 @@ class CachedCurrenciesStatusesOperations( } // temporary code because token list is built using networks list - private fun getQuotesUpdates(rawCurrencyIds: Set): EitherFlow> { + private fun getQuotesUpdates( + rawCurrencyIds: Set, + ): EitherFlow> { return channelFlow { - val state = MutableStateFlow(emptySet()) + val state = MutableStateFlow(emptySet()) rawCurrencyIds.onEach { launch { - singleQuoteSupplier( - params = SingleQuoteProducer.Params(rawCurrencyId = it), + singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = it), ) .onEach { quote -> state.update { loadedStatuses -> @@ -389,7 +391,7 @@ class CachedCurrenciesStatusesOperations( .onEach(::send) .launchIn(scope = this) } - .map, Either>> { it.right() } + .map, Either>> { it.right() } .distinctUntilChanged() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index f890486cc2..82c396b43b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -3,30 +3,24 @@ package com.tangem.domain.tokens.operations import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote import java.math.BigDecimal internal class CurrencyStatusOperations( private val currency: CryptoCurrency, - private val quote: Quote?, + private val quoteStatus: QuoteStatus?, private val networkStatus: NetworkStatus?, private val yieldBalance: YieldBalance?, private val ignoreQuote: Boolean, ) { - private val Quote?.fiatRate: BigDecimal? - get() = when (this) { - is Quote.Value -> this.fiatRate - is Quote.Empty, null -> null - } + private val QuoteStatus?.fiatRate: BigDecimal? + get() = (this?.value as? QuoteStatus.Data)?.fiatRate - private val Quote?.priceChange: BigDecimal? - get() = when (this) { - is Quote.Value -> this.priceChange - is Quote.Empty, null -> null - } + private val QuoteStatus?.priceChange: BigDecimal? + get() = (this?.value as? QuoteStatus.Data)?.priceChange fun createTokenStatus(): CryptoCurrencyStatus = CryptoCurrencyStatus(currency, createStatus()) @@ -41,12 +35,12 @@ internal class CurrencyStatusOperations( } private fun createMissedDerivationStatus(): CryptoCurrencyStatus.MissedDerivation = - CryptoCurrencyStatus.MissedDerivation(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) + CryptoCurrencyStatus.MissedDerivation(priceChange = quoteStatus?.priceChange, fiatRate = quoteStatus?.fiatRate) private fun createUnreachableStatus(status: NetworkStatus.Unreachable): CryptoCurrencyStatus.Unreachable { return CryptoCurrencyStatus.Unreachable( - priceChange = quote?.priceChange, - fiatRate = quote?.fiatRate, + priceChange = quoteStatus?.priceChange, + fiatRate = quoteStatus?.fiatRate, networkAddress = status.address, ) } @@ -54,13 +48,13 @@ internal class CurrencyStatusOperations( private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount { return CryptoCurrencyStatus.NoAccount( amountToCreateAccount = status.amountToCreateAccount, - fiatAmount = if (quote == null) null else BigDecimal.ZERO, - priceChange = quote?.priceChange, - fiatRate = quote?.fiatRate, + fiatAmount = if (quoteStatus == null) null else BigDecimal.ZERO, + priceChange = quoteStatus?.priceChange, + fiatRate = quoteStatus?.fiatRate, networkAddress = status.address, sources = CryptoCurrencyStatus.Sources( networkSource = status.source, - quoteSource = (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, ), ) } @@ -75,7 +69,10 @@ internal class CurrencyStatusOperations( return CryptoCurrencyStatus.Loading } is NetworkStatus.Amount.NotFound -> { - return CryptoCurrencyStatus.NoAmount(priceChange = quote?.priceChange, fiatRate = quote?.fiatRate) + return CryptoCurrencyStatus.NoAmount( + priceChange = quoteStatus?.priceChange, + fiatRate = quoteStatus?.fiatRate, + ) } is NetworkStatus.Amount.Loaded -> amount.value } @@ -96,24 +93,27 @@ internal class CurrencyStatusOperations( } else { null } + + val quoteValue = quoteStatus?.value + // order is important for correct total balance calculation return when { currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, - fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), - fiatRate = quote?.fiatRate, - priceChange = quote?.priceChange, + fiatAmount = calculateFiatAmountOrNull(amount, quoteStatus?.fiatRate), + fiatRate = quoteStatus?.fiatRate, + priceChange = quoteStatus?.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, sources = CryptoCurrencyStatus.Sources( networkSource = networkStatusValue.source, - quoteSource = (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL, ), ) - quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( + quoteValue is QuoteStatus.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, @@ -121,22 +121,22 @@ internal class CurrencyStatusOperations( yieldBalance = currentYieldBalance, sources = CryptoCurrencyStatus.Sources( networkSource = networkStatusValue.source, - quoteSource = (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + quoteSource = quoteStatus?.value?.source ?: StatusSource.ACTUAL, yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL, ), ) - quote is Quote.Value -> CryptoCurrencyStatus.Loaded( + quoteValue is QuoteStatus.Data -> CryptoCurrencyStatus.Loaded( amount = amount, - fiatAmount = calculateFiatAmount(amount, quote.fiatRate), - fiatRate = quote.fiatRate, - priceChange = quote.priceChange, + fiatAmount = calculateFiatAmount(amount, quoteValue.fiatRate), + fiatRate = quoteValue.fiatRate, + priceChange = quoteValue.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, sources = CryptoCurrencyStatus.Sources( networkSource = networkStatusValue.source, - quoteSource = quote.source, + quoteSource = quoteValue.source, yieldBalanceSource = currentYieldBalance?.source ?: StatusSource.ACTUAL, ), ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt index 337ffb44db..0dca67d61a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/utils/CurrencyStatusProxyCreator.kt @@ -6,11 +6,11 @@ import arrow.core.raise.either import arrow.core.toNonEmptySetOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.operations.CurrencyStatusOperations @@ -27,14 +27,14 @@ class CurrencyStatusProxyCreator( fun createCurrencyStatus( currency: CryptoCurrency, - maybeQuote: Either, + maybeQuoteStatus: Either, maybeNetworkStatus: Either, maybeYieldBalance: Either?, ): Either = either { var quoteRetrievingFailed = false val networkStatus = maybeNetworkStatus.bind() - val quote = arrow.core.raise.recover({ maybeQuote.bind() }) { + val quote = arrow.core.raise.recover({ maybeQuoteStatus.bind() }) { quoteRetrievingFailed = true null } @@ -42,7 +42,7 @@ class CurrencyStatusProxyCreator( createCurrencyStatus( currency = currency, - quote = quote, + quoteStatus = quote, networkStatus = networkStatus, ignoreQuote = quoteRetrievingFailed, yieldBalance = yieldBalance, @@ -51,14 +51,14 @@ class CurrencyStatusProxyCreator( fun createCurrenciesStatuses( currencies: NonEmptyList, - maybeQuotes: Either>?, + maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, maybeYieldBalances: Either?, ): Either> = either { var quotesRetrievingFailed = false val networksStatuses = maybeNetworkStatuses?.bind()?.toNonEmptySetOrNull() - val quotes: Set? = maybeQuotes?.fold( + val quoteStatuses: Set? = maybeQuotes?.fold( ifLeft = { quotesRetrievingFailed = true null @@ -74,7 +74,7 @@ class CurrencyStatusProxyCreator( val yieldBalances = maybeYieldBalances?.getOrNull() currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } + val quote = quoteStatuses?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } val address = extractAddress(networkStatus) @@ -89,7 +89,7 @@ class CurrencyStatusProxyCreator( } createCurrencyStatus( currency = currency, - quote = quote, + quoteStatus = quote, networkStatus = networkStatus, ignoreQuote = quotesRetrievingFailed, yieldBalance = yieldBalance, @@ -99,14 +99,14 @@ class CurrencyStatusProxyCreator( fun createCurrencyStatus( currency: CryptoCurrency, - quote: Quote?, + quoteStatus: QuoteStatus?, networkStatus: NetworkStatus?, ignoreQuote: Boolean, yieldBalance: YieldBalance?, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( currency = currency, - quote = quote, + quoteStatus = quoteStatus, networkStatus = networkStatus, ignoreQuote = ignoreQuote, yieldBalance = yieldBalance, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 0d786ca54c..12b9de0cbc 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -3,83 +3,103 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") internal object MockQuotes { - val quote1 = Quote.Value( + val quote1 = QuoteStatus( rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, - fiatRate = BigDecimal("1.23"), - priceChange = BigDecimal("0.01"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + source = StatusSource.ACTUAL, + ), ) - val quote2 = Quote.Value( + val quote2 = QuoteStatus( rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, - fiatRate = BigDecimal("2.34"), - priceChange = BigDecimal("-0.02"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + source = StatusSource.ACTUAL, + ), ) - val quote3 = Quote.Value( + val quote3 = QuoteStatus( rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, - fiatRate = BigDecimal("3.45"), - priceChange = BigDecimal("0.03"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + source = StatusSource.ACTUAL, + ), ) - val quote4 = Quote.Value( + val quote4 = QuoteStatus( rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, - fiatRate = BigDecimal("4.56"), - priceChange = BigDecimal("-0.04"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + source = StatusSource.ACTUAL, + ), ) - val quote5 = Quote.Value( + val quote5 = QuoteStatus( rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, - fiatRate = BigDecimal("5.67"), - priceChange = BigDecimal("0.05"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + source = StatusSource.ACTUAL, + ), ) - val quote6 = Quote.Value( + val quote6 = QuoteStatus( rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, - fiatRate = BigDecimal("6.78"), - priceChange = BigDecimal("-0.06"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + source = StatusSource.ACTUAL, + ), ) - val quote7 = Quote.Value( + val quote7 = QuoteStatus( rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, - fiatRate = BigDecimal("7.89"), - priceChange = BigDecimal("0.07"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + source = StatusSource.ACTUAL, + ), ) - val quote8 = Quote.Value( + val quote8 = QuoteStatus( rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, - fiatRate = BigDecimal("8.90"), - priceChange = BigDecimal("-0.08"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + source = StatusSource.ACTUAL, + ), ) - val quote9 = Quote.Value( + val quote9 = QuoteStatus( rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, - fiatRate = BigDecimal("9.01"), - priceChange = BigDecimal("0.09"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + source = StatusSource.ACTUAL, + ), ) - val quote10 = Quote.Value( + val quote10 = QuoteStatus( rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, - fiatRate = BigDecimal("10.12"), - priceChange = BigDecimal("-0.10"), - source = StatusSource.ACTUAL, + value = QuoteStatus.Data( + fiatRate = BigDecimal("10.12"), + priceChange = BigDecimal("-0.10"), + source = StatusSource.ACTUAL, + ), ) - val quote11 = Quote.Empty(CryptoCurrency.RawID("null")) + val quote11 = QuoteStatus(rawCurrencyId = CryptoCurrency.RawID("null"), value = QuoteStatus.Empty) val quotes = nonEmptySetOf( quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index c3f573af01..fddd277f37 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -3,8 +3,9 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.quote.fold import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Quote import java.math.BigDecimal @Suppress("MemberVisibilityCanBePrivate") @@ -13,8 +14,8 @@ internal object MockTokensStates { val tokenState1 = CryptoCurrencyStatus( currency = MockTokens.token1, value = CryptoCurrencyStatus.Unreachable( - priceChange = MockQuotes.quote1.priceChange, - fiatRate = MockQuotes.quote1.fiatRate, + priceChange = MockQuotes.quote1.getPriceChange(), + fiatRate = MockQuotes.quote1.getFiatRate(), networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), @@ -24,8 +25,8 @@ internal object MockTokensStates { val tokenState2 = CryptoCurrencyStatus( currency = MockTokens.token2, value = CryptoCurrencyStatus.Unreachable( - priceChange = MockQuotes.quote2.priceChange, - fiatRate = MockQuotes.quote2.fiatRate, + priceChange = MockQuotes.quote2.getPriceChange(), + fiatRate = MockQuotes.quote2.getFiatRate(), networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), @@ -35,8 +36,8 @@ internal object MockTokensStates { val tokenState3 = CryptoCurrencyStatus( currency = MockTokens.token3, value = CryptoCurrencyStatus.Unreachable( - priceChange = MockQuotes.quote3.priceChange, - fiatRate = MockQuotes.quote3.fiatRate, + priceChange = MockQuotes.quote3.getPriceChange(), + fiatRate = MockQuotes.quote3.getFiatRate(), networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), @@ -46,24 +47,24 @@ internal object MockTokensStates { val tokenState4 = CryptoCurrencyStatus( currency = MockTokens.token4, value = CryptoCurrencyStatus.MissedDerivation( - priceChange = MockQuotes.quote4.priceChange, - fiatRate = MockQuotes.quote4.fiatRate, + priceChange = MockQuotes.quote4.getPriceChange(), + fiatRate = MockQuotes.quote4.getFiatRate(), ), ) val tokenState5 = CryptoCurrencyStatus( currency = MockTokens.token5, value = CryptoCurrencyStatus.MissedDerivation( - priceChange = MockQuotes.quote5.priceChange, - fiatRate = MockQuotes.quote5.fiatRate, + priceChange = MockQuotes.quote5.getPriceChange(), + fiatRate = MockQuotes.quote5.getFiatRate(), ), ) val tokenState6 = CryptoCurrencyStatus( currency = MockTokens.token6, value = CryptoCurrencyStatus.MissedDerivation( - priceChange = MockQuotes.quote6.priceChange, - fiatRate = MockQuotes.quote6.fiatRate, + priceChange = MockQuotes.quote6.getPriceChange(), + fiatRate = MockQuotes.quote6.getFiatRate(), ), ) @@ -71,8 +72,8 @@ internal object MockTokensStates { currency = MockTokens.token7, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote7.priceChange, - fiatRate = MockQuotes.quote7.fiatRate, + priceChange = MockQuotes.quote7.getPriceChange(), + fiatRate = MockQuotes.quote7.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -85,8 +86,8 @@ internal object MockTokensStates { currency = MockTokens.token8, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote8.priceChange, - fiatRate = MockQuotes.quote8.fiatRate, + priceChange = MockQuotes.quote8.getPriceChange(), + fiatRate = MockQuotes.quote8.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -99,8 +100,8 @@ internal object MockTokensStates { currency = MockTokens.token9, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote9.priceChange, - fiatRate = MockQuotes.quote9.fiatRate, + priceChange = MockQuotes.quote9.getPriceChange(), + fiatRate = MockQuotes.quote9.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -113,8 +114,8 @@ internal object MockTokensStates { currency = MockTokens.token10, value = CryptoCurrencyStatus.NoAccount( fiatAmount = BigDecimal.ZERO, - priceChange = MockQuotes.quote10.priceChange, - fiatRate = MockQuotes.quote10.fiatRate, + priceChange = MockQuotes.quote10.getPriceChange(), + fiatRate = MockQuotes.quote10.getFiatRate(), amountToCreateAccount = MockNetworks.amountToCreateAccount, networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), @@ -145,8 +146,8 @@ internal object MockTokensStates { )?.value ?: BigDecimal.ZERO val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } - val value = when (quote) { - is Quote.Empty -> CryptoCurrencyStatus.NoQuote( + val value = when (val value = quote.value) { + is QuoteStatus.Empty -> CryptoCurrencyStatus.NoQuote( amount = status.value.amount!!, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, @@ -156,11 +157,11 @@ internal object MockTokensStates { yieldBalance = null, sources = CryptoCurrencyStatus.Sources(), ) - is Quote.Value -> CryptoCurrencyStatus.Loaded( + is QuoteStatus.Data -> CryptoCurrencyStatus.Loaded( amount = amount, - fiatAmount = amount * quote.fiatRate, - fiatRate = quote.fiatRate, - priceChange = quote.priceChange, + fiatAmount = amount * value.fiatRate, + fiatRate = value.fiatRate, + priceChange = value.priceChange, pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, @@ -187,4 +188,7 @@ internal object MockTokensStates { ), ) } + + private fun QuoteStatus.getPriceChange(): BigDecimal? = fold(onData = { priceChange }, onEmpty = { null }) + private fun QuoteStatus.getFiatRate(): BigDecimal? = fold(onData = { fiatRate }, onEmpty = { null }) } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt index 536b7207dc..eebd03482d 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthChallenge.kt @@ -23,9 +23,10 @@ fun VisaAuthChallenge.Card.toSignedChallenge(signedChallenge: String, salt: Stri ) } -fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String): VisaAuthSignedChallenge { +fun VisaAuthChallenge.Wallet.toSignedChallenge(signedChallenge: String, salt: String): VisaAuthSignedChallenge { return VisaAuthSignedChallenge.ByWallet( challenge = this, signature = signedChallenge, + salt = salt, ) } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt index a16916a887..607dabccbb 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthSignedChallenge.kt @@ -11,5 +11,6 @@ sealed class VisaAuthSignedChallenge { data class ByWallet( val challenge: VisaAuthChallenge.Wallet, val signature: String, + val salt: String, ) : VisaAuthSignedChallenge() } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt index 256587dce1..f7f19ca0e9 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaAuthRepository.kt @@ -13,7 +13,10 @@ interface VisaAuthRepository { cardPublicKey: String, ): Either - suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): Either + suspend fun getCardWalletAuthChallenge( + cardId: String, + cardWalletAddress: String, + ): Either suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): Either diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt index cdde8975d6..af30e62722 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcEthTransactionParams.kt @@ -12,7 +12,7 @@ data class WcEthTransactionParams( val to: String?, @Json(name = "data") - val data: String, + val data: String?, @Json(name = "gas") val gas: String?, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt index 5a060eaaba..005daafedc 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairError.kt @@ -1,15 +1,19 @@ package com.tangem.domain.walletconnect.model -sealed class WcPairError(override val message: String) : Exception(message) { +sealed class WcPairError( + val code: String, + override val message: String = "", +) : Exception() { - data object UnsupportedDApp : WcPairError("UnsupportedDApp") - data class UnsupportedNetworks( - val chains: Set, - ) : WcPairError("ApprovalErrorMissingNetworks") - - data class ExternalApprovalError( - override val message: String, - ) : WcPairError("ExternalApprovalError") + data class UriAlreadyUsed(override val message: String) : WcPairError("107 001 001") + data class PairingFailed(override val message: String) : WcPairError("107 001 002") + data object InvalidDomainURL : WcPairError("107 001 003") + data object UnsupportedDomain : WcPairError("107 001 004") + data class UnsupportedBlockchains(val chains: Set) : WcPairError("107 001 005") + data object InvalidConnectionRequest : WcPairError("107 002 001") + data object ProposalExpired : WcPairError("107 002 002") + data class ApprovalFailed(override val message: String) : WcPairError("107 002 003") + data object RejectionFailed : WcPairError("107 002 004") data class Unknown(override val message: String) : WcPairError(message) } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt index 53de930b75..d4f126ead7 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcTransactionUseCase.kt @@ -44,6 +44,6 @@ interface WcMutableFee { * [updateAmount] triggered a new [TransactionData] emit */ interface WcApproval { - suspend fun getAmount(): Amount? + fun getAmount(): Amount? fun updateAmount(amount: Amount?) } \ No newline at end of file diff --git a/features/fee-selector/api/build.gradle.kts b/features/fee-selector/api/build.gradle.kts deleted file mode 100644 index f224bf5622..0000000000 --- a/features/fee-selector/api/build.gradle.kts +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - id("configuration") -} - -android { - namespace = "com.tangem.features.feeselector.api" -} - -dependencies { - /** Project - Core */ - implementation(projects.core.ui) - implementation(projects.core.decompose) - - /** Domain models */ - // api(projects.domain.models) - implementation(projects.domain.appCurrency.models) - // implementation(projects.domain.wallets.models) - - /** Compose */ - implementation(deps.compose.runtime) - implementation(deps.compose.foundation) - - /** Other */ - implementation(deps.kotlin.immutable.collections) -} \ No newline at end of file diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorFeatureModule.kt b/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorFeatureModule.kt deleted file mode 100644 index d47efb12e3..0000000000 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorFeatureModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.feeselector.impl.di - -import com.tangem.features.feeselector.api.component.FeeSelectorComponent -import com.tangem.features.feeselector.impl.component.DefaultFeeSelectorComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface FeeSelectorFeatureModule { - - @Binds - @Singleton - fun bindComponentFactory(factory: DefaultFeeSelectorComponent.Factory): FeeSelectorComponent.Factory -} \ No newline at end of file diff --git a/features/hot-wallet/api/.gitignore b/features/hot-wallet/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/hot-wallet/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/hot-wallet/api/build.gradle.kts b/features/hot-wallet/api/build.gradle.kts new file mode 100644 index 0000000000..6260c97d71 --- /dev/null +++ b/features/hot-wallet/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.hotwallet.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt new file mode 100644 index 0000000000..f8ecf58ffe --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.hotwallet + +interface HotWalletFeatureToggles { + val isHotWalletEnabled: Boolean +} \ No newline at end of file diff --git a/features/hot-wallet/impl/.gitignore b/features/hot-wallet/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/hot-wallet/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts new file mode 100644 index 0000000000..7b63af71b6 --- /dev/null +++ b/features/hot-wallet/impl/build.gradle.kts @@ -0,0 +1,67 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.hotwallet.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.hotWallet.api) + + /** Core modules */ + implementation(projects.core.configToggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Tangem libraries */ + implementation(projects.libs.tangemSdkApi) + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { + exclude(module = "joda-time") + } + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.lottie.compose) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + implementation(deps.androidx.datastore) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt new file mode 100644 index 0000000000..cadb09331f --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.hotwallet + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultHotWalletFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : HotWalletFeatureToggles { + override val isHotWalletEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_ENABLED") +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt new file mode 100644 index 0000000000..3b8b7412a8 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt @@ -0,0 +1,25 @@ +package com.tangem.features.hotwallet.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.hotwallet.DefaultHotWalletFeatureToggles +import com.tangem.features.hotwallet.HotWalletFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object HotWalletFeatureModule { + + @Provides + @Singleton + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): HotWalletFeatureToggles { + return DefaultHotWalletFeatureToggles(featureTogglesManager) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface HotWalletFeatureModuleBinds \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt index a6e5e646ce..4f55af238c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/converter/HotTokenItemStateConverter.kt @@ -9,8 +9,9 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.quote.fold import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.model.Quote import com.tangem.utils.converter.Converter import java.math.BigDecimal @@ -30,7 +31,7 @@ internal class HotTokenItemStateConverter( id = value.cryptoCurrency.id.value, iconState = CryptoCurrencyToIconStateConverter().convert(value.cryptoCurrency), titleState = TokenItemState.TitleState.Content(text = stringReference(value.cryptoCurrency.name)), - subtitleState = value.quote.getCryptoPriceState(appCurrency), + subtitleState = value.quoteStatus.getCryptoPriceState(appCurrency), fiatAmountState = null, subtitle2State = null, onItemClick = onItemClick.let { onItemClick -> { onItemClick(it, value) } }, @@ -38,17 +39,17 @@ internal class HotTokenItemStateConverter( ) } - private fun Quote.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { - return when (this) { - is Quote.Empty -> TokenItemState.SubtitleState.Unknown - is Quote.Value -> { + private fun QuoteStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { + return fold( + onData = { TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(appCurrency), priceChangePercent = priceChange.format { percent() }, type = priceChange.getPriceChangeType(), ) - } - } + }, + onEmpty = { TokenItemState.SubtitleState.Unknown }, + ) } private fun BigDecimal.getFormattedCryptoPrice(appCurrency: AppCurrency): String { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt index 48ce4dfda8..fb80254c3c 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/ui/SelectPaymentMethodBottomSheet.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.draw.clip import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodsBottomSheetConfig -import com.tangem.features.onramp.utils.selectedBorder import kotlinx.collections.immutable.ImmutableList @Composable @@ -59,27 +59,16 @@ private fun SelectPaymentMethodBottomSheetContent( val isSelected = remember(item, selectedMethodId) { item.id == selectedMethodId } - val itemModifier = if (isSelected) { - Modifier - .fillMaxWidth() - .selectedBorder() - .clickable(onClick = item.onSelect) - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing10, - bottom = TangemTheme.dimens.spacing10, - ) - } else { - Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .clickable(onClick = item.onSelect) - .padding( - start = TangemTheme.dimens.spacing12, - top = TangemTheme.dimens.spacing10, - bottom = TangemTheme.dimens.spacing10, - ) - } + val itemModifier = Modifier + .fillMaxWidth() + .selectedBorder(isSelected = isSelected) + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .clickable(onClick = item.onSelect) + .padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + ) PaymentMethodItem( modifier = itemModifier, paymentMethod = item, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 6f74ef7c95..59cb8cb0b0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -26,13 +26,10 @@ import androidx.compose.ui.util.fastForEach import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.onramp.model.OnrampPaymentMethod @@ -41,7 +38,6 @@ import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon import com.tangem.features.onramp.providers.entity.ProviderListItemUM import com.tangem.features.onramp.providers.entity.SelectPaymentAndProviderUM import com.tangem.features.onramp.providers.model.previewData.SelectProviderPreviewData -import com.tangem.features.onramp.utils.selectedBorder @Composable internal fun SelectProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { @@ -151,13 +147,7 @@ private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifie private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, modifier: Modifier = Modifier) { Row( modifier = modifier - .then( - if (state.isSelected) { - Modifier.selectedBorder() - } else { - Modifier.clip(RoundedCornerShape(16.dp)) - }, - ) + .selectedBorder(isSelected = state.isSelected) .clickable(onClick = state.onClick) .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, @@ -227,13 +217,7 @@ private fun UnavailableProviderItem( ) { Row( modifier = modifier - .then( - if (isSelected) { - Modifier.selectedBorder() - } else { - Modifier.clip(RoundedCornerShape(16.dp)) - }, - ) + .selectedBorder(isSelected = isSelected) .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt deleted file mode 100644 index 8e38b54575..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ModifierExt.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.onramp.utils - -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -@Composable -internal fun Modifier.selectedBorder() = border( - width = 2.5.dp, - color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), - shape = RoundedCornerShape(18.dp), -) - .padding(2.5.dp) - .border( - width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(16.dp), - ) - .clip(RoundedCornerShape(16.dp)) \ No newline at end of file diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index 2147785720..9aa3093878 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Domain models */ api(projects.domain.models) + implementation(projects.domain.appCurrency.models) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.nft.models) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..bfb687f2eb --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorBlockComponent.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.v2.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface FeeSelectorBlockComponent : ComposableContentComponent { + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component/FeeSelectorComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt similarity index 89% rename from features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component/FeeSelectorComponent.kt rename to features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt index 726aab533a..e3a8a20cc7 100644 --- a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/component/FeeSelectorComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/FeeSelectorComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feeselector.api.component +package com.tangem.features.send.v2.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index 4dbf452662..0e08965245 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -1,3 +1,6 @@ package com.tangem.features.send.v2.api -interface SendFeatureToggles \ No newline at end of file +interface SendFeatureToggles { + + val isSendRedesignEnabled: Boolean +} \ No newline at end of file diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index f3961499eb..dc97603af5 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -16,7 +16,6 @@ dependencies { implementation(projects.features.sendV2.api) implementation(projects.features.txhistory.api) implementation(projects.features.nft.api) - implementation(projects.features.feeSelector.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index 326f72db7e..cce9c99562 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -4,5 +4,8 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.v2.api.SendFeatureToggles internal class DefaultSendFeatureToggles( - @Suppress("UnusedPrivateMember") private val featureToggles: FeatureTogglesManager, -) : SendFeatureToggles \ No newline at end of file + private val featureToggles: FeatureTogglesManager, +) : SendFeatureToggles { + override val isSendRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("SEND_REDESIGN_ENABLED") +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..96c3c5fd9e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.feeselector + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Devices +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.impl.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, +) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { + + @Composable + override fun Content(modifier: Modifier) { + FeeSelectorBlockContent(modifier = modifier) + } + + @AssistedFactory + interface Factory : FeeSelectorBlockComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultFeeSelectorBlockComponent + } +} + +@Composable +private fun FeeSelectorBlockContent(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_fee_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing6) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_token_info_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "~ 0.25 \$", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) +@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FeeSelectorBlockContent_Preview() { + TangemThemePreview { + FeeSelectorBlockContent(modifier = Modifier.fillMaxWidth()) + } +} \ No newline at end of file diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component/DefaultFeeSelectorComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt similarity index 75% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component/DefaultFeeSelectorComponent.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt index 0fda871e10..925d689d4f 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/component/DefaultFeeSelectorComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorComponent.kt @@ -1,13 +1,15 @@ -package com.tangem.features.feeselector.impl.component +package com.tangem.features.send.v2.feeselector import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.features.feeselector.api.component.FeeSelectorComponent -import com.tangem.features.feeselector.impl.model.FeeSelectorModel -import com.tangem.features.feeselector.impl.ui.FeeSelectorModalBottomSheet +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel +import com.tangem.features.send.v2.feeselector.ui.FeeSelectorModalBottomSheet import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,8 +33,9 @@ internal class DefaultFeeSelectorComponent @AssistedInject constructor( // Temporary workaround, to use test this component @Composable override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() BackHandler(onBack = router::pop) - FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = TODO()) + FeeSelectorModalBottomSheet(onDismiss = ::dismiss, state = state) } @AssistedFactory diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt new file mode 100644 index 0000000000..1b6f3fe484 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorFeatureModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.send.v2.feeselector.di + +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorBlockComponent +import com.tangem.features.send.v2.feeselector.DefaultFeeSelectorComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface FeeSelectorFeatureModule { + + @Binds + @Singleton + fun bindComponentFactory(factory: DefaultFeeSelectorComponent.Factory): FeeSelectorComponent.Factory + + @Binds + @Singleton + fun bindBlockComponentFactory(factory: DefaultFeeSelectorBlockComponent.Factory): FeeSelectorBlockComponent.Factory +} \ No newline at end of file diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt similarity index 78% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorModelModule.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt index 2144133681..9afa349960 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/di/FeeSelectorModelModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/di/FeeSelectorModelModule.kt @@ -1,8 +1,8 @@ -package com.tangem.features.feeselector.impl.di +package com.tangem.features.send.v2.feeselector.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model -import com.tangem.features.feeselector.impl.model.FeeSelectorModel +import com.tangem.features.send.v2.feeselector.model.FeeSelectorModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/entity/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt similarity index 90% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/entity/FeeSelectorUM.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt index 9fba9272a5..fd759f5d7a 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/entity/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/entity/FeeSelectorUM.kt @@ -1,10 +1,10 @@ -package com.tangem.features.feeselector.impl.entity +package com.tangem.features.send.v2.feeselector.entity import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt similarity index 82% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/model/FeeSelectorModel.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index ea2ae3c857..fa4ea825a6 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -1,12 +1,12 @@ -package com.tangem.features.feeselector.impl.model +package com.tangem.features.send.v2.feeselector.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.features.feeselector.api.component.FeeSelectorComponent -import com.tangem.features.feeselector.impl.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.FeeSelectorComponent +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt similarity index 97% rename from features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/ui/FeeSelectorModalBottomSheet.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index 4b6215a295..2b79756637 100644 --- a/features/fee-selector/impl/src/main/kotlin/com/tangem/features/feeselector/impl/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feeselector.impl.ui +package com.tangem.features.send.v2.feeselector.ui import android.content.res.Configuration import androidx.annotation.DrawableRes @@ -46,18 +46,20 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM -import com.tangem.features.feeselector.impl.R -import com.tangem.features.feeselector.impl.entity.FeeFiatRateDataHolder -import com.tangem.features.feeselector.impl.entity.FeeItem -import com.tangem.features.feeselector.impl.entity.FeeSelectorUM +import com.tangem.features.send.v2.feeselector.entity.FeeFiatRateDataHolder +import com.tangem.features.send.v2.feeselector.entity.FeeItem +import com.tangem.features.send.v2.feeselector.entity.FeeSelectorUM +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @Composable -internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM.Content, onDismiss: () -> Unit) { +internal fun FeeSelectorModalBottomSheet(state: FeeSelectorUM, onDismiss: () -> Unit) { + if (state !is FeeSelectorUM.Content) return + TangemModalBottomSheetWithFooter( config = TangemBottomSheetConfig( isShown = true, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 9e82a35a76..9aa5fe4c78 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -37,6 +37,7 @@ import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.SendConfirmAlertFactory @@ -87,6 +88,7 @@ internal class SendModel @Inject constructor( private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger, + private val sendFeatureToggles: SendFeatureToggles, ) : Model(), SendComponentCallback { private val params: SendComponent.Params = paramsContainer.require() @@ -357,6 +359,7 @@ internal class SendModel @Inject constructor( amountUM = AmountState.Empty(), destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, ).transform(DestinationUM.Empty()), feeUM = FeeUM.Empty(), confirmUM = ConfirmUM.Empty, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 8ccf0bd907..6bd184f1fc 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.isLocked import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.common.PredefinedValues import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource @@ -65,6 +66,7 @@ internal class SendDestinationModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val sendFeatureToggles: SendFeatureToggles, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -90,6 +92,7 @@ internal class SendDestinationModel @Inject constructor( _uiState.update( SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, + isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, isInitialized = true, ), ) @@ -297,6 +300,7 @@ internal class SendDestinationModel @Inject constructor( flow2 = params.currentRoute, transform = { state, route -> state to route }, ).onEach { (state, route) -> + val isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled params.callback.onNavigationResult( NavigationUM.Content( title = params.title, @@ -319,8 +323,16 @@ internal class SendDestinationModel @Inject constructor( } params.onBackClick() }, - additionalIconRes = R.drawable.ic_qrcode_scan_24, - additionalIconClick = ::onQrCodeScanClick, + additionalIconRes = if (isRedesignEnabled) { + null + } else { + R.drawable.ic_qrcode_scan_24 + }, + additionalIconClick = if (isRedesignEnabled) { + null + } else { + ::onQrCodeScanClick + }, primaryButton = ButtonsUM.PrimaryButtonUM( text = if (route.isEditMode) { resourceReference(R.string.common_continue) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt index 76de538ac5..8ad0687969 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -13,6 +13,7 @@ import com.tangem.utils.transformer.Transformer internal class SendDestinationInitialStateTransformer( val cryptoCurrency: CryptoCurrency, + val isRedesignEnabled: Boolean, val isInitialized: Boolean = false, ) : Transformer { override fun transform(prevState: DestinationUM): DestinationUM { @@ -54,6 +55,7 @@ internal class SendDestinationInitialStateTransformer( recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), networkName = cryptoCurrency.network.name, isValidating = false, + isRedesignEnabled = isRedesignEnabled, ) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt index 4caf6be2e2..1014976ae1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -16,6 +16,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowRecipient @@ -60,10 +64,13 @@ internal fun SendDestinationContent( isError = isError, isValidating = isValidating, onAddressChange = clickIntents::onRecipientAddressValueChange, + onQrCodeClick = clickIntents::onQrCodeScanClick, + isRedesignEnabled = state.isRedesignEnabled, ) memoField( memoField = memoField, onMemoChange = clickIntents::onRecipientMemoValueChange, + isRedesignEnabled = state.isRedesignEnabled, ) listHeaderItem( titleRes = R.string.send_recipient_wallets_title, @@ -100,16 +107,23 @@ internal fun SendDestinationContent( } } +@Suppress("LongParameterList") private fun LazyListScope.addressItem( address: DestinationTextFieldUM.RecipientAddress, networkName: String, isError: Boolean, isValidating: Boolean, onAddressChange: (String, EnterAddressSource) -> Unit, + onQrCodeClick: () -> Unit, + isRedesignEnabled: Boolean, ) { item(key = ADDRESS_FIELD_KEY) { FooterContainer( - footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)), + footer = if (isRedesignEnabled) { + buildHighlightedFooterText(networkName) + } else { + resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)) + }, ) { InputRowRecipient( value = address.value, @@ -117,6 +131,8 @@ private fun LazyListScope.addressItem( placeholder = address.placeholder, onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, + onQrCodeClick = onQrCodeClick, + isRedesignEnabled = isRedesignEnabled, isError = isError, isLoading = isValidating, error = address.error, @@ -131,9 +147,37 @@ private fun LazyListScope.addressItem( } } +@Composable +private fun buildHighlightedFooterText(networkName: String): TextReference { + return annotatedReference( + buildAnnotatedString { + val styledText = stringResourceSafe( + R.string.send_recipient_address_footer_highlighted_part, + networkName, + ) + val styledColor = TangemTheme.colors.text.secondary + appendWithStyledPlaceholder( + template = stringResourceSafe( + R.string.send_recipient_address_footer_v2, + networkName, + ), + placeholder = networkName, + ) { + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + appendColored( + text = styledText, + color = styledColor, + ) + } + } + }, + ) +} + private fun LazyListScope.memoField( memoField: DestinationTextFieldUM.RecipientMemo?, onMemoChange: (String, Boolean) -> Unit, + isRedesignEnabled: Boolean, ) { if (memoField != null) { item(key = MEMO_FIELD_KEY) { @@ -142,7 +186,24 @@ private fun LazyListScope.memoField( value = memoField.value, label = memoField.label, placeholder = placeholder, - footer = resourceReference(R.string.send_recipient_memo_footer), + footer = if (isRedesignEnabled) { + annotatedReference( + buildAnnotatedString { + append(stringResourceSafe(R.string.send_recipient_memo_footer_v2)) + append("\n") + withStyle(SpanStyle(fontWeight = FontWeight.Medium)) { + appendColored( + text = stringResourceSafe( + R.string.send_recipient_memo_footer_v2_highlighted, + ), + color = TangemTheme.colors.text.secondary, + ) + } + }, + ) + } else { + resourceReference(R.string.send_recipient_memo_footer) + }, onValueChange = { onMemoChange(it, false) }, onPasteClick = { onMemoChange(it, true) }, modifier = Modifier.padding(top = 20.dp), @@ -151,6 +212,7 @@ private fun LazyListScope.memoField( error = memoField.error, isReadOnly = !memoField.isEnabled, isValuePasted = memoField.isValuePasted, + isRedesignEnabled = isRedesignEnabled, ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt index 162ab6bf7b..2c2fe6de6a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.containers.FooterContainer +@Suppress("LongMethod", "LongParameterList") @Composable internal fun TextFieldWithPaste( value: String, @@ -27,6 +28,7 @@ internal fun TextFieldWithPaste( label: TextReference, onValueChange: (String) -> Unit, onPasteClick: (String) -> Unit, + isRedesignEnabled: Boolean, modifier: Modifier = Modifier, footer: TextReference? = null, labelStyle: TextStyle = TangemTheme.typography.body2, @@ -96,6 +98,18 @@ internal fun TextFieldWithPaste( PasteButton( isPasteButtonVisible = value.isBlank(), onClick = onPasteClick, + backgroundColorEnabled = if (isRedesignEnabled) { + TangemTheme.colors.button.secondary + } else { + TangemTheme.colors.button.primary + }, + textColor = if (isRedesignEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.primary2 + }, + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8), ) } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt index 06935a8368..8a73b412f0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt @@ -17,6 +17,7 @@ internal sealed class DestinationUM { val networkName: String, val isValidating: Boolean = false, val isInitialized: Boolean = false, + val isRedesignEnabled: Boolean = false, ) : DestinationUM() data class Empty( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt index b8c9fecfcf..2a5863ed1d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/FeeConverter.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt index 26603db4d2..78304904a5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/SendFeeCustomFieldConverter.kt @@ -5,11 +5,11 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.bitcoin.BitcoinCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.kaspa.KaspaCustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.utils.converter.TwoWayConverter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt index 19645e440b..4250d48580 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/CustomFeeConverter.kt @@ -1,7 +1,7 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom import com.tangem.blockchain.common.transaction.Fee -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt index 548887f56d..a2f5349672 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/bitcoin/BitcoinCustomFeeConverter.kt @@ -11,11 +11,11 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.lib.crypto.BlockchainUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt index fbdd94c589..c0828d3d60 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/BaseEthereumCustomFeeConverter.kt @@ -1,8 +1,8 @@ package com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum import com.tangem.blockchain.common.transaction.Fee -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList /** diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt index f083aaa40c..ce2ba52261 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumCustomFeeConverter.kt @@ -10,10 +10,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt index 4a40819826..2a7d389e13 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumEIPCustomFeeConverter.kt @@ -11,7 +11,6 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance @@ -20,6 +19,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt index fe9af7c37b..8b2f86beb7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/ethereum/EthereumLegacyCustomFeeConverter.kt @@ -11,7 +11,6 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.checkExceedBalance @@ -20,6 +19,7 @@ import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.eth import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GAS_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.ethereum.EthereumCustomFeeConverter.Companion.GIGA_DECIMALS import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.setEmpty +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt index af0c7a97b4..2714e7d9f6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/model/converters/custom/kaspa/KaspaCustomFeeConverter.kt @@ -11,10 +11,10 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import com.tangem.features.send.v2.impl.R import com.tangem.features.send.v2.subcomponents.fee.model.SendFeeClickIntents import com.tangem.features.send.v2.subcomponents.fee.model.converters.custom.CustomFeeConverter +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt index d597bdd4b3..cf31789446 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/SendCustomFee.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.components.inputrow.InputRowEnterAmount import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmount import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.CustomFeeFieldUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType import kotlinx.collections.immutable.ImmutableList diff --git a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity/CustomFeeFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt similarity index 90% rename from features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity/CustomFeeFieldUM.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt index cc823ad6d6..cb3a73dae0 100644 --- a/features/fee-selector/api/src/main/kotlin/com/tangem/features/feeselector/api/entity/CustomFeeFieldUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/CustomFeeFieldUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feeselector.api.entity +package com.tangem.features.send.v2.subcomponents.fee.ui.state import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt index ee4677b452..a9f5aae1b4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeSelectorUM.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt index e310e424af..6bf499e1c4 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/ui/state/FeeUM.kt @@ -3,7 +3,6 @@ package com.tangem.features.send.v2.subcomponents.fee.ui.state import androidx.compose.runtime.Stable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.features.feeselector.api.entity.CustomFeeFieldUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts deleted file mode 100644 index f104eedf8b..0000000000 --- a/features/send/impl/build.gradle.kts +++ /dev/null @@ -1,95 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.hilt.android) - id("configuration") -} - -android { - namespace = "com.tangem.features.send.impl" -} - -dependencies { - /** AndroidX */ - implementation(deps.androidx.fragment.ktx) - implementation(deps.androidx.appCompat) - implementation(deps.androidx.paging.runtime) - - /** Other dependencies */ - implementation(deps.kotlin.immutable.collections) - implementation(deps.material) - implementation(deps.arrow.core) - implementation(deps.lifecycle.compose) - implementation(deps.jodatime) - implementation(deps.timber) - implementation(deps.reKotlin) - implementation(deps.kotlin.serialization) - - /** Compose */ - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.material3) - implementation(deps.compose.material) - implementation(deps.compose.foundation) - implementation(deps.compose.ui) - implementation(deps.compose.ui.tooling) - implementation(deps.compose.navigation) - implementation(deps.compose.navigation.hilt) - implementation(deps.compose.paging) - implementation(deps.compose.constraintLayout) - - /** Tangem SDKs */ - implementation(tangemDeps.card.core) - implementation(tangemDeps.blockchain) - - /** Core modules */ - implementation(projects.core.configToggles) - implementation(projects.core.ui) - implementation(projects.core.utils) - implementation(projects.core.navigation) - implementation(projects.core.analytics) - implementation(projects.core.analytics.models) - implementation(projects.core.datasource) - implementation(projects.core.decompose) - - /** Common */ - implementation(projects.common.ui) - implementation(projects.common.routing) - - /** Libs */ - implementation(projects.libs.crypto) - - /** Domain modules */ - implementation(projects.domain.models) - implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets) - implementation(projects.domain.wallets.models) - implementation(projects.domain.appCurrency) - implementation(projects.domain.appCurrency.models) - implementation(projects.domain.txhistory) - implementation(projects.domain.txhistory.models) - implementation(projects.domain.transaction) - implementation(projects.domain.transaction.models) - implementation(projects.domain.card) - implementation(projects.domain.balanceHiding) - implementation(projects.domain.balanceHiding.models) - implementation(projects.domain.feedback) - implementation(projects.domain.feedback.models) - implementation(projects.domain.qrScanning) - implementation(projects.domain.qrScanning.models) - implementation(projects.domain.settings) - implementation(projects.domain.notifications) - - /** Feature modules */ - implementation(projects.features.send.api) - implementation(projects.features.tokendetails.api) - implementation(projects.features.txhistory.api) - implementation(projects.features.qrScanning.api) - - /** DI */ - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) -} \ No newline at end of file diff --git a/features/swap-v2/api/.gitignore b/features/swap-v2/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap-v2/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap-v2/api/build.gradle.kts b/features/swap-v2/api/build.gradle.kts new file mode 100644 index 0000000000..7530654088 --- /dev/null +++ b/features/swap-v2/api/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.swap.v2.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.express.models) + implementation(projects.domain.swap.models) + implementation(projects.domain.manageTokens.models) + implementation(projects.domain.models) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt new file mode 100644 index 0000000000..2bc15c06e7 --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.swap.v2.api + +interface SwapFeatureToggles { + val isSwapRedesignEnabled: Boolean +} \ No newline at end of file diff --git a/features/swap-v2/impl/.gitignore b/features/swap-v2/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap-v2/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/fee-selector/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts similarity index 52% rename from features/fee-selector/impl/build.gradle.kts rename to features/swap-v2/impl/build.gradle.kts index 90c8927b9b..456c959e50 100644 --- a/features/fee-selector/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -1,57 +1,78 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.serialization) alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) alias(deps.plugins.hilt.android) id("configuration") } android { - namespace = "com.tangem.features.feeselector.impl" + namespace = "com.tangem.features.swap.v2.impl" + packaging { + resources { + // To build and run composable preview + merges += "paymentrequest.proto" + } + } } -dependencies { - /** Project - API */ - implementation(projects.features.feeSelector.api) - /** Project - Core */ - implementation(projects.common.ui) - implementation(projects.common.routing) +dependencies { + /** Feature */ + implementation(projects.features.swapV2.api) + implementation(projects.features.manageTokens.api) + implementation(projects.features.sendV2.api) + + /** Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.pagination) implementation(projects.core.navigation) + implementation(projects.core.configToggles) - /** Project - Domain */ - implementation(projects.domain.appCurrency.models) + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Domain */ implementation(projects.domain.models) - implementation(projects.domain.tokens) - implementation(projects.domain.tokens.models) - implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) - - /** AndroidX */ - implementation(deps.androidx.activity.compose) - implementation(deps.lifecycle.compose) + implementation(projects.domain.wallets) + implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.express.models) + implementation(projects.domain.swap.models) + implementation(projects.domain.swap) + implementation(projects.domain.manageTokens.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.runtime) + implementation(deps.compose.material3) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.foundation) - implementation(deps.compose.material3) - implementation(deps.compose.shimmer) + implementation(deps.androidx.activity.compose) + implementation(deps.compose.constraintLayout) + implementation(deps.compose.coil) - /*** Tangem SDK */ - implementation(tangemDeps.blockchain) + /** Other */ + implementation(deps.arrow.core) + implementation(deps.decompose) + implementation(deps.decompose.ext.compose) + implementation(deps.timber) + implementation(deps.kotlin.immutable.collections) + implementation(deps.coil) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) - - /** Other */ - implementation(deps.arrow.core) - implementation(deps.kotlin.immutable.collections) - implementation(deps.decompose.ext.compose) - implementation(deps.timber) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt new file mode 100644 index 0000000000..61d092f28a --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.impl + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.v2.api.SwapFeatureToggles + +internal class DefaultSwapFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : SwapFeatureToggles { + override val isSwapRedesignEnabled: Boolean + get() = featureToggles.isFeatureEnabled("SWAP_REDESIGN_ENABLED") +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt new file mode 100644 index 0000000000..3318c6d6c3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt @@ -0,0 +1,21 @@ +package com.tangem.features.swap.v2.impl.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.v2.api.SwapFeatureToggles +import com.tangem.features.swap.v2.impl.DefaultSwapFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SwapFeatureModules { + + @Provides + @Singleton + fun provideSwapFeatureToggles(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggles { + return DefaultSwapFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 7d52289e23..256c69cbac 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } android { - namespace = "com.tangem.domain.swap" + namespace = "com.tangem.features.domain.swap" } dependencies { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 410639462f..cc21bc6469 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -19,13 +19,13 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository @@ -65,7 +65,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, - private val quotesRepositoryV2: QuotesRepositoryV2, + private val quotesRepository: QuotesRepository, private val swapTransactionRepository: SwapTransactionRepository, private val currencyChecksRepository: CurrencyChecksRepository, private val appCurrencyRepository: AppCurrencyRepository, @@ -1034,7 +1034,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + amountFiat = rates[fromToken.currency.id]?.multiply(amount.value) ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) @@ -1130,7 +1130,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( else -> getNativeToken(networkId = fromToken.network.backendId).id } val rates = getQuotes(feeCurrencyId) - return rates[feeCurrencyId]?.fiatRate?.let { rate -> + return rates[feeCurrencyId]?.let { rate -> fees.map { fee -> rate.multiply(fee).format { fiat( @@ -1233,7 +1233,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + amountFiat = rates[fromToken.currency.id]?.multiply(amount.value) ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) @@ -1307,20 +1307,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenInfo = TokenSwapInfo( tokenAmount = fromTokenAmount, cryptoCurrencyStatus = fromTokenStatus, - amountFiat = rates[fromToken.id]?.fiatRate?.multiply(fromTokenAmount.value) + amountFiat = rates[fromToken.id]?.multiply(fromTokenAmount.value) ?: BigDecimal.ZERO, ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, cryptoCurrencyStatus = toTokenStatus, - amountFiat = rates[toToken.id]?.fiatRate?.multiply(toTokenAmount.value) + amountFiat = rates[toToken.id]?.multiply(toTokenAmount.value) ?: BigDecimal.ZERO, ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.id]?.fiatRate?.toDouble() ?: 0.0, + fromRate = rates[fromToken.id]?.toDouble() ?: 0.0, toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0, + toRate = rates[toToken.id]?.toDouble() ?: 0.0, ), swapDataModel = swapData, txFee = txFeeState, @@ -1795,19 +1795,21 @@ internal class SwapInteractorImpl @AssistedInject constructor( return PriceImpact.Value(value) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { val set = ids.mapNotNull { it.rawCurrencyId } .toSet() .getQuotesOrEmpty() - .filterIsInstance() return ids - .mapNotNull { id -> set.find { it.rawCurrencyId == id.rawCurrencyId }?.let { id to it } } + .mapNotNull { id -> + set.find { it.rawCurrencyId == id.rawCurrencyId && it.value is QuoteStatus.Data } + ?.let { id to (it.value as QuoteStatus.Data).fiatRate } + } .toMap() } - private suspend fun Set.getQuotesOrEmpty(): Set { - return runCatching { quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = this) } + private suspend fun Set.getQuotesOrEmpty(): Set { + return runCatching { quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this) } .getOrNull() .orEmpty() } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt index 4a99b46f96..7ca799438d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/ui/TestPushDeepLinkBottomSheet.kt @@ -2,8 +2,7 @@ package com.tangem.feature.tester.presentation.testpush.ui import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Text @@ -29,8 +28,12 @@ internal fun TestPushDeeplinkBottomSheet(config: TangemBottomSheetConfig?) { @Composable private fun TestPushMenuDeepLinkBottomSheetContent(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content -> - LazyColumn { + TangemBottomSheet(config = config, addBottomInsets = false) { content -> + LazyColumn( + modifier = Modifier + .navigationBarsPadding() + .imePadding(), + ) { items( items = content.itemList.toList(), key = { it }, @@ -56,8 +59,13 @@ private fun TestPushMenuDeepLinkBottomSheetContent(config: TangemBottomSheetConf @Composable private fun TestPushMarketsTokenBottomSheetContent(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content -> - LazyColumn { + TangemBottomSheet(config = config, addBottomInsets = false) { content -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), + ) { item(key = "Markets Search") { OutlineTextField( label = "Title", diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt index abd657eb9e..3c0c470fe5 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenBottomSheetTransformer.kt @@ -15,16 +15,25 @@ internal class TestPushMarketTokenBottomSheetTransformer( private val onItemClick: (TokenMarket) -> Unit, ) : Transformer { override fun transform(prevState: TestPushUM): TestPushUM { + val content = (prevState.bottomSheetConfig?.content as? TestPushMarketsTokenConfigUM)?.copy( + itemList = item, + onSearchValueEdit = onSearchEdit, + onItemClick = onItemClick, + ) ?: TestPushMarketsTokenConfigUM( + itemList = item, + searchValue = TextFieldValue(""), + onSearchValueEdit = onSearchEdit, + onItemClick = onItemClick, + ) return prevState.copy( - bottomSheetConfig = TangemBottomSheetConfig( + bottomSheetConfig = prevState.bottomSheetConfig?.let { + prevState.bottomSheetConfig.copy( + content = content, + ) + } ?: TangemBottomSheetConfig( isShown = true, onDismissRequest = onDismissBottomSheet, - content = TestPushMarketsTokenConfigUM( - itemList = item, - searchValue = TextFieldValue(""), - onSearchValueEdit = onSearchEdit, - onItemClick = onItemClick, - ), + content = content, ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index f02dc11cf4..01835b8f16 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -17,7 +17,8 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.quote.mapData import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatus.Companion.isFailed @@ -57,7 +58,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fun convert( savedTransactions: List, - quotes: Set, + quoteStatuses: Set, ): PersistentList { val result = mutableListOf() @@ -73,12 +74,17 @@ internal class TokenDetailsSwapTransactionsStateConverter( val fromAmount = transaction.fromCryptoAmount var toFiatAmount: BigDecimal? = null var fromFiatAmount: BigDecimal? = null - quotes.forEach { quote -> - if (quote is Quote.Value && quote.rawCurrencyId == toCryptoCurrencyRawId) { - toFiatAmount = quote.fiatRate.multiply(toAmount) + quoteStatuses.forEach { quote -> + quote.mapData { + if (quote.rawCurrencyId == toCryptoCurrencyRawId) { + toFiatAmount = fiatRate.multiply(toAmount) + } } - if (quote is Quote.Value && quote.rawCurrencyId == fromCryptoCurrencyRawId) { - fromFiatAmount = quote.fiatRate.multiply(fromAmount) + + quote.mapData { + if (quote.rawCurrencyId == fromCryptoCurrencyRawId) { + fromFiatAmount = fiatRate.multiply(fromAmount) + } } } val statusModel = transaction.status diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 0a75b5e7d7..c12a0e8048 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -6,9 +6,9 @@ import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.quotes.QuotesRepositoryV2 +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.SwapTransactionRepository @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.map internal class ExchangeStatusFactory @AssistedInject constructor( private val swapTransactionRepository: SwapTransactionRepository, private val swapRepository: SwapRepository, - private val quotesRepositoryV2: QuotesRepositoryV2, + private val quotesRepository: QuotesRepository, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -66,7 +66,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( getExchangeStatusState( savedTransactions = savedTransactions, - quotes = quotes, + quoteStatuses = quotes, ) } } @@ -155,7 +155,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private fun getExchangeStatusState( savedTransactions: List?, - quotes: Set, + quoteStatuses: Set, ): PersistentList { if (savedTransactions == null) { return persistentListOf() @@ -163,7 +163,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( return swapTransactionsStateConverter.convert( savedTransactions = savedTransactions, - quotes = quotes, + quoteStatuses = quoteStatuses, ) } @@ -187,10 +187,10 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } } - private suspend fun Set.getQuotesOrEmpty(): Set { + private suspend fun Set.getQuotesOrEmpty(): Set { val rawIds = mapNotNull { it.rawCurrencyId }.toSet() - return runCatching { quotesRepositoryV2.getMultiQuoteSyncOrNull(currenciesIds = rawIds) } + return runCatching { quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds) } .getOrNull() .orEmpty() } diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 53a4e3c17c..625e3afceb 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { implementation(projects.features.wallet.api) implementation(projects.features.walletconnect.api) + implementation(projects.features.sendV2.api) /** Core */ implementation(projects.core.configToggles) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt index 86acac33bc..73d622b6ce 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectNetworksComponent.kt @@ -66,7 +66,7 @@ internal class WcSelectNetworksComponent( } interface ModelCallback { - fun onNetworksSelected(selectedNetworks: Set) + fun onNetworksSelected(selectedNetworks: Set) } data class WcSelectNetworksParams( @@ -74,7 +74,7 @@ internal class WcSelectNetworksComponent( val requiredNetworks: Set, val availableNetworks: Set, val notAddedNetworks: Set, - val enabledAvailableNetworks: Set, + val enabledAvailableNetworks: Set, val onDismiss: () -> Unit, val callback: ModelCallback, ) @@ -87,8 +87,6 @@ private fun WcSelectNetworksBS( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.required.isEmpty()) return // required can/must not be empty - TangemModalBottomSheetWithFooter( config = TangemBottomSheetConfig( isShown = true, @@ -117,7 +115,6 @@ private fun WcSelectNetworksBS( .fillMaxWidth(), text = "Done", onClick = state.onDone, - enabled = state.missing.isEmpty(), ) }, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index f415b0b68e..622f3c98f0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -58,7 +58,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( appIcon = session.sdkModel.appMetaData.icons.firstOrNull().orEmpty(), isVerified = session.securityStatus == CheckDAppResult.SAFE, verifiedDAppState = extractVerifiedState(session), - appSubtitle = session.sdkModel.appMetaData.description, + appSubtitle = session.sdkModel.appMetaData.url, walletName = session.wallet.name, networks = session.networks .map { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 0167da0484..bf9adcb783 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.models.network.Network +import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal @@ -25,14 +26,10 @@ import com.tangem.features.walletconnect.connections.components.WcSelectNetworks import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig -import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer -import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoWalletChangedTransformer -import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer -import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVerifiedStateConverter +import com.tangem.features.walletconnect.connections.model.transformers.* import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* -import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -67,7 +64,7 @@ internal class WcPairModel @Inject constructor( MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() - private val additionallyEnabledNetworks = mutableSetOf() + private var additionallyEnabledNetworks = setOf() private val dAppVerifiedStateConverter = WcDAppVerifiedStateConverter(onVerifiedClick = ::showVerifiedAlert) val appInfoUiState: StateFlow @@ -78,6 +75,7 @@ internal class WcPairModel @Inject constructor( } private fun loadDAppInfo() { + fun errorToast(error: WcPairError) = messageSender.send(ToastMessage(message = stringReference(error.message))) wcPairUseCase() .onEach { pairState -> when (pairState) { @@ -88,20 +86,20 @@ internal class WcPairModel @Inject constructor( appInfoUiState.transformerUpdate( WcConnectButtonProgressTransformer(showProgress = false), ) + pairState.result.onLeft(::errorToast) router.pop() } is WcPairState.Error -> { appInfoUiState.transformerUpdate( WcConnectButtonProgressTransformer(showProgress = false), ) - messageSender.send(ToastMessage(message = stringReference(pairState.error.message))) - Timber.e(pairState.error) + errorToast(pairState.error) } is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } is WcPairState.Proposal -> { sessionProposal = pairState.dAppSession proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWalletFlow.value) - proposalNetwork.available.mapTo(additionallyEnabledNetworks) { it.id.rawId } + additionallyEnabledNetworks = proposalNetwork.available appInfoUiState.transformerUpdate( WcAppInfoTransformer( dAppSession = sessionProposal, @@ -126,6 +124,7 @@ internal class WcPairModel @Inject constructor( }, userWallet = selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, + additionallyEnabledNetworks = additionallyEnabledNetworks, ), ) } @@ -153,7 +152,7 @@ internal class WcPairModel @Inject constructor( private fun connect() { val enabledAvailableNetworks = - proposalNetwork.available.filter { network -> network.id.rawId in additionallyEnabledNetworks } + proposalNetwork.available.filter { network -> network in additionallyEnabledNetworks } wcPairUseCase.approve( WcSessionApprove( wallet = selectedUserWalletFlow.value, @@ -178,14 +177,25 @@ internal class WcPairModel @Inject constructor( val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } proposalNetwork = sessionProposal.proposalNetwork.getValue(selectedUserWallet) selectedUserWalletFlow.update { selectedUserWallet } - appInfoUiState.transformerUpdate(WcAppInfoWalletChangedTransformer(selectedUserWallet, proposalNetwork)) - additionallyEnabledNetworks.clear() - proposalNetwork.available.mapTo(additionallyEnabledNetworks) { it.id.rawId } + additionallyEnabledNetworks = proposalNetwork.available + appInfoUiState.transformerUpdate( + WcAppInfoWalletChangedTransformer( + selectedUserWallet = selectedUserWallet, + proposalNetwork = proposalNetwork, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ) } - override fun onNetworksSelected(selectedNetworks: Set) { - additionallyEnabledNetworks.clear() - additionallyEnabledNetworks.addAll(selectedNetworks) + override fun onNetworksSelected(selectedNetworks: Set) { + additionallyEnabledNetworks = selectedNetworks + appInfoUiState.transformerUpdate( + WcNetworksSelectedTransformer( + missingNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ) } private fun createLoadingState(): WcAppInfoUM.Loading { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt index ba3466ee86..c7e1f38b60 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectNetworksModel.kt @@ -29,7 +29,7 @@ internal class WcSelectNetworksModel @Inject constructor( ) : Model() { private val params = paramsContainer.require() - private val additionallyEnabledNetworks = MutableStateFlow>( + private val additionallyEnabledNetworks = MutableStateFlow>( filterAdditionallyEnabledNetworks( availableNetworks = params.availableNetworks, additionallyEnabledNetworks = params.enabledAvailableNetworks, @@ -39,11 +39,11 @@ internal class WcSelectNetworksModel @Inject constructor( val state: StateFlow field = MutableStateFlow(getInitialState()) - private fun onCheckedChange(isChecked: Boolean, networkId: String) { + private fun onCheckedChange(isChecked: Boolean, network: Network) { additionallyEnabledNetworks.update { - if (isChecked) it.plus(Network.RawID(networkId)) else it.minus(Network.RawID(networkId)) + if (isChecked) it.plus(network) else it.minus(network) } - state.update(WcSelectNetworksCheckedTransformer(networkId, isChecked)) + state.update(WcSelectNetworksCheckedTransformer(network = network, isChecked = isChecked)) } private fun onDone() { @@ -53,12 +53,9 @@ internal class WcSelectNetworksModel @Inject constructor( private fun filterAdditionallyEnabledNetworks( availableNetworks: Set, - additionallyEnabledNetworks: Set, - ): Set { - return availableNetworks - .asSequence() - .filter { it.id.rawId in additionallyEnabledNetworks } - .mapTo(hashSetOf()) { it.id.rawId } + additionallyEnabledNetworks: Set, + ): Set { + return availableNetworks.filterTo(hashSetOf()) { it in additionallyEnabledNetworks } } private fun getInitialState(): WcSelectNetworksUM { @@ -85,8 +82,8 @@ internal class WcSelectNetworksModel @Inject constructor( icon = network.iconResId, name = network.name, symbol = network.currencySymbol, - checked = network.id.rawId in additionallyEnabledNetworks.value, - onCheckedChange = { onCheckedChange(it, network.rawId) }, + checked = network in additionallyEnabledNetworks.value, + onCheckedChange = { onCheckedChange(it, network) }, ) }.toImmutableList(), notAdded = params.notAddedNetworks.map { network -> diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 2989e66730..61d9f198d0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification @@ -18,6 +19,7 @@ internal class WcAppInfoTransformer( private val onNetworksClick: () -> Unit, private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, + private val additionallyEnabledNetworks: Set, ) : Transformer { override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { @@ -27,11 +29,17 @@ internal class WcAppInfoTransformer( verifiedDAppState = dAppVerifiedStateConverter.convert( dAppSession.securityStatus to dAppSession.dAppMetaData.name, ), - appSubtitle = dAppSession.dAppMetaData.description, + appSubtitle = dAppSession.dAppMetaData.url, notification = createNotification(dAppSession.securityStatus), walletName = userWallet.name, onWalletClick = onWalletClick, - networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + networksInfo = WcNetworksInfoConverter.convert( + value = WcNetworksInfoConverter.Input( + missingNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ), onNetworksClick = onNetworksClick, connectButtonConfig = WcPrimaryButtonConfig( showProgress = false, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt index 453ab9b608..21be3da5a5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.connections.model.transformers +import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM @@ -8,12 +9,19 @@ import com.tangem.utils.transformer.Transformer internal class WcAppInfoWalletChangedTransformer( private val selectedUserWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, + private val additionallyEnabledNetworks: Set, ) : Transformer { override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { val contentState = prevState as? WcAppInfoUM.Content ?: return prevState return contentState.copy( walletName = selectedUserWallet.name, - networksInfo = WcNetworksInfoConverter.convert(proposalNetwork), + networksInfo = WcNetworksInfoConverter.convert( + WcNetworksInfoConverter.Input( + missingNetworks = proposalNetwork.missingRequired, + requiredNetworks = proposalNetwork.required, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ), connectButtonConfig = prevState.connectButtonConfig.copy( enabled = proposalNetwork.missingRequired.isEmpty(), ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt index 85af70aefa..26934b08b7 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksInfoConverter.kt @@ -1,22 +1,21 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.tangem.core.ui.extensions.iconResId -import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.models.network.Network import com.tangem.features.walletconnect.connections.entity.WcNetworkInfoItem import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList -internal object WcNetworksInfoConverter : Converter { - override fun convert(value: WcSessionProposal.ProposalNetwork): WcNetworksInfo { - return if (value.missingRequired.isNotEmpty()) { +internal object WcNetworksInfoConverter : Converter { + override fun convert(value: Input): WcNetworksInfo { + return if (value.missingNetworks.isNotEmpty()) { WcNetworksInfo.MissingRequiredNetworkInfo( - networks = value.missingRequired - .joinToString { it.name }, + networks = value.missingNetworks.joinToString { it.name }, ) } else { WcNetworksInfo.ContainsAllRequiredNetworks( - items = (value.required + value.available) + items = (value.requiredNetworks + value.additionallyEnabledNetworks) .map { WcNetworkInfoItem.Required( id = it.rawId, @@ -29,4 +28,10 @@ internal object WcNetworksInfoConverter : Converter, + val requiredNetworks: Set, + val additionallyEnabledNetworks: Set, + ) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt new file mode 100644 index 0000000000..ee953b6130 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcNetworksSelectedTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.domain.models.network.Network +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.utils.transformer.Transformer + +internal class WcNetworksSelectedTransformer( + private val missingNetworks: Set, + private val requiredNetworks: Set, + private val additionallyEnabledNetworks: Set, +) : Transformer { + override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { + val contentState = prevState as? WcAppInfoUM.Content ?: return prevState + return contentState.copy( + connectButtonConfig = contentState.connectButtonConfig.copy( + enabled = missingNetworks.isEmpty() && + (requiredNetworks.isNotEmpty() || additionallyEnabledNetworks.isNotEmpty()), + ), + networksInfo = WcNetworksInfoConverter.convert( + value = WcNetworksInfoConverter.Input( + missingNetworks = missingNetworks, + requiredNetworks = requiredNetworks, + additionallyEnabledNetworks = additionallyEnabledNetworks, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt index 582c0465dd..c8901e01b8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSelectNetworksCheckedTransformer.kt @@ -1,17 +1,18 @@ package com.tangem.features.walletconnect.connections.model.transformers +import com.tangem.domain.models.network.Network import com.tangem.features.walletconnect.connections.entity.WcSelectNetworksUM import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toImmutableList internal class WcSelectNetworksCheckedTransformer( - private val networkId: String, + private val network: Network, private val isChecked: Boolean, ) : Transformer { override fun transform(prevState: WcSelectNetworksUM): WcSelectNetworksUM { return prevState.copy( available = prevState.available - .map { item -> if (item.id == networkId) item.copy(checked = isChecked) else item } + .map { item -> if (item.id == network.rawId) item.copy(checked = isChecked) else item } .toImmutableList(), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt index b71e892c4e..b3c3ad5b81 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt @@ -25,7 +25,7 @@ internal class WcSessionsTransformer( WcConnectedAppInfo( name = appMetaData.name, iconUrl = appMetaData.icons.firstOrNull().orEmpty(), - subtitle = stringReference(appMetaData.description), + subtitle = stringReference(appMetaData.url), verifiedState = WcDAppVerifiedStateConverter {} .convert(session.securityStatus to appMetaData.name), onClick = { openAppInfoModal(session) }, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index 9732c1e0aa..6c83d5ac07 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -21,7 +21,7 @@ internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent, Route { val missingRequiredNetworks: Set, val requiredNetworks: Set, val availableNetworks: Set, - val enabledAvailableNetworks: Set, + val enabledAvailableNetworks: Set, val notAddedNetworks: Set, ) : WcAppInfoRoutes() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 4e0a019cb7..adbaa219c2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -14,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.features.walletconnect.connections.components.AlertsComponent import com.tangem.features.walletconnect.connections.components.WcPairComponent @@ -29,6 +30,7 @@ import dagger.assisted.AssistedInject internal class DefaultWcRoutingComponent @AssistedInject constructor( @Assisted private val appComponentContext: AppComponentContext, @Assisted params: Unit, + private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, WcRoutingComponent { private val model: WcRoutingModel = getOrCreateModel() @@ -62,14 +64,17 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor( is WcInnerRoute.SignMessage -> WcSignTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.AddNetwork -> WcAddNetworkContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.Send -> WcSendTransactionContainerComponent( appComponentContext = childContext, params = WcTransactionModelParams(config.rawRequest), + feeSelectorBlockComponentFactory = feeSelectorBlockComponentFactory, ) is WcInnerRoute.Pair -> WcPairComponent( appComponentContext = childContext, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt index 1b8b649805..3150cbc5f6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -37,6 +37,7 @@ internal fun WcAppInfoItem( }, ) .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { AsyncImage( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 29f564d601..9e2490c0f9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -349,12 +349,7 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier } Icon( modifier = Modifier.size(width = 18.dp, height = 24.dp), - painter = painterResource( - id = when (networksInfo) { - is WcNetworksInfo.ContainsAllRequiredNetworks -> R.drawable.ic_select_18_24 - is WcNetworksInfo.MissingRequiredNetworkInfo -> R.drawable.ic_chevron_right_24 - }, - ), + painter = painterResource(id = R.drawable.ic_select_18_24), contentDescription = null, tint = TangemTheme.colors.icon.informative, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt index e9ab26ccf8..723b7046bf 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcAlertsFactory.kt @@ -2,16 +2,19 @@ package com.tangem.features.walletconnect.connections.utils import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal object WcAlertsFactory { - internal fun createCommonTransactionAppInfoAlertUM(alertType: WcTransactionRoutes.Alert.Type) = when (alertType) { + fun createCommonTransactionAppInfoAlertUM(alertType: WcTransactionRoutes.Alert.Type) = when (alertType) { is WcTransactionRoutes.Alert.Type.Verified -> createVerifiedDomainAlert(alertType.appName) is WcTransactionRoutes.Alert.Type.UnknownDomain -> createUnknownDomainAlert() is WcTransactionRoutes.Alert.Type.UnsafeDomain -> createUnsafeDomainAlert() + is WcTransactionRoutes.Alert.Type.MaliciousDApp -> + createMaliciousDAppAlert(alertType.description, alertType.onClick) } fun createUnknownDomainAlert(activeButtonOnClick: (() -> Unit)? = null): MessageBottomSheetUMV2 { @@ -66,6 +69,30 @@ internal object WcAlertsFactory { } } + private fun createMaliciousDAppAlert( + description: String, + activeButtonOnClick: (() -> Unit), + ): MessageBottomSheetUMV2 { + return messageBottomSheetUM { + infoBlock { + icon(R.drawable.img_knight_shield_32) { + type = MessageBottomSheetUMV2.Icon.Type.Warning + backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint + } + title = resourceReference(R.string.security_alert_title) + body = TextReference.Str(description) + } + primaryButton { + text = resourceReference(R.string.common_cancel) + onClick { closeBs() } + } + secondaryButton { + text = resourceReference(R.string.wc_alert_sign_anyway) + onClick { activeButtonOnClick() } + } + } + } + fun createVerifiedDomainAlert(appName: String): MessageBottomSheetUMV2 { return messageBottomSheetUM { infoBlock { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..00c218f576 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/PreviewFeeSelectorBlockComponent.kt @@ -0,0 +1,63 @@ +package com.tangem.features.walletconnect.transaction.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.walletconnect.impl.R + +internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { + @Composable + override fun Content(modifier: Modifier) { + Row( + modifier = modifier + .background(TangemTheme.colors.background.action) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(R.drawable.ic_fee_new_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + Text( + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Icon( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing6) + .size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_token_info_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "~ 0.25 \$", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier.size(width = 18.dp, height = 24.dp), + painter = painterResource(id = R.drawable.ic_select_18_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt index 7c9b4b33fc..86d6e850a2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/chain/WcAddNetworkContainerComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -14,7 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcAddNetworkContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, -) : WcCommonTransactionComponentDelegate(appComponentContext) { + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { private val model: WcAddNetworkModel = getOrCreateModel(params = params) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt index 0dd02cb797..87ae3eec22 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcCommonTransactionComponentDelegate.kt @@ -15,10 +15,12 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal abstract class WcCommonTransactionComponentDelegate( private val appComponentContext: AppComponentContext, + private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, ) : AppComponentContext by appComponentContext, ComposableContentComponent { private var stackNavigation: StackNavigation? = null @@ -29,6 +31,10 @@ internal abstract class WcCommonTransactionComponentDelegate( InnerRouter(stackNavigation = it, popCallback = { onChildBack() }) } } + protected val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = appComponentContext, + params = Unit, + ) protected fun init( navigation: StackNavigation, @@ -60,6 +66,7 @@ internal abstract class WcCommonTransactionComponentDelegate( is WcTransactionRoutes.Transaction, is WcTransactionRoutes.TransactionRequestInfo, is WcTransactionRoutes.Alert, + is WcTransactionRoutes.CustomAllowance, -> stackNavigation?.pop() else -> Unit } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt index d06bc28646..c38c4ce6bd 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/common/WcNavigationUtils.kt @@ -4,7 +4,9 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.features.walletconnect.connections.components.AlertsComponentV2 import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory.createCommonTransactionAppInfoAlertUM +import com.tangem.features.walletconnect.transaction.components.send.WcCustomAllowanceComponent import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel +import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal fun getWcCommonScreen( @@ -23,5 +25,12 @@ internal fun getWcCommonScreen( appComponentContext = appComponentContext, model = model, ) + is WcTransactionRoutes.CustomAllowance -> WcCustomAllowanceComponent( + appComponentContext = appComponentContext, + model = model, + onClickDoneCustomAllowance = { value, isUnlimited -> + (model as? WcSendTransactionModel)?.onClickDoneCustomAllowance(value, isUnlimited) + }, + ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt new file mode 100644 index 0000000000..afe90c5207 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcCustomAllowanceComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.walletconnect.transaction.components.send + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionModel +import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransactionUM +import com.tangem.features.walletconnect.transaction.ui.approve.WcCustomAllowanceContent +import java.math.BigDecimal + +internal class WcCustomAllowanceComponent( + private val appComponentContext: AppComponentContext, + private val model: WcCommonTransactionModel, + private val onClickDoneCustomAllowance: (BigDecimal, Boolean) -> Unit, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + val content by model.uiState.collectAsStateWithLifecycle() + val spendAllowance = (content as? WcSendTransactionUM)?.spendAllowance + if (spendAllowance != null) { + WcCustomAllowanceContent( + state = spendAllowance, + onClickDone = { value, isUnlimited -> + router.pop() + onClickDoneCustomAllowance(value, isUnlimited) + }, + onBack = router::pop, + onDismiss = ::dismiss, + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt index 648dc13467..a6b69fd2b0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionComponent.kt @@ -5,12 +5,14 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.model.WcSendTransactionModel import com.tangem.features.walletconnect.transaction.ui.send.WcSendTransactionModalBottomSheet internal class WcSendTransactionComponent( private val appComponentContext: AppComponentContext, private val model: WcSendTransactionModel, + private val feeSelectorBlockComponent: FeeSelectorBlockComponent, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { override fun dismiss() { @@ -24,9 +26,11 @@ internal class WcSendTransactionComponent( if (content != null) { WcSendTransactionModalBottomSheet( state = content!!.transaction, + feeSelectorBlockComponent = feeSelectorBlockComponent, onClickTransactionRequest = model::showTransactionRequest, onBack = router::pop, onDismiss = ::dismiss, + onClickAllowToSpend = model::onClickAllowToSpend, ) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt index 420e419ce1..e80290de33 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/send/WcSendTransactionContainerComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -14,7 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcSendTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, -) : WcCommonTransactionComponentDelegate(appComponentContext) { + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { private val model: WcSendTransactionModel = getOrCreateModel(params = params) @@ -40,6 +42,10 @@ internal class WcSendTransactionContainerComponent( } private fun createTransactionComponent(appComponentContext: AppComponentContext): WcSendTransactionComponent { - return WcSendTransactionComponent(appComponentContext = appComponentContext, model = model) + return WcSendTransactionComponent( + appComponentContext = appComponentContext, + model = model, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt index 92550465c1..4ca73d8c25 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionComponent.kt @@ -5,12 +5,14 @@ import androidx.compose.runtime.getValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel import com.tangem.features.walletconnect.transaction.ui.sign.WcSignTransactionModalBottomSheetContent internal class WcSignTransactionComponent( private val appComponentContext: AppComponentContext, private val model: WcSignTransactionModel, + private val feeSelectorBlockComponent: FeeSelectorBlockComponent, ) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { override fun dismiss() { @@ -24,6 +26,7 @@ internal class WcSignTransactionComponent( if (content != null) { WcSignTransactionModalBottomSheetContent( state = content!!.transaction, + feeSelectorBlockComponent = feeSelectorBlockComponent, onClickTransactionRequest = model::showTransactionRequest, onBack = router::pop, onDismiss = ::dismiss, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt index cdfd63915b..c9d6c4bdba 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/components/sign/WcSignTransactionContainerComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.components.common.WcCommonTransactionComponentDelegate import com.tangem.features.walletconnect.transaction.components.common.WcTransactionModelParams import com.tangem.features.walletconnect.transaction.components.common.getWcCommonScreen @@ -14,7 +15,8 @@ import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes internal class WcSignTransactionContainerComponent( appComponentContext: AppComponentContext, params: WcTransactionModelParams, -) : WcCommonTransactionComponentDelegate(appComponentContext) { + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : WcCommonTransactionComponentDelegate(appComponentContext, feeSelectorBlockComponentFactory) { private val model: WcSignTransactionModel = getOrCreateModel(params = params) @@ -40,6 +42,10 @@ internal class WcSignTransactionContainerComponent( } private fun createTransactionComponent(appComponentContext: AppComponentContext): WcSignTransactionComponent { - return WcSignTransactionComponent(appComponentContext = appComponentContext, model = model) + return WcSignTransactionComponent( + appComponentContext = appComponentContext, + model = model, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt index f7b97c0d9d..05a70969d3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionAppInfoContentUMConverter.kt @@ -15,7 +15,7 @@ internal class WcTransactionAppInfoContentUMConverter @Inject constructor() : verifiedState = WcDAppVerifiedStateConverter(onVerifiedClick = value.onShowVerifiedAlert).convert( value.session.securityStatus to value.session.sdkModel.appMetaData.name, ), - appSubtitle = value.session.sdkModel.appMetaData.description, + appSubtitle = value.session.sdkModel.appMetaData.url, ) data class Input( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt index 188c4bc5d2..241a8ba2bf 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt @@ -8,23 +8,40 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf +import org.json.JSONArray import javax.inject.Inject +private const val DATA_FIELD = "data" + internal class WcTransactionRequestBlockUMConverter @Inject constructor() : Converter { - override fun convert(value: Input) = WcTransactionRequestBlockUM( - persistentListOf( - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_signature_type), - description = value.rawSdkRequest.request.method, + override fun convert(value: Input): WcTransactionRequestBlockUM { + val params = value.rawSdkRequest.request.params + return WcTransactionRequestBlockUM( + persistentListOf( + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_signature_type), + description = value.rawSdkRequest.request.method, + ), + WcTransactionRequestInfoItemUM( + title = resourceReference(R.string.wc_contents), + description = value.signModel?.humanMsg ?: params.tryExtractData() ?: params, + ), ), - WcTransactionRequestInfoItemUM( - title = resourceReference(R.string.wc_contents), - description = value.signModel?.humanMsg ?: value.rawSdkRequest.request.params, - ), - ), - ) + ) + } + + private fun String.tryExtractData() = try { + val array = JSONArray(this) + if (array.length() > 0 && array.getJSONObject(0).has(DATA_FIELD)) { + array.getJSONObject(0).getString(DATA_FIELD) + } else { + null + } + } catch (ignore: Exception) { + null + } data class Input( val rawSdkRequest: WcSdkSessionRequest, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt deleted file mode 100644 index b8107b23c1..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcApproveTransactionUM.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.features.walletconnect.transaction.entity.approve - -import androidx.annotation.DrawableRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM - -internal data class WcApproveTransactionUM( - val transaction: WcApproveTransactionItemUM, - val transactionRequestInfo: WcTransactionRequestInfoUM, - val customAllowance: WcCustomAllowanceUM, -) - -internal data class WcApproveTransactionItemUM( - val onDismiss: () -> Unit, - val onSend: () -> Unit, - val appInfo: WcTransactionAppInfoContentUM, - val spendAllowance: WcSpendAllowanceUM? = null, - val walletName: String, - val networkInfo: WcNetworkInfoUM, - val networkFee: String? = null, - val isLoading: Boolean = false, -) : TangemBottomSheetConfigContent - -internal data class WcCustomAllowanceUM( - @DrawableRes val networkIconRes: Int, - val tokenIconUrl: String, - val amountText: String, - val isUnlimited: Boolean, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt new file mode 100644 index 0000000000..28f8405259 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcCustomAllowanceUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.walletconnect.transaction.entity.approve + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class WcCustomAllowanceUM( + @DrawableRes val networkIconRes: Int, + val tokenIconUrl: String, + val amountText: String, + val isUnlimited: Boolean, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt index d3609afa63..0bdad0e725 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt @@ -1,6 +1,14 @@ package com.tangem.features.walletconnect.transaction.entity.approve +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference +import java.math.BigDecimal + internal data class WcSpendAllowanceUM( - val amountText: String, - val tokenImageUrl: String, -) \ No newline at end of file + val amountValue: BigDecimal, + val isUnlimited: Boolean, + val amountText: TextReference, + val tokenSymbol: String, + val tokenImageUrl: String?, + val networkIconRes: Int?, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt index 418d483031..096af30724 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/blockaid/WcSendReceiveTransactionCheckResultsUM.kt @@ -1,9 +1,11 @@ package com.tangem.features.walletconnect.transaction.entity.blockaid import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM internal data class WcSendReceiveTransactionCheckResultsUM( val estimatedWalletChanges: WcEstimatedWalletChangesUM? = null, + val spendAllowance: WcSpendAllowanceUM? = null, val notificationText: TextReference? = null, val isLoading: Boolean = true, ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt index e7bfe5f339..89a9e3c554 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/send/WcSendTransactionUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.walletconnect.transaction.entity.send import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.entity.common.WcCommonTransactionUM import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM @@ -10,6 +11,7 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction internal data class WcSendTransactionUM( val transaction: WcSendTransactionItemUM, override val transactionRequestInfo: WcTransactionRequestInfoUM, + val spendAllowance: WcSpendAllowanceUM? = null, ) : WcCommonTransactionUM internal data class WcSendTransactionItemUM( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 84765ddb0b..4d8311c79c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -24,6 +24,7 @@ import com.tangem.features.walletconnect.transaction.ui.blockaid.WcSendAndReceiv import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList") @@ -46,6 +47,8 @@ internal class WcSendTransactionModel @Inject constructor( val stackNavigation = StackNavigation() + private var wcApproval: WcApproval? = null + init { @Suppress("UnusedPrivateMember") modelScope.launch { @@ -77,6 +80,7 @@ internal class WcSendTransactionModel @Inject constructor( val isSecurityCheckContent = securityCheck is Lce.Content var isApprovalMethod = isSecurityCheckContent && securityCheck.content is BlockAidTransactionCheck.Result.Approval + wcApproval = useCase as? WcApproval buildUiState(securityCheck, useCase, signState) } else -> unknownMethodRunnable() @@ -90,7 +94,9 @@ internal class WcSendTransactionModel @Inject constructor( signState: WcSignState<*>, ) { val blockAidState = when (securityCheck) { - is Lce.Content -> blockAidUiConverter.convert(securityCheck.content.result) + is Lce.Content -> blockAidUiConverter.convert( + WcSendAndReceiveBlockAidUiConverter.Input(securityCheck.content.result, wcApproval?.getAmount()), + ) is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false) is Lce.Loading -> WcSendReceiveTransactionCheckResultsUM(isLoading = true) } @@ -108,6 +114,7 @@ internal class WcSendTransactionModel @Inject constructor( ) as? WcSendTransactionUM transactionUM = transactionUM?.copy( transaction = transactionUM.transaction.copy(estimatedWalletChanges = blockAidState), + spendAllowance = blockAidState.spendAllowance, ) _uiState.emit(transactionUM) } @@ -120,6 +127,17 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo) } + fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) { + val maxValue = if (isUnlimited) Double.MAX_VALUE.toBigDecimal() else value + wcApproval?.getAmount()?.let { currentAmount -> + wcApproval?.updateAmount(currentAmount.copy(maxValue = maxValue)) + } + } + + fun onClickAllowToSpend() { + wcApproval?.let { stackNavigation.pushNew(WcTransactionRoutes.CustomAllowance) } + } + private fun showVerifiedAlert(appName: String) { stackNavigation.pushNew(WcTransactionRoutes.Alert(WcTransactionRoutes.Alert.Type.Verified(appName))) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt index b762172be6..b08fafc300 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/routes/WcTransactionRoutes.kt @@ -14,6 +14,9 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout @Serializable data object TransactionRequestInfo : WcTransactionRoutes() + @Serializable + data object CustomAllowance : WcTransactionRoutes() + @Serializable data class Alert(val type: Type) : WcTransactionRoutes() { @Serializable @@ -21,6 +24,7 @@ internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent, Rout data class Verified(val appName: String) : Type() data object UnknownDomain : Type() data object UnsafeDomain : Type() + data class MaliciousDApp(val description: String, val onClick: () -> Unit) : Type() } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt deleted file mode 100644 index 6e1ffd76e9..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcApproveTransactionModalBottomSheet.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.approve - -import android.content.res.Configuration -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -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.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.components.divider.DividerWithPadding -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.connections.entity.VerifiedDAppState -import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem -import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.approve.WcApproveTransactionItemUM -import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM -import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM -import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM -import com.tangem.features.walletconnect.transaction.ui.common.* - -@Composable -internal fun WcApproveTransactionModalBottomSheetContent( - state: WcApproveTransactionItemUM, - onClickTransactionRequest: () -> Unit, -) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .fillMaxWidth() - .animateContentSize(), - ) { - WcSmallTitleItem(R.string.wc_request_from) - WcAppInfoItem( - iconUrl = state.appInfo.appIcon, - title = state.appInfo.appName, - subtitle = state.appInfo.appSubtitle, - verifiedDAppState = state.appInfo.verifiedState, - ) - DividerWithPadding(start = 0.dp, end = 0.dp) - WcTransactionRequestItem( - iconRes = R.drawable.ic_doc_new_24, - modifier = Modifier - .fillMaxWidth() - .clickable { onClickTransactionRequest() } - .padding(TangemTheme.dimens.spacing12), - ) - } - Column(modifier = Modifier.padding(top = TangemTheme.dimens.spacing16)) { - if (state.spendAllowance != null) { - WcSpendAllowanceItem(state.spendAllowance) - Spacer(Modifier.height(TangemTheme.dimens.spacing16)) - } - WcSendTransactionItems( - walletName = state.walletName, - networkInfo = state.networkInfo, - networkFee = state.networkFee, - ) - WcTransactionRequestButtons( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), - onDismiss = state.onDismiss, - onClickActiveButton = state.onSend, - activeButtonText = resourceReference(R.string.common_send), - isLoading = state.isLoading, - ) - } - } -} - -@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 WcApproveTransactionBottomSheetPreview( - @PreviewParameter(WcApproveTransactionStateProvider::class) state: WcApproveTransactionItemUM, -) { - TangemThemePreview { - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = state, - ), - containerColor = TangemTheme.colors.background.tertiary, - title = { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.wallet_connect_title), - endIconRes = R.drawable.ic_close_24, - onEndClick = {}, - startIconRes = null, - onStartClick = {}, - ) - }, - content = { - WcApproveTransactionModalBottomSheetContent(state = state, onClickTransactionRequest = {}) - }, - ) - } -} - -private class WcApproveTransactionStateProvider : CollectionPreviewParameterProvider( - listOf( - WcApproveTransactionItemUM( - onDismiss = {}, - onSend = {}, - appInfo = WcTransactionAppInfoContentUM( - appName = "React App", - appIcon = "", - verifiedState = VerifiedDAppState.Verified {}, - appSubtitle = "react-app.walletconnect.com", - ), - spendAllowance = WcSpendAllowanceUM(amountText = "Unlimited USDT", tokenImageUrl = ""), - walletName = "Tangem 2.0", - networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), - networkFee = "~ 0.22 $", - ), - ), -) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt index 08f144454d..ffb2eea5a6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -23,91 +23,126 @@ 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.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemSwitch import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe 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.approve.WcCustomAllowanceUM +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM +import java.math.BigDecimal @Composable internal fun WcCustomAllowanceContent( - allowance: WcCustomAllowanceUM, - onAmountChange: (String) -> Unit, - onToggleChange: (Boolean) -> Unit, - onClickDone: () -> Unit, - modifier: Modifier = Modifier, + state: WcSpendAllowanceUM, + onClickDone: (BigDecimal, Boolean) -> Unit, + onBack: () -> Unit, + onDismiss: () -> Unit, ) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth() - .padding(TangemTheme.dimens.size16), - ) { - AmountTextField( - allowance = allowance, - onAmountChange = onAmountChange, - ) + var amountText by remember { mutableStateOf(state.amountValue.toPlainString()) } + var isUnlimited by remember { mutableStateOf(state.isUnlimited) } - Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) { - Text( - text = stringResourceSafe(R.string.wc_unlimited_amount), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.weight(1f), - ) - TangemSwitch( - checked = allowance.isUnlimited, - onCheckedChange = onToggleChange, - ) - } - - Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing60)) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_done), - onClick = onClickDone, - ) + val amount: BigDecimal by remember(amountText) { + mutableStateOf(amountText.toBigDecimalOrNull() ?: state.amountValue) } + + TangemModalBottomSheetWithFooter( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + containerColor = TangemTheme.colors.background.tertiary, + onBack = onBack, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_custom_allowance_title), + startIconRes = R.drawable.ic_back_24, + onStartClick = onBack, + ) + }, + content = { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .fillMaxWidth() + .padding(16.dp), + ) { + AmountTextField( + amountText = amountText, + onAmountChange = { amountText = it }, + isEnabled = !isUnlimited, + tokenImageUrl = state.tokenImageUrl, + networkIconRes = state.networkIconRes, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.background.action) + .padding(16.dp) + .fillMaxWidth(), + ) { + Text( + text = stringResourceSafe(R.string.wc_unlimited_amount), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.weight(1f), + ) + TangemSwitch( + checked = isUnlimited, + onCheckedChange = { isUnlimited = it }, + ) + } + + Spacer(modifier = Modifier.height(60.dp)) + } + }, + footer = { + PrimaryButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_done), + onClick = { onClickDone(amount, isUnlimited) }, + ) + }, + ) } @Composable private fun AmountTextField( - allowance: WcCustomAllowanceUM, + amountText: String, onAmountChange: (String) -> Unit, + isEnabled: Boolean, + tokenImageUrl: String?, + @DrawableRes networkIconRes: Int?, modifier: Modifier = Modifier, ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action) - .padding( - end = TangemTheme.dimens.size8, - top = TangemTheme.dimens.size4, - bottom = TangemTheme.dimens.size4, - ) + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.background.action) + .padding(end = 8.dp, top = 4.dp, bottom = 4.dp) .fillMaxWidth(), ) { TextField( - value = allowance.amountText, + value = amountText, onValueChange = onAmountChange, + enabled = isEnabled, label = { Text( text = stringResourceSafe(R.string.send_amount_label), @@ -127,6 +162,9 @@ private fun AmountTextField( unfocusedTextColor = TangemTheme.colors.text.primary1, focusedPlaceholderColor = TangemTheme.colors.text.disabled, unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, + disabledContainerColor = Color.Transparent, + disabledTextColor = TangemTheme.colors.text.disabled, + disabledIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, cursorColor = TangemTheme.colors.icon.primary1, @@ -134,40 +172,44 @@ private fun AmountTextField( ) TokenWithNetworkIcon( - tokenIconUrl = allowance.tokenIconUrl, - networkIconRes = allowance.networkIconRes, + tokenIconUrl = tokenImageUrl, + networkIconRes = networkIconRes, ) } } @Composable private fun TokenWithNetworkIcon( - tokenIconUrl: String, - @DrawableRes networkIconRes: Int, + tokenIconUrl: String?, + @DrawableRes networkIconRes: Int?, modifier: Modifier = Modifier, ) { Box( - modifier = modifier - .size(TangemTheme.dimens.size48), + modifier = modifier.size(48.dp), ) { AsyncImage( + model = tokenIconUrl, + contentDescription = null, modifier = Modifier .aspectRatio(1f) .clip(CircleShape) .align(Alignment.Center) - .padding(TangemTheme.dimens.spacing8), - model = tokenIconUrl, - contentDescription = null, - ) - Image( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .border(TangemTheme.dimens.size2, TangemTheme.colors.background.action, CircleShape) - .padding(TangemTheme.dimens.spacing2), - painter = painterResource(id = networkIconRes), - contentDescription = null, + .padding(8.dp), + placeholder = painterResource(R.drawable.ic_nft_placeholder_20), + error = painterResource(R.drawable.ic_nft_placeholder_20), ) + + if (networkIconRes != null) { + Image( + modifier = Modifier + .align(Alignment.TopEnd) + .size(18.dp) + .border(2.dp, TangemTheme.colors.background.action, CircleShape) + .padding(2.dp), + painter = painterResource(networkIconRes), + contentDescription = null, + ) + } } } @@ -175,10 +217,10 @@ private fun TokenWithNetworkIcon( @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) internal fun PreviewWcCustomAllowanceContent( - @PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcCustomAllowanceUM, + @PreviewParameter(WcCustomAllowanceStateProvider::class) state: WcSpendAllowanceUM, ) { TangemThemePreview { - TangemModalBottomSheet( + TangemModalBottomSheet( config = TangemBottomSheetConfig( isShown = true, onDismissRequest = {}, @@ -196,22 +238,24 @@ internal fun PreviewWcCustomAllowanceContent( }, content = { WcCustomAllowanceContent( - allowance = state, - onAmountChange = {}, - onToggleChange = {}, - onClickDone = {}, + state = state, + onClickDone = { _, _ -> }, + onBack = {}, + onDismiss = {}, ) }, ) } } -private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider( +private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvider( listOf( - WcCustomAllowanceUM( + WcSpendAllowanceUM( networkIconRes = R.drawable.img_eth_22, - tokenIconUrl = "https://tangem.com", - amountText = "100", + tokenImageUrl = "https://tangem.com", + amountText = TextReference.Str("100"), + amountValue = BigDecimal("100"), + tokenSymbol = "ETH", isUnlimited = false, ), ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt index 2c1322217f..92ec4f6e17 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcSpendAllowanceItem.kt @@ -2,6 +2,7 @@ package com.tangem.features.walletconnect.transaction.ui.approve import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -13,27 +14,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.tangem.features.walletconnect.impl.R import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem @Composable -internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: Modifier = Modifier) { +internal fun WcSpendAllowanceItem( + spendAllowance: WcSpendAllowanceUM, + onClickAllowToSpend: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .clip(RoundedCornerShape(14.dp)) .background(TangemTheme.colors.background.action) .fillMaxWidth() - .padding( - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ), + .padding(end = 12.dp, bottom = 12.dp) + .clickable { onClickAllowToSpend() }, ) { WcSmallTitleItem(R.string.wc_allow_to_spend) - Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + Spacer(modifier = Modifier.height(8.dp)) Row( verticalAlignment = Alignment.CenterVertically, @@ -46,15 +51,16 @@ internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: AsyncImage( model = spendAllowance.tokenImageUrl, modifier = Modifier - .size(TangemTheme.dimens.size24) + .padding(start = 12.dp) + .size(24.dp) .clip(CircleShape), contentDescription = null, ) - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing12)) + Spacer(modifier = Modifier.width(8.dp)) AnimatedContent( - targetState = spendAllowance.amountText, + targetState = "${spendAllowance.amountText.resolveReference()} ${spendAllowance.tokenSymbol}", label = "Animate Spend allowance text", ) { amount -> Text( @@ -75,7 +81,7 @@ internal fun WcSpendAllowanceItem(spendAllowance: WcSpendAllowanceUM, modifier: color = TangemTheme.colors.text.tertiary, ) - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing8)) + Spacer(modifier = Modifier.width(8.dp)) Icon( painter = painterResource(id = R.drawable.ic_edit_new_12), 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 index 6a8cb9829e..0ff92d4802 100644 --- 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 @@ -19,12 +19,15 @@ 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.WcSendReceiveTransactionCheckResultsUM +import com.tangem.features.walletconnect.transaction.ui.approve.WcSpendAllowanceItem import kotlinx.collections.immutable.persistentListOf @Composable -internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResultsUM, modifier: Modifier = Modifier) { - if (item.notificationText == null && item.estimatedWalletChanges == null) return - +internal fun TransactionCheckResultsItem( + item: WcSendReceiveTransactionCheckResultsUM, + onClickAllowToSpend: () -> Unit, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth(), @@ -38,6 +41,8 @@ internal fun TransactionCheckResultsItem(item: WcSendReceiveTransactionCheckResu } if (item.estimatedWalletChanges != null) { WcEstimatedWalletChangesItem(item.estimatedWalletChanges) + } else if (item.spendAllowance != null) { + WcSpendAllowanceItem(item.spendAllowance, onClickAllowToSpend) } else { WcEstimatedWalletChangesNotLoadedItem() } @@ -56,7 +61,7 @@ private fun TransactionCheckResultsItemPreview( modifier = Modifier .background(TangemTheme.colors.background.tertiary), ) { - TransactionCheckResultsItem(item = item) + TransactionCheckResultsItem(item = item, {}) } } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt index e4dcdea2ea..6e3dc9a87d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeRow.kt @@ -63,7 +63,7 @@ internal fun WcEstimatedWalletChangeRow(item: WcEstimatedWalletChangeUM, modifie } } - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(8.dp)) AsyncImage( model = item.tokenIconUrl, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt new file mode 100644 index 0000000000..c51cc78fbb --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangeUMConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import com.domain.blockaid.models.transaction.simultation.AmountInfo +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import javax.inject.Inject + +internal class WcEstimatedWalletChangeUMConverter @Inject constructor() : + Converter { + + override fun convert(value: Input): WcEstimatedWalletChangeUM { + return when (val amountInfo = value.amountInfo) { + is AmountInfo.FungibleTokens -> WcEstimatedWalletChangeUM( + iconRes = value.iconRes, + title = resourceReference(value.titleRes), + description = "${value.sign} ${amountInfo.amount.amountText()} ${amountInfo.token.symbol}", + tokenIconUrl = amountInfo.token.logoUrl, + ) + is AmountInfo.NonFungibleTokens -> WcEstimatedWalletChangeUM( + iconRes = value.iconRes, + title = resourceReference(value.titleRes), + description = amountInfo.name, + tokenIconUrl = amountInfo.logoUrl, + ) + } + } + + private fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } + + data class Input( + val amountInfo: AmountInfo, + val iconRes: Int, + val titleRes: Int, + val sign: String? = null, + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt index abd604faa3..6fee1c04e0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesLoadingItem.kt @@ -36,7 +36,7 @@ internal fun WcEstimatedWalletChangesLoadingItem(modifier: Modifier = Modifier) Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 14.dp), + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 14.dp), ) { CircularProgressIndicator( color = TangemTheme.colors.icon.accent, @@ -45,7 +45,8 @@ internal fun WcEstimatedWalletChangesLoadingItem(modifier: Modifier = Modifier) ) Text( modifier = modifier - .fillMaxWidth(), + .fillMaxWidth() + .padding(start = 8.dp), text = stringResourceSafe(R.string.wc_common_loading), color = TangemTheme.colors.text.disabled, style = TangemTheme.typography.body2, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt index 04671ce12c..de2a38ce6a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcEstimatedWalletChangesNotLoadedItem.kt @@ -35,7 +35,7 @@ internal fun WcEstimatedWalletChangesNotLoadedItem(modifier: Modifier = Modifier Text( modifier = modifier .fillMaxWidth() - .padding(start = 12.dp, bottom = 14.dp), + .padding(start = 12.dp, end = 12.dp, bottom = 14.dp), text = stringResourceSafe(R.string.wc_estimated_wallet_changes_not_simulated), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 89890d3b27..14d3abede0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -5,55 +5,75 @@ import com.domain.blockaid.models.transaction.SimulationResult import com.domain.blockaid.models.transaction.ValidationResult import com.domain.blockaid.models.transaction.simultation.AmountInfo import com.domain.blockaid.models.transaction.simultation.SimulationData +import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.features.walletconnect.impl.R -import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM 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.WcSendReceiveTransactionCheckResultsUM +import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal import javax.inject.Inject -private const val DECIMALS_AMOUNT = 2 +internal const val DECIMALS_AMOUNT = 2 -internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor() : - Converter { - override fun convert(value: CheckTransactionResult): WcSendReceiveTransactionCheckResultsUM { +@Suppress("CyclomaticComplexMethod") +internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( + private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter, + private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter, +) : Converter { + override fun convert(value: Input): WcSendReceiveTransactionCheckResultsUM { + val description = value.result.description return WcSendReceiveTransactionCheckResultsUM( isLoading = false, - notificationText = when (value.validation) { + notificationText = when (value.result.validation) { ValidationResult.SAFE, ValidationResult.FAILED_TO_VALIDATE -> null - ValidationResult.UNSAFE -> TextReference.Str("") // TODO("Add text after approve with designer") + ValidationResult.UNSAFE -> if (!description.isNullOrEmpty()) TextReference.Str(description) else null }, - estimatedWalletChanges = (value.simulation as? SimulationResult.Success)?.data?.let { data -> + estimatedWalletChanges = (value.result.simulation as? SimulationResult.Success)?.data?.let { data -> when (data) { is SimulationData.Approve -> null is SimulationData.SendAndReceive -> { val items: ImmutableList = ( data.send.map { when (it) { - is AmountInfo.FungibleTokens -> it.toUM( - iconRes = R.drawable.ic_send_new_24, - titleRes = R.string.common_send, + is AmountInfo.FungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_receive_new_24, + titleRes = R.string.common_send, + sign = StringsSigns.MINUS, + ), ) - is AmountInfo.NonFungibleTokens -> it.toUM( - iconRes = R.drawable.ic_send_new_24, - titleRes = R.string.common_send, + is AmountInfo.NonFungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_send_new_24, + titleRes = R.string.common_send, + ), ) } } + data.receive.map { when (it) { - is AmountInfo.FungibleTokens -> it.toUM( - iconRes = R.drawable.ic_receive_new_24, - titleRes = R.string.common_receive, + is AmountInfo.FungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_receive_new_24, + titleRes = R.string.common_receive, + sign = StringsSigns.PLUS, + ), ) - is AmountInfo.NonFungibleTokens -> it.toUM( - iconRes = R.drawable.ic_receive_new_24, - titleRes = R.string.common_receive, + is AmountInfo.NonFungibleTokens -> estimatedWalletChangeUMConverter.convert( + WcEstimatedWalletChangeUMConverter.Input( + amountInfo = it, + iconRes = R.drawable.ic_receive_new_24, + titleRes = R.string.common_receive, + ), ) } } @@ -62,25 +82,21 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor() : } } }, + spendAllowance = (value.result.simulation as? SimulationResult.Success)?.data?.let { data -> + when (data) { + is SimulationData.SendAndReceive -> null + is SimulationData.Approve -> value.approvedAmount?.let { + data.approvedAmounts.map { spendAllowanceUMConverter.convert(it) }.firstOrNull() + } + } + }, ) } -} -private fun AmountInfo.FungibleTokens.toUM(iconRes: Int, titleRes: Int): WcEstimatedWalletChangeUM { - val amountText = amount.format { crypto(token.symbol, DECIMALS_AMOUNT) } - return WcEstimatedWalletChangeUM( - iconRes = iconRes, - title = resourceReference(titleRes), - description = "- $amountText", - tokenIconUrl = token.logoUrl, + data class Input( + val result: CheckTransactionResult, + val approvedAmount: Amount?, ) } -private fun AmountInfo.NonFungibleTokens.toUM(iconRes: Int, titleRes: Int): WcEstimatedWalletChangeUM { - return WcEstimatedWalletChangeUM( - iconRes = iconRes, - title = resourceReference(titleRes), - description = name, - tokenIconUrl = logoUrl, - ) -} \ No newline at end of file +internal fun BigDecimal.amountText() = format { crypto("", DECIMALS_AMOUNT) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt new file mode 100644 index 0000000000..96d797734c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.features.walletconnect.transaction.ui.blockaid + +import com.domain.blockaid.models.transaction.simultation.ApprovedAmount +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { + + override fun convert(value: ApprovedAmount) = WcSpendAllowanceUM( + amountValue = value.approvedAmount, + amountText = if (value.isUnlimited) { + TextReference.Res(R.string.wc_common_unlimited) + } else { + TextReference.Str(value.approvedAmount.amountText()) + }, + isUnlimited = value.isUnlimited, + tokenSymbol = value.tokenInfo.symbol, + tokenImageUrl = value.tokenInfo.logoUrl, + networkIconRes = value.tokenInfo.chainId?.toString()?.let { getActiveIconRes(it) }, + ) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt deleted file mode 100644 index df6080b3e4..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcNetworkFeeItem.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.walletconnect.transaction.ui.common - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.walletconnect.impl.R - -@Composable -internal fun WcNetworkFeeItem(networkFeeText: String, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(R.drawable.ic_fee_new_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - - Text( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), - text = stringResourceSafe(R.string.common_network_fee_title), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing6) - .size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - - Spacer(modifier = Modifier.weight(1f)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - ) { - AnimatedContent( - targetState = networkFeeText, - label = "Animate Network fee text", - ) { fee -> - Text( - text = fee, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - ) - } - Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6)) - Icon( - modifier = Modifier - .clip(CircleShape) - .size(width = TangemTheme.dimens.size18, height = TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_select_18_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt index 0767feb930..80054def8d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSendTransactionItems.kt @@ -12,14 +12,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.divider.DividerWithPadding import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM @Composable internal fun WcSendTransactionItems( walletName: String, networkInfo: WcNetworkInfoUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, modifier: Modifier = Modifier, - networkFee: String? = null, address: String? = null, ) { Column( @@ -50,12 +51,7 @@ internal fun WcSendTransactionItems( addressText = address, ) } - if (!networkFee.isNullOrEmpty()) { - DividerWithPadding(start = TangemTheme.dimens.spacing40, end = TangemTheme.dimens.spacing12) - WcNetworkFeeItem( - modifier = itemsModifier, - networkFeeText = networkFee, - ) - } + DividerWithPadding(start = 40.dp, end = 12.dp) + feeSelectorBlockComponent.Content(Modifier) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt index 501084213c..5cff971145 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcSmallTitleItem.kt @@ -15,7 +15,7 @@ fun WcSmallTitleItem(@StringRes textRex: Int, modifier: Modifier = Modifier) { Text( modifier = modifier .fillMaxWidth() - .padding(top = 12.dp, start = 12.dp), + .padding(top = 12.dp, start = 12.dp, end = 12.dp), text = stringResourceSafe(textRex), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index f6a2782b93..9e66c41bc8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -24,9 +24,11 @@ import com.tangem.core.ui.extensions.TextReference 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.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent 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.WcSendReceiveTransactionCheckResultsUM @@ -40,12 +42,15 @@ import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequ import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import kotlinx.collections.immutable.persistentListOf +@Suppress("LongParameterList") @Composable internal fun WcSendTransactionModalBottomSheet( state: WcSendTransactionItemUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, onClickTransactionRequest: () -> Unit, onBack: () -> Unit, onDismiss: () -> Unit, + onClickAllowToSpend: () -> Unit, ) { TangemModalBottomSheetWithFooter( config = TangemBottomSheetConfig( @@ -92,13 +97,13 @@ internal fun WcSendTransactionModalBottomSheet( } Column(modifier = Modifier.padding(top = 16.dp)) { if (state.estimatedWalletChanges != null) { - TransactionCheckResultsItem(state.estimatedWalletChanges) + TransactionCheckResultsItem(state.estimatedWalletChanges, onClickAllowToSpend) } Spacer(Modifier.height(16.dp)) WcSendTransactionItems( walletName = state.walletName, networkInfo = state.networkInfo, - networkFee = state.networkFee, + feeSelectorBlockComponent = feeSelectorBlockComponent, address = state.address, ) } @@ -140,7 +145,14 @@ private fun WcSendTransactionBottomSheetPreview( ) }, content = { - WcSendTransactionModalBottomSheet(state, {}, {}, {}) + WcSendTransactionModalBottomSheet( + state = state, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + onClickTransactionRequest = {}, + onBack = {}, + onDismiss = {}, + onClickAllowToSpend = {}, + ) }, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt index 3791a02ae4..bdf2adb43a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/sign/WcSignTransactionModalBottomSheetContent.kt @@ -25,9 +25,11 @@ import com.tangem.core.ui.components.divider.DividerWithPadding 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.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent import com.tangem.features.walletconnect.transaction.entity.common.WcNetworkInfoUM import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionAppInfoContentUM import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM @@ -36,6 +38,7 @@ import com.tangem.features.walletconnect.transaction.ui.common.* @Composable internal fun WcSignTransactionModalBottomSheetContent( state: WcSignTransactionItemUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, onClickTransactionRequest: () -> Unit, onBack: () -> Unit, onDismiss: () -> Unit, @@ -84,7 +87,7 @@ internal fun WcSignTransactionModalBottomSheetContent( ) } Column(modifier = Modifier.padding(top = 16.dp)) { - WcSignTransactionItems(state) + WcSignTransactionItems(state, feeSelectorBlockComponent) } } }, @@ -101,9 +104,13 @@ internal fun WcSignTransactionModalBottomSheetContent( } @Composable -private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { +private fun WcSignTransactionItems( + state: WcSignTransactionItemUM, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + modifier: Modifier = Modifier, +) { Column( - modifier = Modifier + modifier = modifier .clip(RoundedCornerShape(14.dp)) .background(color = TangemTheme.colors.background.action) .fillMaxWidth() @@ -130,6 +137,8 @@ private fun WcSignTransactionItems(state: WcSignTransactionItemUM) { addressText = state.addressText, ) } + DividerWithPadding(start = 40.dp, end = 12.dp) + feeSelectorBlockComponent.Content(modifier = Modifier) } } @@ -157,7 +166,13 @@ private fun WcSignTransactionBottomSheetPreview( ) }, content = { - WcSignTransactionModalBottomSheetContent(state, {}, {}, {}) + WcSignTransactionModalBottomSheetContent( + state = state, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + onClickTransactionRequest = {}, + onBack = {}, + onDismiss = {}, + ) }, ) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 34c29de01a..04c6f4b039 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -3,7 +3,7 @@ [versions] # region Classpath -androidGradlePlugin = "8.2.2" +androidGradlePlugin = "8.10.1" firebaseCrashlytics = "3.0.1" googleServices = "4.4.1" kotlin = "2.1.10" diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 14daca0245..1aa479a9c8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1081" +tangemBlockchainSdk = "develop-1087" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-482" +tangemCardSdk = "develop-484" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f5e4b2a14c..911e41ca23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Feb 13 17:04:37 MSK 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts index c46c443982..c827ecc8a0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -185,6 +185,8 @@ include(":features:swap:domain") include(":features:swap:domain:models") include(":features:swap:domain:api") include(":features:swap:impl") +include(":features:swap-v2:api") +include(":features:swap-v2:impl") include(":features:tester:api") include(":features:tester:impl") @@ -243,8 +245,8 @@ include(":features:nft:impl") include(":features:walletconnect:api") include(":features:walletconnect:impl") -include(":features:fee-selector:api") -include(":features:fee-selector:impl") +include(":features:hot-wallet:api") +include(":features:hot-wallet:impl") // endregion Feature modules // region Domain modules @@ -299,6 +301,10 @@ include(":domain:blockaid:models") include(":domain:notifications") include(":domain:notifications:models") include(":domain:notifications:toggles") +include(":domain:express") +include(":domain:express:models") +include(":domain:swap") +include(":domain:swap:models") // endregion Domain modules // region Data modules @@ -328,4 +334,6 @@ include(":data:onramp") include(":data:quotes") include(":data:notifications") include(":data:blockaid") -// endregion Data modules \ No newline at end of file +// endregion Data modules +include(":data:swap") +include(":data:express") \ No newline at end of file