diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cd64023632..323d254426 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -134,7 +134,6 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.core.decompose) - implementation(projects.core.deepLinks) implementation(projects.core.error.ext) implementation(projects.libs.crypto) implementation(projects.libs.auth) @@ -280,6 +279,8 @@ dependencies { implementation(tangemDeps.card.android) { exclude(module = "joda-time") } + implementation(tangemDeps.hot.core) + implementation(tangemDeps.hot.android) /** DI */ implementation(deps.hilt.android) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index c68bec0ed0..ed9d07c577 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -39,7 +39,6 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles -import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles @@ -138,8 +137,6 @@ interface ApplicationEntryPoint { fun getWorkerFactory(): HiltWorkerFactory - fun getOnlineCardVerifier(): OnlineCardVerifier - fun getColdUserWalletBuilderFactory(): ColdUserWalletBuilder.Factory fun getApiConfigsManager(): ApiConfigsManager diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 3a4577ab02..776dbb7b61 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -26,17 +26,13 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.RoutingFeatureToggle +import com.tangem.common.routing.deeplink.DeeplinkConst.WEBLINK_KEY +import com.tangem.common.routing.deeplink.PayloadToDeeplinkConverter import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.deeplink.DEEPLINK_KEY -import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.deeplink.WEBLINK_KEY -import com.tangem.core.navigation.email.EmailSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.UiDependencies import com.tangem.data.balancehiding.DefaultDeviceFlipDetector import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode @@ -53,6 +49,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService @@ -69,7 +66,6 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel -import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig @@ -103,9 +99,6 @@ val mainScope = CoroutineScope(mainCoroutineContext) @AndroidEntryPoint class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { - @Inject - lateinit var appStateHolder: AppStateHolder - /** Router for opening tester menu */ @Inject lateinit var cardSdkOwner: CardSdkOwner @@ -122,9 +115,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var walletConnectInteractor: WalletConnectInteractor - @Inject - lateinit var deepLinksRegistry: DeepLinksRegistry - @Inject lateinit var settingsRepository: SettingsRepository @@ -143,9 +133,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var userWalletsListManager: UserWalletsListManager - @Inject - lateinit var emailSender: EmailSender - @Inject @RootAppComponentContext internal lateinit var rootComponentContext: AppComponentContext @@ -174,15 +161,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var dispatchers: CoroutineDispatcherProvider - @Inject - internal lateinit var uiDependencies: UiDependencies - @Inject internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector - @Inject - internal lateinit var routingFeatureToggle: RoutingFeatureToggle - @Inject internal lateinit var deeplinkFactory: DeepLinkFactory @@ -192,6 +173,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var urlOpener: UrlOpener + @Inject + internal lateinit var testerMenuLauncher: TesterMenuLauncher + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -244,13 +228,12 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { sendStakingUnsubmittedHashes() checkGoogleServicesAvailability() - if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) { - // handle intent only on start, not on recreate - handleDeepLink(intent = intent, isFromOnNewIntent = false) - } - lifecycle.addObserver(WindowObscurationObserver) lifecycle.addObserver(defaultDeviceFlipDetector) + + if (BuildConfig.TESTER_MENU_ENABLED) { + lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver) + } } private fun setRootContent() { @@ -481,7 +464,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } - if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) { + if (intent != null) { handleDeepLink(intent = intent, isFromOnNewIntent = false) } @@ -489,26 +472,22 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) { - if (routingFeatureToggle.isDeepLinkNavigationEnabled) { - val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() - val webLink = intent.getStringExtra(WEBLINK_KEY) + val deepLinkExtras = PayloadToDeeplinkConverter.convertBundle(intent.extras)?.toUri() + val webLink = intent.getStringExtra(WEBLINK_KEY) - val receivedDeepLink = intent.data ?: deepLinkExtras + val receivedDeepLink = intent.data ?: deepLinkExtras - when { - receivedDeepLink != null -> { - deeplinkFactory.handleDeeplink( - deeplinkUri = receivedDeepLink, - coroutineScope = lifecycleScope, - isFromOnNewIntent = isFromOnNewIntent, - ) - } - webLink?.uriValidate() == true -> { - urlOpener.openUrl(webLink) - } + when { + receivedDeepLink != null -> { + deeplinkFactory.handleDeeplink( + deeplinkUri = receivedDeepLink, + coroutineScope = lifecycleScope, + isFromOnNewIntent = isFromOnNewIntent, + ) + } + webLink?.uriValidate() == true -> { + urlOpener.openUrl(webLink) } - } else { - deepLinksRegistry.launch(intent) } } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 337a48938a..ef784f8347 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -58,7 +58,6 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles -import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.attestation.api.TangemApiServiceSettings import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -78,7 +77,6 @@ import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.* import org.rekotlin.Store -import kotlin.collections.set import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository lateinit var store: Store @@ -220,9 +218,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat .setWorkerFactory(workerFactory) .build() - private val onlineCardVerifier: OnlineCardVerifier - get() = entryPoint.getOnlineCardVerifier() - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory get() = entryPoint.getColdUserWalletBuilderFactory() @@ -367,7 +362,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat clipboardManager = clipboardManager, settingsManager = settingsManager, uiMessageSender = uiMessageSender, - onlineCardVerifier = onlineCardVerifier, coldUserWalletBuilderFactory = coldUserWalletBuilderFactory, userTokensResponseStore = userTokensResponseStore, ), diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index d8dc74443d..eacdbd1b9a 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions import com.tangem.core.analytics.Analytics import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor /** @@ -21,6 +22,15 @@ fun Analytics.setContext(scanResponse: ScanResponse) { addParamsInterceptor(LinkedCardContextInterceptor(scanResponse)) } +fun Analytics.setContext(userWallet: UserWallet) { + setUserId(userWallet.walletId.stringValue) + // TODO add product type for hot ([REDACTED_TASK_KEY]) + + if (userWallet is UserWallet.Cold) { + addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse)) + } +} + /** * Erases the context */ diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index ea9a016793..ebeec6dffc 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -15,9 +15,6 @@ import coil.executeBlocking import coil.request.ImageRequest import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage -import com.tangem.core.deeplink.DEEPLINK_KEY -import com.tangem.core.deeplink.WEBLINK_KEY -import com.tangem.core.deeplink.converter.PayloadToDeeplinkConverter import com.tangem.domain.common.LogConfig import com.tangem.tap.MainActivity import com.tangem.tap.common.images.createCoilImageLoader @@ -38,11 +35,11 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID - val deeplink = PayloadToDeeplinkConverter.convert(message.data) val intent = Intent(applicationContext, MainActivity::class.java).apply { - putExtra(DEEPLINK_KEY, deeplink) - putExtra(WEBLINK_KEY, message.data[WEBLINK_KEY]) + message.data.forEach { + putExtra(it.key, it.value) + } putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index a950925df9..d2e5f890b2 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.legacy import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.redux.LegacyAction -import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState @@ -37,8 +37,7 @@ internal object LegacyMiddleware { ) store.dispatchWithMain( DetailsAction.PrepareScreen( - // TODO [REDACTED_TASK_KEY] - scanResponse = selectedUserWallet.requireColdWallet().scanResponse, + scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse, initializedAppSettingsState = initializedAppSettingsStateContent, ), ) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index 4661ff7c91..d113493313 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -16,16 +16,13 @@ import com.tangem.crypto.bip39.Wordlist import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.datasource.api.common.config.ApiConfig -import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import com.tangem.datasource.api.common.config.managers.ApiConfigsManager -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.api.common.config.managers.MutableApiConfigsManager import com.tangem.datasource.utils.AddHeadersInterceptor import com.tangem.datasource.utils.RequestHeader import com.tangem.operations.attestation.api.TangemApiServiceSettings import com.tangem.sdk.DefaultSessionViewDelegate -import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles import com.tangem.sdk.extensions.* import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider import com.tangem.sdk.nfc.NfcManager @@ -34,13 +31,6 @@ import com.tangem.tap.foregroundActivityObserver import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.info.AppInfoProvider import com.tangem.utils.version.AppVersionProvider -import com.tangem.wallet.BuildConfig -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import javax.inject.Inject import javax.inject.Singleton @@ -55,11 +45,9 @@ import javax.inject.Singleton internal class DefaultCardSdkProvider @Inject constructor( private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, - private val cardSdkFeatureToggles: CardSdkFeatureToggles, private val apiConfigsManager: ApiConfigsManager, appVersionProvider: AppVersionProvider, appInfoProvider: AppInfoProvider, - appPreferencesStore: AppPreferencesStore, ) : CardSdkProvider, CardSdkOwner { private val observer = Observer() @@ -70,27 +58,15 @@ internal class DefaultCardSdkProvider @Inject constructor( get() = holder?.sdk ?: tryToRegisterWithForegroundActivity() init { - if (BuildConfig.TESTER_MENU_ENABLED) { - appPreferencesStore.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) - .map { - when (it[ApiConfig.ID.TangemTech.name]) { - ApiEnvironment.DEV, - ApiEnvironment.STAGE, - ApiEnvironment.MOCK, - -> false - ApiEnvironment.PROD, - null, - -> true - } + val mutableManager = apiConfigsManager as? MutableApiConfigsManager + + mutableManager?.addListener( + object : MutableApiConfigsManager.ApiConfigEnvChangeListener(id = ApiConfig.ID.TangemTech) { + override fun onChange(environmentConfig: ApiEnvironmentConfig) { + holder?.sdk?.config?.tangemApiBaseUrl = environmentConfig.baseUrl } - .distinctUntilChanged() - .onEach { isProd -> - holder?.let { - it.sdk.config.isTangemAttestationProdEnv = isProd - } - } - .launchIn(CoroutineScope(SupervisorJob() + dispatchers.main)) - } + }, + ) TangemApiServiceSettings.addInterceptors( AddHeadersInterceptor( @@ -188,10 +164,8 @@ internal class DefaultCardSdkProvider @Inject constructor( keystoreManager = keystoreManager, wordlist = Wordlist.getWordlist(activity), config = config.apply { - isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled - val apiConfig = apiConfigsManager.getEnvironmentConfig(id = ApiConfig.ID.TangemTech) - isTangemAttestationProdEnv = apiConfig.environment == ApiEnvironment.PROD + tangemApiBaseUrl = apiConfig.baseUrl }, ) diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index e5627b864f..e963c1f140 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -5,7 +5,6 @@ import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -49,7 +48,6 @@ internal object ActivityModule { appStateHolder: AppStateHolder, expressServiceLoader: ExpressServiceLoader, currenciesRepository: CurrenciesRepository, - getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, excludedBlockchains: ExcludedBlockchains, dispatchers: CoroutineDispatcherProvider, ): RampStateManager { @@ -57,7 +55,6 @@ internal object ActivityModule { sellService = Provider { requireNotNull(appStateHolder.sellService) }, expressServiceLoader = expressServiceLoader, currenciesRepository = currenciesRepository, - getNetworkCoinStatusUseCase = getNetworkCoinStatusUseCase, dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, ) 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 43391888d5..d89dd6d8ce 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 @@ -7,7 +7,6 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher 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.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -72,7 +71,6 @@ internal object ManageTokensDomainModule { walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, derivationsRepository: DerivationsRepository, - stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, @@ -84,7 +82,6 @@ internal object ManageTokensDomainModule { walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, derivationsRepository = derivationsRepository, - stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, 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 d23de84aaf..1dab6fc6f5 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 @@ -10,8 +10,6 @@ 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 -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import dagger.Module @@ -62,21 +60,17 @@ object MarketsDomainModule { derivationsRepository: DerivationsRepository, marketsTokenRepository: MarketsTokenRepository, currenciesRepository: CurrenciesRepository, - stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( derivationsRepository = derivationsRepository, marketsTokenRepository = marketsTokenRepository, currenciesRepository = currenciesRepository, - stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 22f61dc1f1..b6152265bf 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -94,12 +94,10 @@ internal object StakingDomainModule { @Provides @Singleton fun provideFetchStakingYieldBalanceUseCase( - stakingRepository: StakingRepository, stakingErrorResolver: StakingErrorResolver, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, ): FetchStakingYieldBalanceUseCase { return FetchStakingYieldBalanceUseCase( - stakingRepository = stakingRepository, stakingErrorResolver = stakingErrorResolver, singleYieldBalanceFetcher = singleYieldBalanceFetcher, ) @@ -224,4 +222,12 @@ internal object StakingDomainModule { ): CheckAccountInitializedUseCase { return CheckAccountInitializedUseCase(walletManagersFacade) } + + @Provides + @Singleton + fun provideGetActionRequirementAmountUseCase( + stakingRepository: StakingRepository, + ): GetActionRequirementAmountUseCase { + return GetActionRequirementAmountUseCase(stakingRepository) + } } \ No newline at end of file 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 f0ba15b856..8cf51ccfe1 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 @@ -3,10 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.swap.SwapErrorResolver import com.tangem.domain.swap.SwapRepositoryV2 import com.tangem.domain.swap.SwapTransactionRepository -import com.tangem.domain.swap.usecase.GetSwapPairsUseCase -import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase -import com.tangem.domain.swap.usecase.GetSwapSupportedPairsUseCase -import com.tangem.domain.swap.usecase.SelectInitialPairUseCase +import com.tangem.domain.swap.usecase.* import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import dagger.Module import dagger.Provides @@ -73,4 +70,30 @@ internal object SwapDomainModule { swapErrorResolver = swapErrorResolver, ) } + + @Provides + @Singleton + fun provideGetSwapDataUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapErrorResolver: SwapErrorResolver, + ): GetSwapDataUseCase { + return GetSwapDataUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapErrorResolver = swapErrorResolver, + ) + } + + @Provides + @Singleton + fun provideSwapTransactionSentUseCase( + swapRepositoryV2: SwapRepositoryV2, + swapTransactionRepository: SwapTransactionRepository, + swapErrorResolver: SwapErrorResolver, + ): SwapTransactionSentUseCase { + return SwapTransactionSentUseCase( + swapRepositoryV2 = swapRepositoryV2, + swapTransactionRepository = swapTransactionRepository, + 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 f83422933e..eddbbc0fd4 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 @@ -41,7 +41,6 @@ internal object TokensDomainModule { @Singleton fun provideAddCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, - stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, @@ -50,7 +49,6 @@ internal object TokensDomainModule { ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, - stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, @@ -63,19 +61,15 @@ internal object TokensDomainModule { @Singleton fun provideFetchTokenListUseCase( currenciesRepository: CurrenciesRepository, - stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): FetchTokenListUseCase { return FetchTokenListUseCase( currenciesRepository = currenciesRepository, - stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -173,7 +167,6 @@ internal object TokensDomainModule { @Singleton fun provideFetchCurrencyStatusUseCase( currenciesRepository: CurrenciesRepository, - stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, singleYieldBalanceFetcher: SingleYieldBalanceFetcher, @@ -182,7 +175,6 @@ internal object TokensDomainModule { ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( currenciesRepository = currenciesRepository, - stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, singleYieldBalanceFetcher = singleYieldBalanceFetcher, @@ -195,19 +187,15 @@ internal object TokensDomainModule { @Singleton fun provideFetchCardTokenListUseCase( currenciesRepository: CurrenciesRepository, - stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( currenciesRepository = currenciesRepository, - stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiYieldBalanceFetcher = multiYieldBalanceFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -256,20 +244,16 @@ internal object TokensDomainModule { fun provideGetCryptoCurrencyActionsUseCase( rampStateManager: RampStateManager, walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, promoRepository: PromoRepository, dispatchers: CoroutineDispatcherProvider, - currencyStatusOperations: BaseCurrencyStatusOperations, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( rampManager = rampStateManager, walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, promoRepository = promoRepository, dispatchers = dispatchers, - currencyStatusOperations = currencyStatusOperations, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 0e1a88fec9..73b6077ca8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -11,6 +11,7 @@ import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.tap.domain.hot.TangemHotSigner import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -43,6 +44,7 @@ internal object TransactionDomainModule { transactionRepository: TransactionRepository, walletManagersFacade: WalletManagersFacade, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + tangemHotSignerFactory: TangemHotSigner.Factory, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig(), @@ -50,6 +52,7 @@ internal object TransactionDomainModule { transactionRepository = transactionRepository, walletManagersFacade = walletManagersFacade, singleNetworkStatusFetcher = singleNetworkStatusFetcher, + getHotSigner = tangemHotSignerFactory::create, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 684a7f0cf7..26467ac00e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -15,10 +15,7 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase -import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.features.nft.NFTFeatureToggles import com.tangem.operations.attestation.CardArtworksProvider -import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -191,28 +188,14 @@ internal object WalletsDomainModule { @Provides @Singleton - fun provideGetCardImageUseCase( - onlineCardVerifier: OnlineCardVerifier, - cardArtworksProvider: CardArtworksProvider, - cardSdkFeatureToggles: CardSdkFeatureToggles, - ): GetCardImageUseCase { - return GetCardImageUseCase( - verifier = onlineCardVerifier, - cardArtworksProvider = cardArtworksProvider, - cardSdkFeatureToggles = cardSdkFeatureToggles, - ) + fun provideGetCardImageUseCase(cardArtworksProvider: CardArtworksProvider): GetCardImageUseCase { + return GetCardImageUseCase(cardArtworksProvider = cardArtworksProvider) } @Provides @Singleton - fun providesIsWalletNFTEnabledSyncUseCase( - walletsRepository: WalletsRepository, - nftFeatureToggles: NFTFeatureToggles, - ): IsWalletNFTEnabledSyncUseCase { - return IsWalletNFTEnabledSyncUseCase( - walletsRepository = walletsRepository, - nftFeatureToggles = nftFeatureToggles, - ) + fun providesIsWalletNFTEnabledSyncUseCase(walletsRepository: WalletsRepository): IsWalletNFTEnabledSyncUseCase { + return IsWalletNFTEnabledSyncUseCase(walletsRepository = walletsRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt new file mode 100644 index 0000000000..f68e642f94 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/hot/TangemHotSdkModule.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.di.hot + +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.tap.domain.hot.HotWalletPasswordRequester +import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester +import com.tangem.tap.features.hot.TangemHotSDKProxy +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 TangemHotSdkModule { + + @Binds + @Singleton + fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk + + @Binds + @Singleton + fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt index a95acf5cd6..3bfc0b3a44 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -1,9 +1,7 @@ package com.tangem.tap.di.routing import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.api.AnalyticsExceptionHandler -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.tap.routing.ProxyAppRouter import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.MutableAppRouterConfig @@ -33,10 +31,4 @@ internal object AppRouterModule { @Provides @Singleton fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig() - - @Provides - @Singleton - fun provideRoutingFeatureToggle(featureTogglesManager: FeatureTogglesManager): RoutingFeatureToggle { - return RoutingFeatureToggle(featureTogglesManager) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index b812453401..c745f899b4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.Wallet import com.tangem.core.analytics.Analytics import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store @@ -35,14 +34,15 @@ class TapWalletManager( } private suspend fun loadUserWalletData(userWallet: UserWallet) { - Analytics.setContext(userWallet.requireColdWallet().scanResponse) // [REDACTED_TASK_KEY] - val scanResponse = userWallet.scanResponse + Analytics.setContext(userWallet) - tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) - - withMainContext { - // Order is important - store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) + if (userWallet is UserWallet.Cold) { + val scanResponse = userWallet.scanResponse + tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) + withMainContext { + // Order is important + store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) + } } } } diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 4c6e048c73..f0c52c1fb2 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -50,7 +50,7 @@ internal class DefaultDerivationsRepository( networkFactory.create( blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) }, ) @@ -61,7 +61,11 @@ internal class DefaultDerivationsRepository( userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") } - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + if (userWallet is UserWallet.Hot) { + return + } + + userWallet.requireColdWallet() if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { Timber.d("Nothing to derive") @@ -84,14 +88,18 @@ internal class DefaultDerivationsRepository( ): Boolean { val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") + if (userWallet is UserWallet.Hot) { + return false + } + val derivations = - MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY] + MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse) .findByNetworks( networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) -> networkFactory.create( blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null, extraDerivationPath = extraDerivationPath, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) }, ) diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt deleted file mode 100644 index 547dfbd6ce..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.domain.extensions - -import com.tangem.common.extensions.toHexString -import com.tangem.common.services.Result -import com.tangem.domain.common.TwinCardNumber -import com.tangem.domain.common.getTwinCardNumber -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.wallets.models.Artwork -import com.tangem.operations.attestation.CardArtworksProvider -import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo - -fun CardDTO.signedHashesCount(): Int { - return wallets.sumOf { it.totalSignedHashes ?: 0 } -} - -suspend fun CardDTO.getOrLoadCardArtworkUrl( - cardInfo: Result? = null, - onlineCardVerifier: OnlineCardVerifier, -): String { - fun ifAnyError(): String { - return when { - cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL - cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL - else -> { - when (getTwinCardNumber()) { - TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL - TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL - else -> Artwork.DEFAULT_IMG_URL - } - } - } - } - - return when (val cardInfoResult = cardInfo ?: onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) { - is Result.Success -> { - val artworkId = cardInfoResult.data.artwork?.id - if (artworkId.isNullOrEmpty()) { - ifAnyError() - } else { - CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) - } - } - - is Result.Failure -> ifAnyError() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt new file mode 100644 index 0000000000..5014f950b6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt @@ -0,0 +1,44 @@ +package com.tangem.tap.domain.hot + +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.* +import javax.inject.Inject + +class HotWalletAccessor @Inject constructor( + private val tangemHotSdk: TangemHotSdk, + private val hotWalletPasswordRequester: HotWalletPasswordRequester, +) { + + suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List { + val auth = when (hotWalletId.authType) { + HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth + HotWalletId.AuthType.Password -> { + hotWalletPasswordRequester.requestPassword(hotWalletId) + } + HotWalletId.AuthType.Biometry -> HotAuth.Biometry + } + + return runCatching { + tangemHotSdk.signHashes( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = auth, + ), + dataToSign = dataToSign, + ) + }.getOrElse { + if (hotWalletId.authType == HotWalletId.AuthType.Biometry) { + val passwordAuth = hotWalletPasswordRequester.requestPassword(hotWalletId) + tangemHotSdk.signHashes( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = passwordAuth, + ), + dataToSign = dataToSign, + ) + } else { + throw it + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt new file mode 100644 index 0000000000..1f80fd785f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletPasswordRequester.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.domain.hot + +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId + +interface HotWalletPasswordRequester { + + suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt b/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt new file mode 100644 index 0000000000..d70815434d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/hot/TangemHotSigner.kt @@ -0,0 +1,78 @@ +package com.tangem.tap.domain.hot + +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.Wallet +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError +import com.tangem.common.map +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.hot.sdk.model.DataToSign +import com.tangem.operations.sign.SignData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class TangemHotSigner @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Hot, + private val hotWalletAccessor: HotWalletAccessor, +) : TransactionSigner { + + override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult { + return sign(listOf(hash), publicKey).map { it.first() } + } + + override suspend fun sign( + hashes: List, + publicKey: Wallet.PublicKey, + ): CompletionResult> { + val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey } + ?: return CompletionResult.Failure( + TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), + ) + + val result = hotWalletAccessor.signHashes( + hotWalletId = userWallet.hotWalletId, + dataToSign = listOf( + DataToSign( + curve = wallet.curve, + hashes = hashes, + derivationPath = publicKey.derivationPath, + ), + ), + ) + + return CompletionResult.Success(result.map { it.signatures }.flatten()) + } + + override suspend fun multiSign( + dataToSign: List, + publicKey: Wallet.PublicKey, + ): CompletionResult> { + val result = hotWalletAccessor.signHashes( + hotWalletId = userWallet.hotWalletId, + dataToSign = dataToSign.map { signData -> + val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey } + ?: return CompletionResult.Failure( + TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), + ) + + DataToSign( + curve = wallet.curve, + hashes = listOf(signData.hash), + derivationPath = signData.derivationPath, + ) + }, + ) + + return CompletionResult.Success( + result.mapIndexed { index, data -> + dataToSign[index].publicKey to data.signatures.first() + }.toMap(), + ) + } + + @AssistedFactory + interface Factory { + fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 1612d89f64..49ac95c749 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -7,9 +7,6 @@ internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokensFeatureToggles { - override val isStakingLoadingRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_LOADING_REFACTORING_ENABLED") - override val isWalletBalanceFetcherEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "WALLET_BALANCE_FETCHER_ENABLED") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index 2d52d344fe..f67330ee89 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -7,7 +7,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isLocked -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository @@ -208,8 +207,7 @@ internal class BiometricUserWalletsListManager( changeSelectedUserWallet: Boolean, canOverridePublicInfo: Boolean, ): CompletionResult { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - val encryptionKey = userWallet.scanResponse.card.encryptionKey + val encryptionKey = userWallet.encryptionKey ?.let { UserWalletEncryptionKey(userWallet.walletId, it) } ?: return CompletionResult.Success(Unit) // No encryption key, no need to save diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt index 95e75741b7..b4ff819375 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt @@ -2,31 +2,44 @@ package com.tangem.tap.domain.userWalletList.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.domain.models.MobileWallet import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.hot.sdk.model.HotWalletId @JsonClass(generateAdapter = true) internal data class UserWalletSensitiveInformation( + // Cold @Json(name = "wallets") - val wallets: List, + val wallets: List?, @Json(name = "visaCardActivationStatus") val visaCardActivationStatus: VisaCardActivationStatus? = null, + // Hot + @Json(name = "mobileWallets") + val mobileWallets: List? = null, ) @JsonClass(generateAdapter = true) internal data class UserWalletPublicInformation( + // Common @Json(name = "name") val name: String, @Json(name = "walletId") val walletId: UserWalletId, + // Cold @Json(name = "cardsInWallet") val cardsInWallet: Set, @Json(name = "scanResponse") - val scanResponse: ScanResponse, + val scanResponse: ScanResponse?, @Json(name = "isMultiCurrency") val isMultiCurrency: Boolean, @Json(name = "hasBackupError") val hasBackupError: Boolean = false, + // Hot + @Json(name = "hotWalletId") + val hotWalletId: HotWalletId? = null, + @Json(name = "backedUp") + val backedUp: Boolean? = null, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index df0951fab2..e22b7167a4 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -2,6 +2,7 @@ package com.tangem.tap.domain.userWalletList.utils import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation @@ -10,8 +11,12 @@ internal val UserWallet.sensitiveInformation: UserWalletSensitiveInformation is UserWallet.Cold -> UserWalletSensitiveInformation( wallets = scanResponse.card.wallets, visaCardActivationStatus = scanResponse.visaCardActivationStatus, + mobileWallets = null, + ) + is UserWallet.Hot -> UserWalletSensitiveInformation( + wallets = null, + mobileWallets = this.wallets, ) - is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") } internal val UserWallet.publicInformation: UserWalletPublicInformation @@ -28,19 +33,39 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation visaCardActivationStatus = null, ), hasBackupError = hasBackupError, + hotWalletId = null, + backedUp = null, + ) + is UserWallet.Hot -> UserWalletPublicInformation( + name = name, + walletId = walletId, + isMultiCurrency = isMultiCurrency, + cardsInWallet = emptySet(), + scanResponse = null, + hotWalletId = hotWalletId, + backedUp = backedUp, ) - is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") } internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { - return UserWallet.Cold( - name = name, - walletId = walletId, - cardsInWallet = cardsInWallet, - scanResponse = scanResponse, - isMultiCurrency = isMultiCurrency, - hasBackupError = hasBackupError, - ) + return if (hotWalletId != null) { + UserWallet.Hot( + name = name, + walletId = walletId, + hotWalletId = hotWalletId, + wallets = null, + backedUp = backedUp!!, + ) + } else { + UserWallet.Cold( + name = name, + walletId = walletId, + cardsInWallet = cardsInWallet, + scanResponse = scanResponse!!, + isMultiCurrency = isMultiCurrency, + hasBackupError = hasBackupError, + ) + } } internal fun List.toUserWallets(): List { @@ -53,13 +78,15 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo copy( scanResponse = scanResponse.copy( card = scanResponse.card.copy( - wallets = sensitiveInformation.wallets, + wallets = sensitiveInformation.wallets!!, ), visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus, ), ) } - is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") + is UserWallet.Hot -> copy( + wallets = sensitiveInformation.mobileWallets, + ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt index f58758f35a..19530de594 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt @@ -1,11 +1,16 @@ package com.tangem.tap.domain.userWalletList.utils import com.tangem.common.extensions.calculateSha256 -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.common.extensions.calculateHmacSha256 +import com.tangem.domain.models.MobileWallet +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.models.UserWallet -internal val CardDTO.encryptionKey: ByteArray? - get() = findPublicKey(wallets)?.let { calculateEncryptionKey(it) } +internal val UserWallet.encryptionKey: ByteArray? + get() = when (this) { + is UserWallet.Cold -> findPublicKey(this.scanResponse.card.wallets) + is UserWallet.Hot -> findPublicKey(this.wallets.orEmpty()) + }?.let { calculateEncryptionKey(it) } private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray { val message = MESSAGE_FOR_ENCRYPTION_KEY.toByteArray() @@ -15,8 +20,12 @@ private fun calculateEncryptionKey(publicKey: ByteArray): ByteArray { } private fun findPublicKey(wallets: List): ByteArray? { - return wallets.firstOrNull() - ?.publicKey + return wallets.firstOrNull()?.publicKey +} + +@JvmName("findPublicKeyInMobileWallets") +private fun findPublicKey(wallets: List): ByteArray? { + return wallets.firstOrNull()?.publicKey } private const val MESSAGE_FOR_ENCRYPTION_KEY = "UserWalletEncryptionKey" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 069e6089e9..918c4bb5e5 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -14,7 +14,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.extensions.dispatchOnMain @@ -422,8 +421,7 @@ class WalletConnectInteractor( } private fun getCardId(userWallet: UserWallet): String? { - userWallet.requireColdWallet() // [REDACTED_TASK_KEY] - return if (userWallet.scanResponse.card.backupStatus?.isActive != true) { + return if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.backupStatus?.isActive != true) { userWallet.cardId } else { // if wallet has backup, any card from wallet can be used to sign null diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 56b822cc8e..d398da42c9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -9,7 +9,7 @@ import org.rekotlin.Action sealed class DetailsAction : Action { data class PrepareScreen( - val scanResponse: ScanResponse, + val scanResponse: ScanResponse?, val initializedAppSettingsState: AppSettingsState, ) : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/DefaultCardSettingsComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/DefaultCardSettingsComponent.kt index bb92d1a9a1..5c31eb375c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/DefaultCardSettingsComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/DefaultCardSettingsComponent.kt @@ -22,7 +22,6 @@ internal class DefaultCardSettingsComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val state by model.screenState.collectAsStateWithLifecycle() - CardSettingsScreen(modifier = modifier, state = state) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 3ae653038c..babdcb09be 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -27,7 +27,6 @@ import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.features.details.ui.cardsettings.CardInfo import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent @@ -75,6 +74,8 @@ internal class CardSettingsModel @Inject constructor( override fun onDestroy() { super.onDestroy() + // Reset card scanned data + cardSettingsInteractor.clear() // Restore the previous value of access code request policy cardSdkConfigRepository.isBiometricsRequestPolicy = previousBiometricsRequestPolicy } @@ -169,6 +170,8 @@ internal class CardSettingsModel @Inject constructor( } } + private fun CardDTO.signedHashesCount(): Int = wallets.sumOf { it.totalSignedHashes ?: 0 } + private fun handleClickingItem(item: CardInfo) { when (item) { is CardInfo.ChangeAccessCode -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt index 762b7afb71..a231426ebb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -31,12 +32,18 @@ internal class WalletConnectModel @Inject constructor( init { modelScope.launch { - listenToQrScanningUseCase(SourceType.WALLET_CONNECT) + listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .map { + .map { result -> + val source = when (result.resultSource) { + QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD + QrResultSource.CAMERA, + QrResultSource.GALLERY, + -> WalletConnectAction.OpenSession.SourceType.QR + } WalletConnectAction.OpenSession( - wcUri = it, - source = WalletConnectAction.OpenSession.SourceType.QR, + wcUri = result.qrCode, + source = source, userWalletId = params.userWalletId, ) } 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 ade00b0024..b89b6f5255 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 @@ -13,6 +13,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect import com.tangem.core.ui.utils.findActivity +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.api.HomeComponent import com.tangem.tap.features.home.compose.StoriesScreen @@ -29,6 +30,7 @@ import org.rekotlin.StoreSubscriber internal class DefaultHomeComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: Unit, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber { private val model: HomeModel = getOrCreateModel() @@ -58,7 +60,7 @@ internal class DefaultHomeComponent @AssistedInject constructor( val activity = LocalContext.current.findActivity() BackHandler(onBack = activity::finish) SystemBarsIconsDisposable(darkIcons = false) - if (homeState.value.isV2StoriesEnabled) { + if (hotWalletFeatureToggles.isHotWalletEnabled) { StoriesScreenV2( homeState = homeState, onCreateNewWalletButtonClick = model::onCreateNewWalletScreen, 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 d36ea27e67..b1d45779ba 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,7 +7,6 @@ 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/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt b/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt new file mode 100644 index 0000000000..b9d1c6ae6c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/hot/DefaultHotWalletPasswordRequester.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.features.hot + +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.tap.domain.hot.HotWalletPasswordRequester +import javax.inject.Inject + +class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester { + + override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password { + return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt new file mode 100644 index 0000000000..9cc8db4191 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/hot/TangemHotSDKProxy.kt @@ -0,0 +1,53 @@ +package com.tangem.tap.features.hot + +import com.tangem.crypto.bip39.Mnemonic +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Proxy for [TangemHotSdk] to allow lazy initialization and provide a way to access the SDK state from domain layer. + * SDK is initialized in the [com.tangem.tap.routing.component.RoutingComponent] and can be accessed through this proxy. + * Be aware that the SDK is initialized on activity creation, so it may not be available immediately. + */ +@Singleton +class TangemHotSDKProxy @Inject constructor() : TangemHotSdk { + + val sdkState = MutableStateFlow(null) + + override suspend fun importWallet(mnemonic: Mnemonic, passphrase: CharArray?, auth: HotAuth): HotWalletId = + callSdk { importWallet(mnemonic, passphrase, auth) } + + override suspend fun generateWallet(auth: HotAuth, mnemonicType: MnemonicType): HotWalletId = + callSdk { generateWallet(auth, mnemonicType) } + + override suspend fun exportMnemonic(unlockHotWallet: UnlockHotWallet): SeedPhrasePrivateInfo = + callSdk { exportMnemonic(unlockHotWallet) } + + override suspend fun exportBackup(unlockHotWallet: UnlockHotWallet): ByteArray = + callSdk { exportBackup(unlockHotWallet) } + + override suspend fun delete(id: HotWalletId) = callSdk { delete(id) } + + override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId = + callSdk { changeAuth(unlockHotWallet, auth) } + + override suspend fun derivePublicKey( + unlockHotWallet: UnlockHotWallet, + request: DeriveWalletRequest, + ): DerivedPublicKeyResponse = callSdk { derivePublicKey(unlockHotWallet, request) } + + override suspend fun signHashes(unlockHotWallet: UnlockHotWallet, dataToSign: List): List = + callSdk { signHashes(unlockHotWallet, dataToSign) } + + private suspend fun callSdk(block: suspend TangemHotSdk.() -> T): T { + return withTimeout(timeMillis = 1000) { + sdkState.filterNotNull().first() + }.block() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 832de3675c..2991faccbd 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -4,13 +4,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.common.keyboard.KeyboardValidator -import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.ui.R import com.tangem.core.ui.coil.ImagePreloader import com.tangem.core.ui.extensions.resourceReference @@ -46,7 +44,6 @@ import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCas import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents -import com.tangem.features.onramp.deeplink.OnrampDeepLink import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog @@ -85,8 +82,6 @@ internal class MainViewModel @Inject constructor( private val imagePreloader: ImagePreloader, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, private val onboardingRepository: OnboardingRepository, - private val deepLinksRegistry: DeepLinksRegistry, - private val onrampDeepLinkFactory: OnrampDeepLink.Factory, private val notificationsToggles: NotificationsFeatureToggles, private val getApplicationIdUseCase: GetApplicationIdUseCase, private val subscribeOnWalletsUseCase: GetSavedWalletChangesUseCase, @@ -97,7 +92,6 @@ internal class MainViewModel @Inject constructor( private val multiQuoteUpdater: MultiQuoteUpdater, private val appStateHolder: AppStateHolder, private val environmentConfigStorage: EnvironmentConfigStorage, - routingFeatureToggle: RoutingFeatureToggle, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -140,10 +134,6 @@ internal class MainViewModel @Inject constructor( sendKeyboardIdentifierEvent() preloadImages() - - if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { - initializeDeepLinks() - } } override fun onCleared() { @@ -200,7 +190,7 @@ internal class MainViewModel @Inject constructor( userWalletsListManager.selectedUserWallet .distinctUntilChanged() .onEach { userWallet -> - Analytics.setContext(userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY] + Analytics.setContext(userWallet) } .flowOn(dispatchers.io) .launchIn(viewModelScope) @@ -417,7 +407,7 @@ internal class MainViewModel @Inject constructor( } } } catch (ex: Exception) { - Timber.e(ex.message) + Timber.e(ex) analyticsEventHandler.send( StoriesEvents.Error( type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, @@ -426,10 +416,6 @@ internal class MainViewModel @Inject constructor( } } - private fun initializeDeepLinks() { - deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope)) - } - private suspend fun initPushNotifications() { if (notificationsToggles.isNotificationsEnabled) { getApplicationIdUseCase().onRight { applicationId -> diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index ef31519427..0f95bec6a9 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -15,7 +15,7 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable -import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.backupService import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.extensions.* @@ -95,7 +95,7 @@ internal class WelcomeMiddleware { } .doOnSuccess { selectedUserWallet -> sendSignedInAnalyticsEvent( - scanResponse = selectedUserWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = selectedUserWallet, signInType = Basic.SignedIn.SignInType.Biometric, ) @@ -129,7 +129,7 @@ internal class WelcomeMiddleware { store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error)) } .doOnSuccess { - sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card) + sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedIn.SignInType.Card) store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) @@ -142,7 +142,14 @@ internal class WelcomeMiddleware { } } - private fun sendSignedInAnalyticsEvent(scanResponse: ScanResponse, signInType: Basic.SignedIn.SignInType) { + private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) { + // TODO [REDACTED_TASK_KEY] + + if (userWallet !is UserWallet.Cold) { + return + } + + val scanResponse = userWallet.scanResponse val currency = ParamCardCurrencyConverter().convert( value = scanResponse.cardTypesResolver, ) diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 2820d0ed44..2143401575 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -4,18 +4,27 @@ import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet internal class DefaultAuthProvider(private val userWalletsListManager: UserWalletsListManager) : AuthProvider { override fun getCardPublicKey(): String { - return userWalletsListManager.selectedUserWalletSync - ?.requireColdWallet()?.scanResponse?.card?.cardPublicKey?.toHexString() ?: "" + val userWallet = userWalletsListManager.selectedUserWalletSync + + if (userWallet !is UserWallet.Cold) { + return "" + } + + return userWallet.scanResponse.card.cardPublicKey.toHexString() } override fun getCardId(): String { - return userWalletsListManager.selectedUserWalletSync - ?.requireColdWallet()?.scanResponse?.card?.cardId ?: "" + val userWallet = userWalletsListManager.selectedUserWalletSync + + if (userWallet !is UserWallet.Cold) { + return "" + } + + return userWallet.scanResponse.card.cardId } override fun getCardsPublicKeys(): Map { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index e69337350b..f71c4160f9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency import com.tangem.tap.proxy.redux.DaggerGraphState @@ -25,10 +24,8 @@ internal class CryptoCurrencyConverter( cryptoCurrencyFactory.createCoin( blockchain = value.blockchain, extraDerivationPath = value.derivationPath, - scanResponse = requireNotNull( - store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync - ?.requireColdWallet() // TODO [REDACTED_TASK_KEY] - ?.scanResponse, + userWallet = requireNotNull( + store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync, ), ), ) @@ -37,10 +34,8 @@ internal class CryptoCurrencyConverter( sdkToken = value.token, blockchain = value.blockchain, extraDerivationPath = value.derivationPath, - scanResponse = requireNotNull( - store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync - ?.requireColdWallet() // TODO [REDACTED_TASK_KEY] - ?.scanResponse, + userWallet = requireNotNull( + store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 05b844c54b..5049b25bb6 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -13,8 +13,6 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -32,7 +30,6 @@ internal class DefaultRampManager( private val sellService: Provider, private val expressServiceLoader: ExpressServiceLoader, private val currenciesRepository: CurrenciesRepository, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : RampStateManager { @@ -40,11 +37,10 @@ internal class DefaultRampManager( private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains) override suspend fun availableForBuy( - scanResponse: ScanResponse, - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): ScenarioUnavailabilityReason { - val availabilityState = runCatching { getOnrampAvailableState(userWalletId, cryptoCurrency) } + val availabilityState = runCatching { getOnrampAvailableState(userWallet.walletId, cryptoCurrency) } .getOrNull() ?: ExpressAvailabilityState.Error @@ -52,8 +48,9 @@ internal class DefaultRampManager( } override suspend fun availableForSell( - userWallet: UserWallet, + userWalletId: UserWalletId, status: CryptoCurrencyStatus, + sendUnavailabilityReason: ScenarioUnavailabilityReason?, ): Either { return either { val sellSupportedByService = catch( @@ -65,7 +62,8 @@ internal class DefaultRampManager( catch = { raise(ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name)) }, ) - val reason = getSendUnavailabilityReason(userWallet = userWallet, cryptoCurrencyStatus = status) + val reason = sendUnavailabilityReason + ?: getSendUnavailabilityReason(userWalletId = userWalletId, cryptoCurrencyStatus = status) ensure(condition = reason is ScenarioUnavailabilityReason.None) { when (reason) { @@ -111,6 +109,27 @@ internal class DefaultRampManager( return expressServiceLoader.getInitializationStatus(userWalletId) } + override suspend fun getSendUnavailabilityReason( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): ScenarioUnavailabilityReason { + return when { + cryptoCurrencyStatus.value.amount.isNullOrZero() -> { + ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) + } + currenciesRepository.isSendBlockedByPendingTransactions( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) -> { + ScenarioUnavailabilityReason.PendingTransaction( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND, + networkName = cryptoCurrencyStatus.currency.network.name, + ) + } + else -> ScenarioUnavailabilityReason.None + } + } + private suspend fun getExchangeableState( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, @@ -179,33 +198,4 @@ internal class DefaultRampManager( val contractAddress = (this as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE return asset.network == network.backendId && asset.contractAddress.equals(contractAddress, ignoreCase = true) } - - private suspend fun getSendUnavailabilityReason( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): ScenarioUnavailabilityReason { - val coinStatus = getNetworkCoinStatusUseCase.invokeSync( - userWallet = userWallet, - networkId = cryptoCurrencyStatus.currency.network.id, - derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, - ).getOrNull() - - return when { - cryptoCurrencyStatus.value.amount.isNullOrZero() -> { - ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) - } - currenciesRepository.isSendBlockedByPendingTransactions( - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) -> { - ScenarioUnavailabilityReason.PendingTransaction( - withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND, - networkName = coinStatus?.currency?.network?.name.orEmpty(), - ) - } - else -> { - ScenarioUnavailabilityReason.None - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 5cfc7781f2..1c696ac842 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -33,7 +33,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.attestation.CardArtworksProvider -import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -75,7 +74,6 @@ data class DaggerGraphState( val clipboardManager: ClipboardManager? = null, val settingsManager: SettingsManager? = null, val uiMessageSender: UiMessageSender? = null, - val onlineCardVerifier: OnlineCardVerifier? = null, val cardArworksProvider: CardArtworksProvider? = null, val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, val userTokensResponseStore: UserTokensResponseStore? = null, diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 64d10c0193..909be69b49 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.subscribe +import com.arkivanov.essenty.lifecycle.subscribe import com.google.android.material.snackbar.Snackbar import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext @@ -16,7 +17,10 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.features.walletconnect.components.WcRoutingComponent +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.android.create import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child @@ -36,6 +40,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val uiDependencies: UiDependencies, private val wcRoutingComponentFactory: WcRoutingComponent.Factory, private val deeplinkFactory: DeepLinkFactory, + private val tangemHotSDKProxy: TangemHotSDKProxy, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { @@ -70,6 +75,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor( appRouterConfig.stack = stackItems } } + + configureHotSdk() } @Composable @@ -120,6 +127,17 @@ internal class DefaultRoutingComponent @AssistedInject constructor( initialStack } + private fun configureHotSdk() { + lifecycle.subscribe( + onCreate = { + tangemHotSDKProxy.sdkState.value = TangemHotSdk.create(activity) + }, + onDestroy = { + tangemHotSDKProxy.sdkState.value = null + }, + ) + } + @AssistedFactory interface Factory : RoutingComponent.Factory { override fun create(context: AppComponentContext, initialStack: List?): DefaultRoutingComponent diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 825a388842..084e92653f 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -26,7 +26,6 @@ import com.tangem.features.send.v2.api.NFTSendComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.staking.api.StakingComponent import com.tangem.features.swap.SwapComponent -import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent import com.tangem.features.walletconnect.components.WalletConnectEntryComponent @@ -86,7 +85,6 @@ internal class ChildFactory @Inject constructor( private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory, private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory, private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory, - private val testerRouter: TesterRouter, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { @@ -132,9 +130,6 @@ internal class ChildFactory @Inject constructor( componentFactory = welcomeComponentFactory, ) } - is AppRoute.TesterMenu -> { - Child.LegacyIntent(testerRouter.getEntryIntent()) - } is AppRoute.WalletSettings -> { createComponentChild( context = context, @@ -370,7 +365,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.PushNotification -> { createComponentChild( context = context, - params = Unit, + params = PushNotificationsComponent.Params.Route(AppRoute.Home), componentFactory = pushNotificationsComponentFactory, ) } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index 3b75f3bc36..3bd8d9ad3d 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -107,7 +107,7 @@ internal class DefaultDerivationsRepositoryTest { runCatching { repository.derivePublicKeys( userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf), + currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), ) } .onSuccess { Truth.assertThat(it) } @@ -129,7 +129,7 @@ internal class DefaultDerivationsRepositoryTest { runCatching { repository.derivePublicKeys( userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf), + currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), ) } .onSuccess { error("Should throws exception") } @@ -155,7 +155,7 @@ internal class DefaultDerivationsRepositoryTest { runCatching { repository.derivePublicKeys( userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet.scanResponse).ethereum.let(::listOf), + currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), ) } .onSuccess { Truth.assertThat(it) } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt index 063a5bc85b..31c1d12582 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/MissedDerivationsFinderTest.kt @@ -8,6 +8,7 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.common.configs.MultiWalletCardConfig @@ -34,9 +35,10 @@ internal class MissedDerivationsFinderTest { fun `empty derivations for non supported blockchains`() { // Bls is not supported val scanResponse = MockScanResponseFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()) + val userWallet = MockUserWalletFactory.create(scanResponse) val finder = MissedDerivationsFinder(scanResponse) - val currencies = MockCryptoCurrencyFactory(scanResponse).chia.let(::listOf) + val currencies = MockCryptoCurrencyFactory(userWallet).chia.let(::listOf) val actual = finder.find(currencies) Truth.assertThat(actual).isEmpty() @@ -55,9 +57,10 @@ internal class MissedDerivationsFinderTest { ), ) } + val userWallet = MockUserWalletFactory.create(scanResponse) val finder = MissedDerivationsFinder(scanResponse) - val currencies = MockCryptoCurrencyFactory(scanResponse).chiaAndEthereum + val currencies = MockCryptoCurrencyFactory(userWallet).chiaAndEthereum val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( @@ -69,9 +72,10 @@ internal class MissedDerivationsFinderTest { @Test fun `derivations for custom token`() { val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) + val userWallet = MockUserWalletFactory.create(scanResponse) val finder = MissedDerivationsFinder(scanResponse) - val currencies = MockCryptoCurrencyFactory(scanResponse).ethereumTokenWithBinanceDerivation + val currencies = MockCryptoCurrencyFactory(userWallet).ethereumTokenWithBinanceDerivation val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( @@ -86,9 +90,10 @@ internal class MissedDerivationsFinderTest { @Test fun `derivations for cardano`() { val scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()) + val userWallet = MockUserWalletFactory.create(scanResponse) val finder = MissedDerivationsFinder(scanResponse) - val currencies = MockCryptoCurrencyFactory(scanResponse).cardano.let(::listOf) + val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( @@ -111,9 +116,10 @@ internal class MissedDerivationsFinderTest { cardConfig = Wallet2CardConfig, derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) + val userWallet = MockUserWalletFactory.create(scanResponse) val finder = MissedDerivationsFinder(scanResponse) - val currencies = MockCryptoCurrencyFactory(scanResponse).ethereum.let(::listOf) + val currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf) val actual = finder.find(currencies) Truth.assertThat(actual).isEmpty() @@ -125,9 +131,10 @@ internal class MissedDerivationsFinderTest { cardConfig = MultiWalletCardConfig, derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, ) + val userWallet = MockUserWalletFactory.create(scanResponse) val finder = MissedDerivationsFinder(scanResponse) - val currencies = MockCryptoCurrencyFactory(scanResponse).ethereumAndStellar + val currencies = MockCryptoCurrencyFactory(userWallet).ethereumAndStellar val actual = finder.find(currencies) Truth.assertThat(actual).containsExactly( diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index 7a2f8e3e73..17e65d1d6a 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -30,4 +30,10 @@ dependencies { api(deps.kotlin.serialization) implementation(deps.androidx.core.ktx) implementation(deps.timber) + + /* Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 410b01ee6d..9d9c95165f 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -30,10 +30,6 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/welcome"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val INITIAL_INTENT_KEY = "intent" - } } @Serializable @@ -177,9 +173,6 @@ sealed class AppRoute(val path: String) : Route { "/$isInitialReverseOrder", ) - @Serializable - data object TesterMenu : AppRoute(path = "/tester_menu") - @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt deleted file mode 100644 index 9574ce9bf3..0000000000 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/RoutingFeatureToggle.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.common.routing - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -class RoutingFeatureToggle( - private val featureTogglesManager: FeatureTogglesManager, -) { - - val isDeepLinkNavigationEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "DEEPLINK_NAVIGATION_ENABLED") -} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/DeepLinkBuilder.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilder.kt similarity index 68% rename from core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/DeepLinkBuilder.kt rename to common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilder.kt index 1d6216e48c..d1863be6e0 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/DeepLinkBuilder.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilder.kt @@ -1,51 +1,40 @@ -package com.tangem.core.deeplink.converter +package com.tangem.common.routing.deeplink -import com.tangem.core.deeplink.DeeplinkConst.TANGEM_SCHEME +import com.tangem.common.routing.deeplink.DeeplinkConst.TANGEM_SCHEME -/** - * Builder class for constructing deep links with a fluent interface. - */ +/** Builder class for constructing deep links with a fluent interface */ internal class DeepLinkBuilder { + private var scheme: String = TANGEM_SCHEME private var action: String = "" private val pathParams: MutableList = mutableListOf() private val queryParams: MutableMap = mutableMapOf() - /** - * Sets the scheme for the deep link (e.g., "tangem", "https") - */ + /** Sets the scheme for the deep link (e.g., "tangem", "https") */ fun setScheme(scheme: String): DeepLinkBuilder { this.scheme = scheme return this } - /** - * Sets the action for the deep link (e.g., "link", "wallet") - */ + /** Sets the action for the deep link (e.g., "link", "wallet") */ fun setAction(action: String): DeepLinkBuilder { this.action = action return this } - /** - * Adds a path parameter to the deep link - */ + /** Adds a path parameter to the deep link */ fun addPathParam(param: String): DeepLinkBuilder { pathParams.add(param) return this } - /** - * Adds a query parameter to the deep link - */ + /** Adds a query parameter to the deep link */ fun addQueryParam(key: String, value: String): DeepLinkBuilder { queryParams[key] = value return this } - /** - * Builds the deep link URI string - */ + /** Builds the deep link URI string */ fun build(): String { val path = if (pathParams.isEmpty()) { action diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt similarity index 75% rename from core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt rename to common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt index e0ffec3703..a435e90549 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeeplinkConst.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/DeeplinkConst.kt @@ -1,6 +1,9 @@ -package com.tangem.core.deeplink +package com.tangem.common.routing.deeplink object DeeplinkConst { + const val DEEPLINK_KEY = "deeplink" + const val WEBLINK_KEY = "link" + const val TANGEM_SCHEME = "tangem" const val WALLET_ID_KEY = "user_wallet_id" const val NETWORK_ID_KEY = "network_id" diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt similarity index 57% rename from core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt rename to common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt index f6ce9ad32e..ff88b96b22 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverter.kt @@ -1,15 +1,16 @@ -package com.tangem.core.deeplink.converter +package com.tangem.common.routing.deeplink +import android.os.Bundle import com.tangem.common.routing.DeepLinkRoute import com.tangem.common.routing.DeepLinkScheme -import com.tangem.core.deeplink.DEEPLINK_KEY -import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY -import com.tangem.core.deeplink.DeeplinkConst.NAME_KEY -import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY -import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NAME_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.utils.converter.Converter object PayloadToDeeplinkConverter : Converter, String?> { @@ -22,13 +23,25 @@ object PayloadToDeeplinkConverter : Converter, String?> { } } + fun convertBundle(bundle: Bundle?): String? { + if (bundle == null) return null + val bundleDataMap = mutableMapOf() + for (key in bundle.keySet()) { + val value = bundle.getString(key) + if (value != null) { + bundleDataMap[key] = value + } + } + return convert(bundleDataMap) + } + @Suppress("ReturnCount") private fun buildNotificationDeeplink(payload: Map): String? { val type = payload[TYPE_KEY] ?: return null val networkId = payload[NETWORK_ID_KEY] ?: return null val tokenId = payload[TOKEN_ID_KEY] ?: return null val walletId = payload[WALLET_ID_KEY] ?: return null - val derivationPath = payload[DERIVATION_PATH_KEY] ?: return null + val derivationPath = payload[DERIVATION_PATH_KEY].orEmpty() val transactionId = payload[TRANSACTION_ID_KEY] val name = payload[NAME_KEY] @@ -38,7 +51,9 @@ object PayloadToDeeplinkConverter : Converter, String?> { addQueryParam(TOKEN_ID_KEY, tokenId) addQueryParam(TYPE_KEY, type) addQueryParam(WALLET_ID_KEY, walletId) - addQueryParam(DERIVATION_PATH_KEY, derivationPath) + if (derivationPath.isNotBlank()) { + addQueryParam(DERIVATION_PATH_KEY, derivationPath) + } transactionId?.let { addQueryParam(TRANSACTION_ID_KEY, it) } name?.let { addQueryParam(NAME_KEY, it) } @@ -49,7 +64,6 @@ object PayloadToDeeplinkConverter : Converter, String?> { return payload.containsKey(TYPE_KEY) && payload.containsKey(NETWORK_ID_KEY) && payload.containsKey(TOKEN_ID_KEY) && - payload.containsKey(WALLET_ID_KEY) && - payload.containsKey(DERIVATION_PATH_KEY) + payload.containsKey(WALLET_ID_KEY) } } \ No newline at end of file diff --git a/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/DeepLinkBuilderTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt similarity index 97% rename from core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/DeepLinkBuilderTest.kt rename to common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt index 9f74bcee23..596d7cd6c2 100644 --- a/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/DeepLinkBuilderTest.kt +++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/DeepLinkBuilderTest.kt @@ -1,7 +1,6 @@ -package com.tangem.core.deeplink.converter +package com.tangem.common.routing.deeplink import com.google.common.truth.Truth.assertThat -import com.tangem.core.deeplink.DeeplinkConst import org.junit.Before import org.junit.Test diff --git a/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt similarity index 76% rename from core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt rename to common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt index f3887c86e9..03e08c352b 100644 --- a/core/deep-links/src/test/kotlin/com/tangem/core/deeplink/converter/PayloadToDeeplinkConverterTest.kt +++ b/common/routing/src/test/kotlin/com/tangem/common/routing/deeplink/PayloadToDeeplinkConverterTest.kt @@ -1,12 +1,12 @@ -package com.tangem.core.deeplink.converter +package com.tangem.common.routing.deeplink import com.google.common.truth.Truth.assertThat -import com.tangem.core.deeplink.DEEPLINK_KEY -import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY -import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY -import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import org.junit.Test internal class PayloadToDeeplinkConverterTest { @@ -48,6 +48,25 @@ internal class PayloadToDeeplinkConverterTest { ) } + @Test + fun `GIVEN push notification payload without derivationPath WHEN convert THEN should return correct deeplink without derivation_path`() { + // GIVEN + val payload = mapOf( + TYPE_KEY to "token", + NETWORK_ID_KEY to "ethereum", + TOKEN_ID_KEY to "0x123", + WALLET_ID_KEY to "wallet123", + ) + + // WHEN + val result = PayloadToDeeplinkConverter.convert(payload) + + // THEN + assertThat(result).isEqualTo( + "tangem://token?network_id=ethereum&token_id=0x123&type=token&user_wallet_id=wallet123", + ) + } + @Test fun `GIVEN push notification payload with missing type WHEN convert THEN should return null`() { // GIVEN diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index 4580b669b4..5dced08b37 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -6,18 +6,19 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.test.domain.card.MockScanResponseFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.configs.GenericCardConfig import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet /** [REDACTED_AUTHOR] */ -class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = defaultScanResponse) { +class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaultUserWallet) { private val factory = CryptoCurrencyFactory(excludedBlockchains = ExcludedBlockchains()) @@ -49,7 +50,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default val derivationPath = createDerivationPath( blockchain = blockchain, extraDerivationPath = null, - cardDerivationStyleProvider = scanResponse.derivationStyleProvider, + cardDerivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, ) val network = Network( @@ -72,7 +73,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default fun createCustomToken(blockchain: Blockchain, derivationBlockchain: Blockchain): CryptoCurrency { val derivationPath = Network.DerivationPath.Custom( value = derivationBlockchain.derivationPath( - scanResponse.derivationStyleProvider.getDerivationStyle(), + userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), )!!.rawPath, ) @@ -114,7 +115,7 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default ), blockchain = blockchain, extraDerivationPath = null, - scanResponse = scanResponse, + userWallet = userWallet, )!! } @@ -161,9 +162,11 @@ class MockCryptoCurrencyFactory(private val scanResponse: ScanResponse = default private companion object { - val defaultScanResponse = MockScanResponseFactory.create( - cardConfig = GenericCardConfig(2), - derivedKeys = emptyMap(), + val defaultUserWallet = MockUserWalletFactory.create( + scanResponse = MockScanResponseFactory.create( + cardConfig = GenericCardConfig(2), + derivedKeys = emptyMap(), + ), ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index fe714763b9..a1ba4366f4 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -165,12 +165,12 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi .padding(end = 16.dp) .clip(RoundedCornerShape(16.dp)) .background(TangemTheme.colors.background.secondary) - .padding(horizontal = 12.dp, vertical = 4.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onMaxAmountClick, - ), + ) + .padding(horizontal = 12.dp, vertical = 4.dp), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt index 534a8de836..043582d194 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/UserWalletItemUMConverter.kt @@ -15,7 +15,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isLocked -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.utils.converter.Converter /** @@ -57,13 +56,19 @@ class UserWalletItemUMConverter( } private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded { - userWallet.requireColdWallet() - val cardCount = userWallet.getCardsCount() ?: 1 - val text = TextReference.PluralRes( - id = R.plurals.card_label_card_count, - count = cardCount, - formatArgs = wrappedList(cardCount), - ) + val text = when (userWallet) { + is UserWallet.Cold -> { + val cardCount = userWallet.getCardsCount() ?: 1 + TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + } + is UserWallet.Hot -> { + TextReference.Res(R.string.hw_mobile_wallet) + } + } return UserWalletItemUM.Information.Loaded(text) } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 1dd4445077..6667534f25 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -83,6 +83,7 @@ sealed class AnalyticsParam { data object Onboarding : ScreensSources("Onboarding") data object LongTap : ScreensSources("Long Tap") data object Markets : ScreensSources("Markets") + data object HotWallet : ScreensSources("Hot Wallet") } sealed class TxSentFrom(val value: String) { 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 d63c340831..6b99bc3c7e 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 @@ -11,18 +11,6 @@ "name": "STAKING_TON_ENABLED", "version": "undefined" }, - { - "name": "NFT_ENABLED", - "version": "5.25.0" - }, - { - "name": "NFT_EVM_ENABLED", - "version": "5.25.0" - }, - { - "name": "NFT_SOLANA_ENABLED", - "version": "5.25.0" - }, { "name": "NFT_MEDIA_CONTENT_ENABLED", "version": "undefined" @@ -31,26 +19,10 @@ "name": "STAKING_CARDANO_ENABLED", "version": "undefined" }, - { - "name": "NEW_ARTWORK_LOADING", - "version": "5.25.0" - }, - { - "name": "NEW_ATTESTATION_ENABLED", - "version": "5.24.0" - }, { "name": "WALLET_CONNECT_REDESIGN_ENABLED", "version": "undefined" }, - { - "name": "STAKING_LOADING_REFACTORING_ENABLED", - "version": "5.25.0" - }, - { - "name": "DEEPLINK_NAVIGATION_ENABLED", - "version": "5.25.0" - }, { "name": "PUSH_NOTIFICATIONS_ENABLED", "version": "undefined" @@ -74,5 +46,9 @@ { "name": "WALLET_BALANCE_FETCHER_ENABLED", "version": "5.27.0" + }, + { + "name": "HOT_WALLET_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt index 19fc906239..c6e72a8a75 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiEnvironment.kt @@ -13,6 +13,9 @@ enum class ApiEnvironment { @Json(name = "DEV") DEV, + @Json(name = "DEV_2") + DEV_2, + @Json(name = "STAGE") STAGE, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index af3ce61013..23b46cf40b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -27,6 +27,7 @@ internal class Express( override val environmentConfigs: List = listOf( createDevEnvironment(), + createDev2Environment(), createStageEnvironment(), createMockedEnvironment(), createProdEnvironment(), @@ -53,6 +54,12 @@ internal class Express( headers = createHeaders(isProd = false), ) + private fun createDev2Environment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV_2, + baseUrl = "[REDACTED_ENV_URL]", + headers = createHeaders(isProd = false), + ) + private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, baseUrl = "[REDACTED_ENV_URL]", diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index be68593d10..8d2f1f19e2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -55,7 +55,7 @@ internal class TangemTech( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.tangem.org/v1/", + baseUrl = "https://api.tangem.org/", headers = createHeaders(), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt index 60d34dab96..f0de0bfdda 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/DevApiConfigsManager.kt @@ -23,7 +23,7 @@ internal class DevApiConfigsManager( private val apiConfigs: ApiConfigs, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, -) : MutableApiConfigsManager { +) : MutableApiConfigsManager() { override val configs: StateFlow> field = MutableStateFlow(value = getInitialConfigs()) @@ -34,22 +34,22 @@ internal class DevApiConfigsManager( override fun initialize() { isInitialized.value = false - // We can't use appPreferencesStore.getObjectMap as base flow, - // because we should keep possibility to work with configs synchronous. - // See [getBaseUrl] appPreferencesStore.getObjectMap(PreferencesKeys.apiConfigsEnvironmentKey) + .distinctUntilChanged() .onEach { savedEnvironments -> - configs.update { apiConfigs -> - apiConfigs.mapValues { - val (config, currentEnvironment) = it + val apiConfigs = configs.value - savedEnvironments[config.id.name] ?: currentEnvironment - } + configs.value = apiConfigs.mapValues { + val (config, currentEnvironment) = it + + savedEnvironments[config.id.name] ?: currentEnvironment } if (!isInitialized.value) { isInitialized.value = true } + + notifyListeners(apiConfigs = apiConfigs, savedEnvironments = savedEnvironments) } .launchIn(CoroutineScope(SupervisorJob() + dispatchers.default)) } @@ -96,6 +96,35 @@ internal class DevApiConfigsManager( } } + private fun notifyListeners( + apiConfigs: Map, + savedEnvironments: Map, + ) { + if (registerListeners.isNotEmpty()) { + val changedConfigs = apiConfigs.mapNotNull { (config, prevEnvironment) -> + val newEnvironment = savedEnvironments[config.id.name] ?: config.defaultEnvironment + + if (prevEnvironment == newEnvironment) return@mapNotNull null + + val environmentConfig = config.environmentConfigs + .firstOrNull { it.environment == newEnvironment } + ?: return@mapNotNull null + + config.id to environmentConfig + } + + if (changedConfigs.isNotEmpty()) { + registerListeners.forEach { listener -> + val changedConfig = changedConfigs.firstOrNull { it.first == listener.id }?.second + + if (changedConfig != null) { + listener.onChange(changedConfig) + } + } + } + } + } + private fun getInitialConfigs(): Map { return apiConfigs.associateWith(ApiConfig::defaultEnvironment) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MockApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MockApiConfigsManager.kt index cf388f26ec..5df19d76e6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MockApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MockApiConfigsManager.kt @@ -4,9 +4,10 @@ import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.ApiEnvironmentConfig -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.* /** * Implementation of [ApiConfigsManager] in MOCK environment @@ -17,13 +18,16 @@ import kotlinx.coroutines.flow.update */ internal class MockApiConfigsManager( private val apiConfigs: ApiConfigs, -) : MutableApiConfigsManager { + dispatchers: CoroutineDispatcherProvider, +) : MutableApiConfigsManager() { override val configs: StateFlow> field = MutableStateFlow(value = getInitialConfigs()) override val isInitialized: StateFlow = MutableStateFlow(value = true) + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default) + override fun initialize() = Unit override fun getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig { @@ -55,6 +59,20 @@ internal class MockApiConfigsManager( } } + override fun addListener(listener: ApiConfigEnvChangeListener) { + super.addListener(listener) + + configs + .map { it.entries.firstOrNull { it.key.id == listener.id } } + .filterNotNull() + .onEach { (apiConfig, currentEnvironment) -> + listener.onChange( + environmentConfig = apiConfig.environmentConfigs.first { it.environment == currentEnvironment }, + ) + } + .launchIn(coroutineScope) + } + private fun getInitialConfigs(): Map { return apiConfigs.associateWith { it.defaultEnvironment } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt index 8b17b0af71..cdd9a6b0a2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/managers/MutableApiConfigsManager.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config.managers import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.ApiEnvironmentConfig import kotlinx.coroutines.flow.Flow /** @@ -9,14 +10,41 @@ import kotlinx.coroutines.flow.Flow * [REDACTED_AUTHOR] */ -interface MutableApiConfigsManager : ApiConfigsManager { +abstract class MutableApiConfigsManager : ApiConfigsManager { /** Api configs with current [ApiEnvironment] */ - val configs: Flow> + abstract val configs: Flow> + + /** + * A set of listeners registered to observe changes in API environment configurations. + * These listeners are notified whenever an environment change occurs. + */ + protected val registerListeners: Set + field = mutableSetOf() /** Change api environment [environment] by [id] */ - suspend fun changeEnvironment(id: String, environment: ApiEnvironment) + abstract suspend fun changeEnvironment(id: String, environment: ApiEnvironment) /** Change api environment [environment] for all configs */ - suspend fun changeEnvironment(environment: ApiEnvironment) + abstract suspend fun changeEnvironment(environment: ApiEnvironment) + + /** Adds a [listener] to observe changes in API environment configurations */ + open fun addListener(listener: ApiConfigEnvChangeListener) { + registerListeners += listener + } + + /** + * Listener for observing changes in API environment configurations + * + * @property id the identifier of the API configuration this listener is associated with + */ + abstract class ApiConfigEnvChangeListener(val id: ApiConfig.ID) { + + /** + * Called when the environment configuration changes + * + * @param environmentConfig the updated [ApiEnvironmentConfig] for the associated API configuration + */ + abstract fun onChange(environmentConfig: ApiEnvironmentConfig) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index 117123d566..eaeb531130 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -9,7 +9,7 @@ import retrofit2.http.Query interface TangemTechMarketsApi { @Suppress("LongParameterList") - @GET("coins/list") + @GET("v1/coins/list") suspend fun getCoinsList( @Query("currency") currency: String, @Query("interval") interval: String, @@ -20,24 +20,24 @@ interface TangemTechMarketsApi { @Query("timestamp") timestamp: Long?, ): ApiResponse - @GET("coins/{coin_id}") + @GET("v1/coins/{coin_id}") suspend fun getCoinMarketData( @Path("coin_id") coinId: String, @Query("currency") currency: String, @Query("language") language: String, ): ApiResponse - @GET("coins/{coin_id}/history") + @GET("v1/coins/{coin_id}/history") suspend fun getCoinChart( @Path("coin_id") coinId: String, @Query("currency") currency: String, @Query("interval") interval: String, ): ApiResponse - @GET("coins/{coin_id}/exchanges") + @GET("v1/coins/{coin_id}/exchanges") suspend fun getCoinExchanges(@Path("coin_id") coinId: String): ApiResponse - @GET("coins/history_preview") + @GET("v1/coins/history_preview") suspend fun getCoinsListCharts( @Query("coin_ids") coinIds: String, @Query("currency") currency: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt index 46ca99bbb6..ec0d6eb2c7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt @@ -33,10 +33,10 @@ data class OnrampQuoteResponse( val providerId: String, @Json(name = "minFromAmount") - val minFromAmount: String, + val minFromAmount: String?, @Json(name = "maxFromAmount") - val maxFromAmount: String, + val maxFromAmount: String?, @Json(name = "minToAmount") val minToAmount: String?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index a6793286f0..14ec9f906f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit @Suppress("TooManyFunctions") interface TangemTechApi { - @GET("coins") + @GET("v1/coins") suspend fun getCoins( @Header("Cache-Control") cacheControl: String = "max-age=600", @Query("contractAddress") contractAddress: String? = null, @@ -30,150 +30,150 @@ interface TangemTechApi { @Query("limit") limit: Int? = null, ): ApiResponse - @GET("rates") + @GET("v1/rates") suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse - @GET("currencies") + @GET("v1/currencies") suspend fun getCurrencyList( @Header("Cache-Control") cacheControl: String = "max-age=600", ): ApiResponse - @GET("geo") + @GET("v1/geo") suspend fun getUserCountryCode(): GeoResponse - @GET("user-tokens/{user-id}") + @GET("v1/user-tokens/{user-id}") suspend fun getUserTokens(@Path(value = "user-id") userId: String): ApiResponse - @PUT("user-tokens/{user-id}") + @PUT("v1/user-tokens/{user-id}") suspend fun saveUserTokens( @Path(value = "user-id") userId: String, @Body userTokens: UserTokensResponse, ): ApiResponse - @POST("user-tokens") + @POST("v1/user-tokens") suspend fun markUserWallerWasCreated(@Body body: MarkUserWalletWasCreatedBody): ApiResponse /** Returns referral status by [walletId] */ - @GET("referral/{walletId}") + @GET("v1/referral/{walletId}") suspend fun getReferralStatus(@Path("walletId") walletId: String): ApiResponse /** Make user referral, requires [StartReferralBody] */ - @POST("referral") + @POST("v1/referral") suspend fun startReferral(@Body startReferralBody: StartReferralBody): ApiResponse - @GET("quotes") + @GET("v1/quotes") suspend fun getQuotes( @Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String, @Query("fields") fields: String, ): ApiResponse - @GET("promotion") + @GET("v1/promotion") suspend fun getPromotionInfo( @Query("programName") name: String, @Header("Cache-Control") cacheControl: String = "max-age=600", ): ApiResponse - @GET("settings/{wallet_id}") + @GET("v1/settings/{wallet_id}") suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse - @PUT("settings/{wallet_id}") + @PUT("v1/settings/{wallet_id}") suspend fun saveUserTokensSettings( @Path("wallet_id") walletId: String, @Body userTokensSettings: UserTokensSettingsResponse, ): ApiResponse - @POST("user-network-account") + @POST("v1/user-network-account") suspend fun createUserNetworkAccount( @Body body: CreateUserNetworkAccountBody, ): ApiResponse - @POST("account") + @POST("v1/account") suspend fun createUserTokensAccount( @Body body: CreateUserTokensAccountBody, ): ApiResponse - @PUT("account/{account_id}") + @PUT("v1/account/{account_id}") suspend fun updateUserTokensAccount( @Path("account_id") accountId: Int, @Body body: UpdateUserTokensAccountBody, ): ApiResponse - @PUT("account/{account_id}/archive") + @PUT("v1/account/{account_id}/archive") suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - @PUT("account/{account_id}/unarchive") + @PUT("v1/account/{account_id}/unarchive") suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse - @GET("features") + @GET("v1/features") suspend fun getFeatures(): ApiResponse @ReadTimeout(duration = 5, unit = TimeUnit.SECONDS) - @GET("networks/providers") + @GET("v1/networks/providers") suspend fun getBlockchainProviders(): Map> - @GET("seedphrase-notification/{wallet_id}") + @GET("v1/seedphrase-notification/{wallet_id}") suspend fun getSeedPhraseNotificationStatus( @Path("wallet_id") walletId: String, ): ApiResponse - @PUT("seedphrase-notification/{wallet_id}") + @PUT("v1/seedphrase-notification/{wallet_id}") suspend fun updateSeedPhraseNotificationStatus( @Path("wallet_id") walletId: String, @Body body: SeedPhraseNotificationDTO, ): ApiResponse - @GET("seedphrase-notification/{wallet_id}/confirmed") + @GET("v1/seedphrase-notification/{wallet_id}/confirmed") suspend fun getSeedPhraseSecondNotificationStatus( @Path("wallet_id") walletId: String, ): ApiResponse - @PUT("seedphrase-notification/{wallet_id}/confirmed") + @PUT("v1/seedphrase-notification/{wallet_id}/confirmed") suspend fun updateSeedPhraseSecondNotificationStatus( @Path("wallet_id") walletId: String, @Body body: SeedPhraseNotificationDTO, ): ApiResponse - @GET("hot_crypto") + @GET("v1/hot_crypto") suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse - @GET("stories/{story_id}") + @GET("v1/stories/{story_id}") suspend fun getStoryById(@Path("story_id") storyId: String): ApiResponse // region push notifications - @GET("notification/push_notifications_eligible_networks") + @GET("v1/notification/push_notifications_eligible_networks") suspend fun getEligibleNetworksForPushNotifications(): ApiResponse> - @POST("user-wallets/applications/") + @POST("v1/user-wallets/applications/") suspend fun createApplicationId( @Body body: NotificationApplicationCreateBody, ): ApiResponse - @PATCH("user-wallets/applications/{application_id}") + @PATCH("v1/user-wallets/applications/{application_id}") suspend fun updatePushTokenForApplicationId( @Path("application_id") applicationId: String, @Body body: NotificationApplicationCreateBody, ): ApiResponse - @PATCH("user-wallets/wallets/{wallet_id}/notify") + @PATCH("v1/user-wallets/wallets/{wallet_id}/notify") suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse // endregion // region wallets - @PATCH("user-wallets/wallets/{wallet_id}") + @PATCH("v1/user-wallets/wallets/{wallet_id}") suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse - @POST("user-wallets/wallets/create-and-connect-by-appuid/{application_id}") + @POST("v1/user-wallets/wallets/create-and-connect-by-appuid/{application_id}") suspend fun associateApplicationIdWithWallets( @Path("application_id") applicationId: String, @Body body: List, ): ApiResponse - @GET("user-wallets/wallets/{wallet_id}") + @GET("v1/user-wallets/wallets/{wallet_id}") suspend fun getWalletById(@Path("wallet_id") walletId: String): ApiResponse - @GET("user-wallets/wallets/by-app/{app_id}") + @GET("v1/user-wallets/wallets/by-app/{app_id}") suspend fun getWallets(@Path("app_id") appId: String): ApiResponse> // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 3001458ab9..2363c048e7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -58,7 +58,7 @@ internal object NetworkModule { dispatchers: CoroutineDispatcherProvider, ): ApiConfigsManager { return when { - BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs) + BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE -> MockApiConfigsManager(apiConfigs, dispatchers) BuildConfig.TESTER_MENU_ENABLED -> DevApiConfigsManager(apiConfigs, appPreferencesStore, dispatchers) else -> ProdApiConfigsManager(apiConfigs) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index a631c02284..c946177dc7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -8,8 +8,10 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.token.* +import com.tangem.datasource.local.token.DefaultStakingActionsStore +import com.tangem.datasource.local.token.DefaultStakingYieldsStore +import com.tangem.datasource.local.token.StakingActionsStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.mapWithStringKeyTypes @@ -66,17 +68,6 @@ internal object StakingStoreModule { ) } - @Provides - @Singleton - fun provideStakingBalanceStore( - persistenceStore: DataStore>>, - ): StakingBalanceStore { - return DefaultStakingBalanceStore( - persistenceStore = persistenceStore, - runtimeStore = RuntimeSharedStore(), - ) - } - @Provides @Singleton fun provideStakingActionsStore(): StakingActionsStore { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt deleted file mode 100644 index c1814b4ee1..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ /dev/null @@ -1,269 +0,0 @@ -package com.tangem.datasource.local.token - -import androidx.datastore.core.DataStore -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.token.StakingBalanceStore.StakingID -import com.tangem.datasource.local.token.converter.YieldBalanceConverter -import com.tangem.domain.models.StatusSource -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch - -internal typealias YieldBalanceWrappersDTO = Map> -internal typealias YieldBalanceListByWalletId = Map> - -/** - * Default implementation of [StakingBalanceStore] - * - * @property persistenceStore persistence store - * @property runtimeStore runtime store - */ -internal class DefaultStakingBalanceStore( - private val persistenceStore: DataStore, - private val runtimeStore: RuntimeSharedStore, -) : StakingBalanceStore { - - override fun get(userWalletId: UserWalletId, stakingIds: List): Flow> = channelFlow { - val cachedBalances = persistenceStore.data - .map { - val wrappers = it[userWalletId.stringValue].orEmpty() - .filter { wrapper -> - stakingIds.any { id -> - id.address == wrapper.addresses.address && id.integrationId == wrapper.integrationId - } - } - - YieldBalanceConverter(isCached = true).convertSet(input = wrappers) - } - .firstOrNull() - .orEmpty() - - if (cachedBalances.isNotEmpty()) { - send(cachedBalances) - } - - runtimeStore.get() - .map { - it[userWalletId].orEmpty().filter { balance -> - stakingIds.any { id -> - id.address == balance.address && id.integrationId == balance.integrationId - } - } - .toSet() - } - .onEach { - val mergedBalances = mergeYieldBalances( - stakingIds = stakingIds, - cachedBalances = cachedBalances, - runtimeBalances = it, - ) - - send(mergedBalances) - } - .launchIn(scope = this) - } - - override fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow { - return get(userWalletId = userWalletId, stakingIds = listOf(stakingID)).map { balances -> - balances.getBalance(stakingID = stakingID) - } - } - - override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { - val runtimeBalances = runtimeStore.getSyncOrNull()?.getValue(userWalletId).orEmpty() - val cachedBalances = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue).orEmpty() - - if (runtimeBalances.isEmpty() && cachedBalances.isEmpty()) return null - - return cachedBalances.mapTo(hashSetOf()) { - val cached = YieldBalanceConverter(source = StatusSource.ONLY_CACHE).convert(value = it) - val runtime = runtimeBalances.getBalance(address = cached.address, integrationId = cached.integrationId) - - if (runtime == null || runtime is YieldBalance.Error) cached else runtime - } - } - - override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List): Set? { - val runtime = runtimeStore.getSyncOrNull()?.getValue(userWalletId) - val cached = persistenceStore.data.firstOrNull()?.get(userWalletId.stringValue) - - if (runtime.isNullOrEmpty() && cached.isNullOrEmpty()) return null - - return mergeYieldBalances( - cachedBalances = YieldBalanceConverter(source = StatusSource.ONLY_CACHE) - .convertSet(input = cached.orEmpty()), - runtimeBalances = runtime.orEmpty(), - stakingIds = stakingIds, - ) - } - - override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance? { - val balances = getSyncOrNull(userWalletId = userWalletId, stakingIds = listOf(stakingID)) ?: return null - - return balances.getBalance(stakingID = stakingID) - } - - override suspend fun store(userWalletId: UserWalletId, items: Set) { - coroutineScope { - launch { - val newBalances = YieldBalanceConverter(isCached = false).convertSet(input = items) - - runtimeStore.update(default = emptyMap()) { saved -> - saved.toMutableMap().apply { - this[userWalletId] = saved[userWalletId] - ?.addOrReplace(newBalances) { old, new -> - old.integrationId == new.integrationId && old.address == new.address - } - ?: newBalances - } - } - } - launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) } - } - } - - override suspend fun refresh(userWalletId: UserWalletId, stakingIds: List) { - updateRuntimeStore(userWalletId = userWalletId) { saved -> - saved.mapTo(hashSetOf()) { - val yieldBalance = it.takeIf { balance -> - stakingIds.any { id -> balance.integrationId == id.integrationId && balance.address == id.address } - } - - yieldBalance?.copySealed(source = StatusSource.CACHE) ?: it - } - } - } - - override suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO) { - coroutineScope { - launch { - storeInRuntimeStore( - userWalletId = userWalletId, - integrationId = stakingID.integrationId, - address = stakingID.address, - item = item, - ) - - storeInPersistenceStore( - userWalletId = userWalletId, - integrationId = stakingID.integrationId, - address = stakingID.address, - item = item, - ) - } - } - } - - override suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance) { - runtimeStore.update(default = emptyMap()) { saved -> - saved.toMutableMap().apply { - this[userWalletId] = saved[userWalletId] - ?.addOrReplace(item) { it.integrationId == item.integrationId && it.address == item.address } - ?: setOf(item) - } - } - } - - private suspend fun storeInRuntimeStore( - userWalletId: UserWalletId, - integrationId: String, - address: String, - item: YieldBalanceWrapperDTO, - ) { - val newBalance = YieldBalanceConverter(isCached = false).convert(value = item) - - runtimeStore.update(default = emptyMap()) { saved -> - saved.toMutableMap().apply { - this[userWalletId] = saved[userWalletId] - ?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address } - ?: setOf(newBalance) - } - } - } - - private suspend fun updateRuntimeStore( - userWalletId: UserWalletId, - function: (Set) -> Set, - ) { - runtimeStore.update(default = emptyMap()) { saved -> - saved.toMutableMap().apply { - this[userWalletId] = function(this[userWalletId].orEmpty()) - } - } - } - - private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, items: Set) { - persistenceStore.updateData { current -> - current.toMutableMap().apply { - this[userWalletId.stringValue] = current[userWalletId.stringValue] - ?.addOrReplace(items = items) { old, new -> - old.integrationId == new.integrationId && old.addresses.address == new.addresses.address - } - ?: items - } - } - } - - private suspend fun storeInPersistenceStore( - userWalletId: UserWalletId, - integrationId: String, - address: String, - item: YieldBalanceWrapperDTO, - ) { - persistenceStore.updateData { current -> - current.toMutableMap().apply { - this[userWalletId.stringValue] = current[userWalletId.stringValue] - ?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address } - ?: setOf(item) - } - } - } - - private fun mergeYieldBalances( - cachedBalances: Set, - runtimeBalances: Set, - stakingIds: List, - ): Set { - return stakingIds.mapTo(hashSetOf()) { id -> - val runtime = runtimeBalances.getBalance(stakingID = id) - - if (runtime == null || runtime is YieldBalance.Error) { - getCachedBalanceIfPossible(cachedBalances = cachedBalances, stakingID = id) - } else { - runtime - } - } - } - - private fun getCachedBalanceIfPossible(cachedBalances: Set, stakingID: StakingID): YieldBalance { - val cached = cachedBalances.getBalance(stakingID) - ?: return YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId) - - val updatedCached = when (cached) { - is YieldBalance.Data -> cached.copy(source = StatusSource.ONLY_CACHE) - is YieldBalance.Empty -> cached.copy(source = StatusSource.ONLY_CACHE) - is YieldBalance.Error, - is YieldBalance.Unsupported, - -> null - } - - return updatedCached ?: YieldBalance.Error(integrationId = stakingID.address, address = stakingID.integrationId) - } - - private fun Set.getBalance(stakingID: StakingID): YieldBalance? { - return getBalance(address = stakingID.address, integrationId = stakingID.integrationId) - } - - private fun Set.getBalance(address: String?, integrationId: String?): YieldBalance? { - return firstOrNull { yieldBalance -> - val isCorrectAddress = address != null && address == yieldBalance.address - val isCorrectIntegration = integrationId != null && yieldBalance.integrationId == integrationId - - isCorrectIntegration && isCorrectAddress - } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt deleted file mode 100644 index 779536d430..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.datasource.local.token - -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow - -/** Staking balance store */ -interface StakingBalanceStore { - - /** Get flow of [YieldBalanceList] by [userWalletId] and [stakingIds] */ - fun get(userWalletId: UserWalletId, stakingIds: List): Flow> - - /** Get flow of [YieldBalance] by [userWalletId] and [stakingID] */ - fun get(userWalletId: UserWalletId, stakingID: StakingID): Flow - - /** Get all [YieldBalance] synchronously or null by [userWalletId] */ - suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? - - /** Get [YieldBalanceList] synchronously or null by [userWalletId] and [stakingIds] */ - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingIds: List): Set? - - /** Get [YieldBalance] synchronously or null by [userWalletId] and [stakingID] */ - suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingID: StakingID): YieldBalance? - - /** Store [items] by [userWalletId] */ - suspend fun store(userWalletId: UserWalletId, items: Set) - - /** Store [item] by [userWalletId] and [stakingID] */ - suspend fun store(userWalletId: UserWalletId, stakingID: StakingID, item: YieldBalanceWrapperDTO) - - /** Store [item] by [userWalletId] */ - suspend fun storeSingleYieldBalance(userWalletId: UserWalletId, item: YieldBalance) - - /** Refresh balances of [stakingIds] by [userWalletId] */ - suspend fun refresh(userWalletId: UserWalletId, stakingIds: List) - - data class StakingID(val integrationId: String, val address: String) -} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt new file mode 100644 index 0000000000..651e704cdc --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -0,0 +1,53 @@ +package com.tangem.datasource.api.common.config + +import com.google.common.truth.Truth +import io.mockk.mockk +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ApiConfigTest { + + @Test + fun `all baseUrls ends with slash`() { + // Arrange + val allBaseUrls = createApiConfigs().flatMap { it.environmentConfigs.map { it.baseUrl } } + + // Actual + val actual = allBaseUrls.all { it.endsWith("/") } + + Timber.e(allBaseUrls.joinToString(separator = "\n")) + + // Assert + Truth.assertThat(actual).isTrue() + } + + private fun createApiConfigs(): ApiConfigs { + return ApiConfig.ID.entries.mapTo(destination = hashSetOf()) { + when (it) { + ApiConfig.ID.Express -> { + Express( + environmentConfigStorage = mockk(), + expressAuthProvider = mockk(), + appVersionProvider = mockk(), + appInfoProvider = mockk(), + ) + } + ApiConfig.ID.TangemTech -> { + TangemTech( + appVersionProvider = mockk(), + authProvider = mockk(), + appInfoProvider = mockk(), + ) + } + ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk()) + ApiConfig.ID.TangemPay -> TangemPay(appVersionProvider = mockk()) + ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk()) + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 3d86042af6..defd5db207 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -162,7 +162,7 @@ internal class ProdApiConfigsManagerTest { id = ApiConfig.ID.TangemTech, expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.tangem.org/v1/", + baseUrl = "https://api.tangem.org/", headers = mapOf( "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityInstanceHolder.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityInstanceHolder.kt new file mode 100644 index 0000000000..8833f92f26 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/utils/ActivityInstanceHolder.kt @@ -0,0 +1,43 @@ +package com.tangem.core.decompose.utils + +import androidx.appcompat.app.AppCompatActivity +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.arkivanov.essenty.lifecycle.subscribe +import com.tangem.core.decompose.context.AppComponentContext +import java.lang.ref.WeakReference + +class ActivityInstanceHolder { + private var instance: T? = null + + lateinit var instanceAccess: WeakReference + private set + + fun set(instance: T) { + this.instance = instance + instanceAccess = WeakReference(instance) + } + + fun clear() { + instance = null + instanceAccess.clear() + } +} + +inline fun AppComponentContext.getOrCreateActivityInstanceHolder( + noinline factory: (AppCompatActivity) -> T, +): ActivityInstanceHolder { + val holder = instanceKeeper.getOrCreateSimple { + ActivityInstanceHolder() + } + + lifecycle.subscribe( + onCreate = { + holder.set(factory(activity)) + }, + onDestroy = { + holder.clear() + }, + ) + + return holder +} \ No newline at end of file diff --git a/core/deep-links/.gitignore b/core/deep-links/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/core/deep-links/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/core/deep-links/build.gradle.kts b/core/deep-links/build.gradle.kts deleted file mode 100644 index dd23856660..0000000000 --- a/core/deep-links/build.gradle.kts +++ /dev/null @@ -1,34 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - id("configuration") -} - -android { - namespace = "com.tangem.core.deeplink" -} - -dependencies { - /* Common */ - implementation(projects.common.routing) - - /* Core */ - implementation(projects.core.decompose) - - /* Libs - AndroidX */ - implementation(deps.lifecycle.runtime.ktx) - - /* Libs - Other */ - implementation(deps.timber) - - /* DI */ - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) - - /* Tests */ - testImplementation(deps.test.junit) - testImplementation(deps.test.coroutine) - testImplementation(deps.test.truth) - testImplementation(deps.test.mockk) -} \ No newline at end of file diff --git a/core/deep-links/global/.gitignore b/core/deep-links/global/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/core/deep-links/global/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/core/deep-links/global/build.gradle.kts b/core/deep-links/global/build.gradle.kts deleted file mode 100644 index ac47c5ac3d..0000000000 --- a/core/deep-links/global/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - id("configuration") -} - -android { - namespace = "com.tangem.core.deeplink.global" -} - -dependencies { - - /* Project */ - implementation(projects.core.deepLinks) -} \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt deleted file mode 100644 index 5dcf6211d5..0000000000 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/ReferralDeepLink.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.core.deeplink.global - -import com.tangem.core.deeplink.DeepLink - -@Deprecated("Use ReferralDeepLinkHandler") -class ReferralDeepLink( - val onReceive: () -> Unit, -) : DeepLink(shouldHandleDelayed = true) { - override val uri: String = "tangem://referral" - - override fun onReceive(params: Map) { - onReceive() - } -} \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt deleted file mode 100644 index c3ac0e7faa..0000000000 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.core.deeplink.global - -import com.tangem.core.deeplink.DeepLink - -@Deprecated("Use SellDeepLinkHandler") -class SellCurrencyDeepLink( - val onReceive: (data: Data) -> Unit, - shouldHandleDelayed: Boolean, -) : DeepLink(shouldHandleDelayed) { - - override val uri: String = "tangem://redirect_sell" - - override fun onReceive(params: Map) { - val data = Data( - transactionId = params["transactionId"] ?: return, - baseCurrencyAmount = params["baseCurrencyAmount"] ?: return, - depositWalletAddress = params["depositWalletAddress"] ?: return, - currencyId = params["currency_id"] ?: return, - depositWalletAddressTag = params["depositWalletAddressTag"], - ) - - onReceive(data) - } - - data class Data( - val transactionId: String, - val baseCurrencyAmount: String, - val depositWalletAddress: String, - val currencyId: String, - val depositWalletAddressTag: String?, - ) -} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt deleted file mode 100644 index a3ea2b5083..0000000000 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.core.deeplink - -/** - * Represents a deep link. - */ -abstract class DeepLink(val shouldHandleDelayed: Boolean = false) { - - /** - * ID of the deep link. - * - * By default, it is the same as the [uri]. - * */ - val id: String get() = uri - - /** - * URI of the deep link. - * - * **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.** - * - * Query parameters will be received automatically. - * - * Path parameters can be added using the following syntax: - * ```kotlin - * "tangem://link" // Without parameters - * "tangem://link/{param1}/{param2}" // With path parameters - * ``` - * */ - abstract val uri: String - - /** - * Method to be called when this deep link is received. - * - * @param params Map of parameters received from the deep link. - * */ - abstract fun onReceive(params: Map) -} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt deleted file mode 100644 index 3fa3e28ef8..0000000000 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.core.deeplink - -import android.content.Intent - -/** - * Key to pass deeplink via intent - */ -const val DEEPLINK_KEY = "deeplink" -const val WEBLINK_KEY = "link" - -// TODO: Add tests -/** - * Provides functionality to handle deep links. - * - * Allows deep links to be launched, registered, or unregistered. - */ -interface DeepLinksRegistry { - - /** - * Finds matches registered deep links for the given [intent] and launches them. - * - * @return `true` if any deep link was received, `false` otherwise. - */ - fun launch(intent: Intent): Boolean - - /** - * Registers the given [deepLink]. - */ - fun register(deepLink: DeepLink) - - /** - * Registers the given [deepLinks]. - */ - fun register(deepLinks: Collection) - - /** - * Unregisters the given [deepLinks]. - */ - fun unregister(deepLinks: Collection) - - /** - * Unregisters the given [deepLink]. - */ - fun unregister(deepLink: DeepLink) - - /** - * Unregisters deep links with the given [ids]. - * */ - fun unregisterByIds(ids: Collection) - - /** - * Triggers run last launched [Intent] with deeplink handlers that can handle delayed deeplink - * of specific [deepLinkClass] after handle [Intent] clear that and second time no intent will be handled - */ - fun triggerDelayedDeeplink(deepLinkClass: Class) - - fun cancelDelayedDeeplink() -} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt deleted file mode 100644 index 4398db12a2..0000000000 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.core.deeplink.di - -import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry -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 DeepLinksModule { - - @Provides - @Singleton - fun provideDeepLinksRegistry(): DeepLinksRegistry { - return DefaultDeepLinksRegistry() - } -} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt deleted file mode 100644 index ff0c60033b..0000000000 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.tangem.core.deeplink.impl - -import android.content.Intent -import android.net.Uri -import androidx.core.net.toUri -import com.tangem.core.deeplink.DEEPLINK_KEY -import com.tangem.core.deeplink.DeepLink -import com.tangem.core.deeplink.DeepLinksRegistry -import timber.log.Timber - -internal class DefaultDeepLinksRegistry : DeepLinksRegistry { - - private var registries: List = emptyList() - private var lastDeepLink: Uri? = null - - override fun launch(intent: Intent): Boolean { - // Try to get deeplink from data (direct deeplink flow) - // Otherwise, try to get from extras (notification deeplink flow) - val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() - val received = intent.data ?: deepLinkExtras ?: return false - lastDeepLink = received - var hasMatch = false - - Timber.i( - """ - Received deep link intent - |- Received URI: $received - |- Registries: $registries - """.trimIndent(), - ) - registries.forEach { deepLink -> - val expected = deepLink.uri.toUri() - - if (!isMatches(expected, received)) return@forEach - hasMatch = true - - val params = getParams(expected, received) - - logMatch(hasMatch, expected, received, params) - - deepLink.onReceive(params) - lastDeepLink = null // clear deeplink if it was handled - } - - if (!hasMatch) { - logMatch(hasMatch, null, received, null) - } - - return hasMatch - } - - override fun register(deepLinks: Collection) { - registries = (registries + deepLinks).distinctBy(DeepLink::id) - - Timber.d( - """ - Registered deep links - |- Registries: $registries - """.trimIndent(), - ) - } - - override fun register(deepLink: DeepLink) { - registries = (registries + deepLink).distinctBy(DeepLink::id) - - Timber.d( - """ - Registered deep link - |- Registries: $registries - """.trimIndent(), - ) - } - - override fun unregister(deepLinks: Collection) { - registries = registries.filter { it !in deepLinks } - - Timber.d( - """ - Unregistered deep links - |- Registries: $registries - """.trimIndent(), - ) - } - - override fun unregister(deepLink: DeepLink) { - registries = registries.filter { it.id != deepLink.id } - - Timber.d( - """ - Unregistered deep link - |- Registries: $registries - """.trimIndent(), - ) - } - - override fun unregisterByIds(ids: Collection) { - registries = registries.filter { it.id !in ids } - - Timber.d( - """ - Unregistered deep links - |- Registries: $registries - """.trimIndent(), - ) - } - - override fun triggerDelayedDeeplink(deepLinkClass: Class) { - val received = lastDeepLink - if (received != null) { - var hasMatch = false - registries - .filterIsInstance(deepLinkClass) - .forEach { deepLink -> - if (!deepLink.shouldHandleDelayed) return@forEach - val expected = deepLink.uri.toUri() - if (!isMatches(expected, received)) return@forEach - hasMatch = true - - val params = getParams(expected, received) - logMatch(hasMatch, expected, received, params) - deepLink.onReceive(params) - } - - if (!hasMatch) { - logMatch(hasMatch, null, received, null) - } - lastDeepLink = null // clear deeplink in any case handle or not - } - } - - override fun cancelDelayedDeeplink() { - lastDeepLink = null - } - - private fun logMatch(hasMatch: Boolean, expected: Uri?, received: Uri?, params: Map?) { - if (hasMatch) { - Timber.i( - """ - Matched deep link - |- Expected URI: $expected - |- Received URI: $received - |- Params: $params - """.trimIndent(), - ) - } else { - Timber.i( - """ - No match found for deep link - |- Received URI: $received - |- Registries: $registries - """.trimIndent(), - ) - } - } - - private fun isMatches(received: Uri, expected: Uri): Boolean { - if (received == expected) return true - if (received.authority != expected.authority || - received.pathSegments.size != expected.pathSegments.size - ) { - return false - } - - received.pathSegments.forEachIndexed { index, receivedSegment -> - val expectedSegment = expected.pathSegments[index] - if (receivedSegment != expectedSegment && - !(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}")) - ) { - return false - } - } - - return true - } - - private fun getParams(received: Uri, expected: Uri): Map { - val params = mutableMapOf() - - received.pathSegments.forEachIndexed { index, receivedSegment -> - val expectedSegment = expected.pathSegments[index] - if (receivedSegment != expectedSegment && - receivedSegment.startsWith(prefix = "{") && - receivedSegment.endsWith(suffix = "}") - ) { - val path = receivedSegment - .replace(oldValue = "{", newValue = "") - .replace(oldValue = "}", newValue = "") - - params[path] = expectedSegment - } - } - - expected.queryParameterNames.forEach { paramName -> - expected.getQueryParameter(paramName)?.let { param -> - params[paramName] = param - } - } - - return params - } -} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/AppComponentContextExtensions.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/AppComponentContextExtensions.kt deleted file mode 100644 index 594b8305ca..0000000000 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/AppComponentContextExtensions.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.core.deeplink.utils - -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.deeplink.DeepLink -import com.tangem.core.deeplink.DeepLinksRegistry - -fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, vararg deepLinks: DeepLink) { - registerDeepLinks(registry, deepLinks.toList()) -} - -fun AppComponentContext.registerDeepLinks(registry: DeepLinksRegistry, deepLinks: Collection) { - lifecycle.subscribe( - onCreate = { - registry.register(deepLinks) - }, - onDestroy = { - registry.unregister(deepLinks) - }, - ) -} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 2ad0dfb2fe..ce1167a434 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1316,7 +1316,7 @@ Die Genehmigung des Swaps ist im Gange und wird in Kürze abgeschlossen sein. Genehmigung in Arbeit Der Mindestbetrag für den Tausch beträgt %1$s. Bitte stelle sicher, dass der Restsaldo nach dem Swap nicht unter %2$s liegt. - Du hast keine %s austauschbaren Coins in Deiner Liste + Sie haben keine Token in Ihrem Portfolio, gegen die %s getauscht werden kann. Bitte fügen Sie einen anderen Token hinzu, um den Tausch durchzuführen. Keine Token zum Tauschen verfügbar Um eine Transaktion durchzuführen, du etwas etwas einzahlen %1$s %2$s Die Gebühr %s kann nicht gedeckt werden diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c2084479a7..fb5d92c4e0 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1236,7 +1236,7 @@ La aprobación del intercambio está en progreso y se completará en breve Aprobación en proceso El monto mínimo de transacción es %1$s. Asegúrese de que el saldo restante después del canje no sea inferior a %2$s. - No tiene %s monedas negociables en su lista + No tienes tokens en tu portafolio a los que puedas intercambiar %s. Por favor, añade otro token para realizar el intercambio. No hay tokens disponibles para intercambiar Para realizar una transacción necesita depositar %1$s %2$s No se pueden cubrir %s tarifa diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index b3b307cac4..fb1cde4a04 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -49,6 +49,8 @@ Par défaut du système Thème Paramètres de l\'application + Ces mots ne peuvent pas être récupérés en cas de perte. Assurez-vous de les conserver en lieu sûr. + Ne partagez jamais ces mots. Quiconque les apprend peut voler toutes vos cryptomonnaies. Tangem ne vous les demandera jamais. Les %s mots ci-dessous constituent la phrase de récupération de votre portefeuille. Cette phrase vous permet de récupérer votre portefeuille en cas de perte de votre appareil. Pour masquer ou afficher vos soldes, il suffit de retourner l\'écran de votre appareil vers le bas ou de le désactiver dans les paramètres Ne plus afficher Compris @@ -216,6 +218,7 @@ Inaccessible Unstakez En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant. + Valeur copiée semaine avec Oui @@ -699,6 +702,7 @@ Par solde Organiser les jetons Dégrouper + Recevez des alertes pour les transactions entrantes sur les réseaux pris en charge. Sélectionnez dans la galerie Paramètres Vous n\'avez pas donné accès à votre caméra @@ -855,6 +859,7 @@ Montant invalide Les frais de commissions dépassent le solde Le total dépasse le solde + Échanger et envoyer Transaction envoyée Scannez la carte/ bague que vous souhaitez configurer. Oublier le portefeuille @@ -1085,6 +1090,7 @@ Utilisez %s ou scannez une carte/bague pour avoir accès à votre portefeuille Échec de la connexion : Cette dApp utilise Wallet Connect version1.0, qui n\'est pas prise en charge. Veuillez vous assurer que la dApp prend en charge Wallet Connect version2.0 pour réussir la connexion. Restez à jour avec les dernières fonctionnalités et actualités + Recevez des notifications des transactions entrantes Soyez le premier informé des nouvelles promotions Souhaitez-vous utiliser les\nnotifications push? Ajouter un nouveau portefeuille @@ -1157,6 +1163,7 @@ Obtenez-le maintenant avec 10 % de réduction Accédez à plus de 13 000 cryptomonnaies. Achetez, vendez, échangez et stakez en un seul clic.\nAssociez jusqu\'à trois cartes pour une sauvegarde. Découvrez le Portefeuille Tangem + Recevez des notifications sur les transactions entrantes du portefeuille et les mises à jour de Tangem. Paramètres du portefeuille Tangem Utilisez %s ou scannez une carte/bague pour déverrouiller l\'accès à votre portefeuille @@ -1184,7 +1191,7 @@ L\'approbation de l\'échange est en cours et sera achevée sous peu Approbation en cours Le montant minimum d\'échange est de %1$s. Veuillez vous assurer que le solde restant après l\'échange ne sera pas inférieur à %2$s. - Vous n\'avez pas de pièces échangeables %s dans votre liste + Vous n’avez aucun jeton dans votre portefeuille pouvant recevoir %s en échange. Veuillez ajouter un autre jeton pour effectuer l’échange. Aucun jeton disponible à échanger Pour effectuer une transaction, vous devez déposer %1$s %2$s Impossible de couvrir %s frais @@ -1243,9 +1250,13 @@ Certains réseaux sont actuellement inaccessibles. Veuillez réessayer plus tard. Certains réseaux sont inaccessibles Certains soldes de jetons n\'ont pas pu être mis à jour + Pas assez de %s. Rechargez votre compte XLM pour associer ce jeton Il s\'agit d\'une carte Testnet. Elle ne peut pas traiter les transactions et ne doit être utilisée qu\'à des fins de test et de développement. À des fins de test uniquement Le solde peut être obsolète. Rafraîchissez la page. + Domaine malveillant + Le portefeuille Tangem ne prend actuellement pas en charge %ss + dApp non prise en charge Connexions Déconnecter tout Texte sur la déconnexion de toutes les dApps diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index b828454968..19a625c9ee 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -4,6 +4,9 @@ とにかくスキップ アクセスコードが設定されていません アクセスコードを入力 + アクセスコードが間違っています。あと%s回間違えるとホットウォレットが削除されます。 + アクセスコードが間違っています。あと%s回入力エラーが発生するとアプリがロックされます。 + アクセスコードが間違っています。 \n %s秒待ってから再試行してください。 続行するには、以前に入力したコードを確認してください アクセスコードを再入力 ウォレットのロックを解除するには、 %s桁のアクセスコードを設定します。 @@ -655,7 +658,7 @@ ウォレットへ進む バックアップを完了する 暗号資産を受け取る - プライマリカードをスキャン + メインカードまたはリングをスキャン スキップする どのように機能しますか? すべての秘密鍵をカードまたはリング内で生成し、安全なウォレットを作成しましょう @@ -706,7 +709,7 @@ バックアップデバイスなし 通知 バックアップデバイスが1つ追加されました - カードを準備してください + カードまたはリングを用意してください バックアップデバイス2つが追加されました 始めるには、ウォレットに任意の金額を入金するだけです 始めるには、ウォレットに%1$s %2$s以上入金するだけです @@ -924,6 +927,7 @@ 無効な金額 手数料が残高を超えています 合計金額が残高を超えています + スワップして送信 トークンを送信すれば、送信中に変換されます。受信者は必要なものをシームレスに受け取ります。 受信者に送信されます 受取金額 @@ -1296,8 +1300,8 @@ スワップ承認は現在進行中で、まもなく完了する予定です。 承認が進行中 最低のスワップ金額は%1$s です。スワップ後の残金が%2$s を下回らないようにしてください。 - あなたのリストには、交換可能な %s トークンがありません。 - スワップ可能なトークンがありません + ポートフォリオ内に%sとスワップ可能なトークンがありません。交換を有効にするには、別のトークンを追加してください。 + 互換性のあるトークンが追加されていません 取引を行うには、 %1$s %2$sを入金する必要があります。 %s 手数料を支払えません 受け取る金額は、 %s 以上である必要があります。 @@ -1375,6 +1379,8 @@ エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました + Tangemウォレットは、現在%sをサポートしていません。 + サポートされていないdApp エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 不明なエラーが発生しました Tangemは現在%sで必要なネットワークをサポートしていません。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 32178f4574..0283698666 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -620,7 +620,7 @@ Перейти к моему кошельку Завершение бэкапа Получить криптовалюту - Сканировать основную карту + Сканировать основную карту или кольцо Пропустить Как это работает? Давайте сгенерируем все ключи на вашей карте или кольце и создадим безопасный кошелек @@ -676,7 +676,7 @@ Нет резервных устройств Уведомления Добавлено одно резервное устройство - Подготовьте свою карту + Подготовьте свою карту или кольцо Добавлены два резервных девайса Пополните кошелек на любую сумму, чтобы начать пользоваться картой Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой @@ -723,7 +723,12 @@ Упорядочить токены Список Подробнее + Вы можете включить нотификации в настройках + Включить позже + Настройки Подключить нотификации + Получайте уведомления о входящих транзакциях в поддерживаемых сетях. + Уведомления о транзакциях Выбрать из галереи Настройки Вы не предоставили доступ к вашей камере @@ -1119,6 +1124,7 @@ Используйте %s или отсканируйте карту/кольцо, чтобы получить доступ к своему кошельку Соединение не удалось: это dApp использует Wallet Connect версии 1.0, которая не поддерживается. Убедитесь, что dApp поддерживает Wallet Connect версии 2.0 для успешного подключения. Будьте в курсе новых функций и новостей + Получайте уведомления о входящих транзакциях Узнавайте первым о новых акциях Хотите использовать Push-уведомления? Добавить новый кошелек @@ -1174,6 +1180,7 @@ Получить с 10% скидкой Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. Откройте Tangem Wallet + Получайте уведомления о входящих транзакциях в кошельке и обновлениях Tangem. Уведомления о транзакциях Настройки кошелька Tangem @@ -1202,7 +1209,7 @@ Разрешение обмена в процессе и будет скоро завершено Разрешение в процессе Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. - У вас в списке нет монет доступных для обмена с %s + У вас нет токенов в портфеле, на которые можно обменять %s. Пожалуйста, добавьте другой токен, чтобы выполнить обмен. Нет доступных для обмена токенов Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s Невозможно покрыть комиссию %s @@ -1268,6 +1275,7 @@ Согласно правилам сети, чтобы пополнить этот токен, необходимо сначала открыть Trustline — это позволит вашему кошельку принимать и хранить этот актив. Откройте Trustline Пожалуйста, вернитесь в браузер и выполните повторное подключение через WalletConnect. + Неподдерживаемый dApp Выбрана не верная карта или кольцо Адрес Подключение diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index d1048dabe7..3669539d24 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -316,6 +316,7 @@ Очікуємо на депозит Очікуємо на депозит... Повернено + Повернення коштів Надсилаємо вам Надсилаємо вам... Надіслано @@ -370,6 +371,7 @@ Щоб продовжити, вам потрібно дозволити смарт-контракту %1s використовувати ваш %2s Надати дозвіл Необмежено + Створити новий гаманець Купити Сканувати в %s @@ -572,10 +574,11 @@ cNFT і pNFT наразі не підтримуються. Будь ласка, не надсилайте їх на свій гаманець. Надіслати NFT Риси + Безіменна колекція NFTs в %2$d колекціях%1$d %1$d NFT в %2$d колекціi - + %1$d NFTs в %2$d колекціях %1$d NFTs в %2$d колекціях @@ -720,6 +723,7 @@ Список Більше Увімкнути сповіщення + Отримуйте сповіщення про вхідні транзакції в підтримуваних мережах. Виберіть з галереї Налаштування Ви не надали доступ до своєї камери @@ -829,6 +833,9 @@ Комісія може сягати до Недопустимий Memo Покриття мережевої комісії + Nonce + Унікальний номер транзакції. Змініть його, щоб пришвидшити відправку або скасувати завислу операцію. + Введіть nonce… Недостатньо коштів для здійснення переказу, оскільки загальна сума комісії та переказу перевищує наявний баланс Сума перевищує баланс Для збереження вашого акаунту у блокчейні та захисту від можливих ризиків необхідний баланс не менше %s. Ця сума залишиться на вашому рахунку та не може бути знята. @@ -837,6 +844,7 @@ Встановлена комісія завелика Через особливості мережі %1$s комісія за переказ всього балансу вища. Щоб зменшити комісію, Ви можете залишити %2$s. Підвищена комісія + Рахунок одержувача не активований. Мінімальна сума переказу повинна бути не менша балансу, необхідного для покриття арендної плати: %1$s. Баланс вашого рахунку не може бути меншим за орендну плату. Будь ласка, залиште на рахунку не менше %1$s або виведіть всі кошти. Включена комісія перевищує суму переказу, що призводить до від’ємного значення Недопустима сума @@ -1095,6 +1103,8 @@ валідатор: %s Мінімум %s Мінімальна сума транзакції становить %1$s. + Комісії мережі Tron для популярних токенів можуть бути вищими. Стейкінг TRX може допомогти знизити транзакційні витрати. + Заощаджуйте на Tron комісіях Спробуйте знову Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу @@ -1111,6 +1121,7 @@ Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. Будьте в курсі останніх функцій та новин + Отримуйте сповіщення про вхідні транзакції Дізнавайтеся першими про нові акції Бажаєте використовувати Push-повідомлення? Додати новий гаманець @@ -1122,6 +1133,7 @@ Перейменування гаманця Розблокувати все Розблокувати все з %s + PIN-код не прийнятий. Спробуйте ще раз або введіть інший код. Блокчейн недоступний. Спробуйте пізніше Відскануйте картку або кільце Цей гаманець вже був активований раніше.\nЯкщо це було зроблено не вами, зверніться до служби підтримки.\nTangem ніколи не продає гаманці разом з попередньо згенерованим кодом доступу. @@ -1165,6 +1177,7 @@ Отримати з 10% знижкою Доступ до 13,000+ криптовалют. Купуйте, продавайте, обмінюйте та стейкайте одним дотиком.\nЗ’єднайте до трьох карток для бекапу. Відкрийте Tangem Wallet + Отримуйте сповіщення про вхідні транзакції в гаманці та оновлення Tangem. Сповіщення про транзакції Налаштування гаманця Tangem @@ -1193,7 +1206,7 @@ Дозвіл обміну триває і незабаром буде завершено Затвердження в процесі Мінімальна сума обміну становить - %1$s. Будь ласка, переконайтеся, що залишок на рахунку після обміну буде не менше за %2$s. - У вашому списку немає доступних монет для обміну %s + У вашому портфелі немає токенів, на які можна обміняти %s. Будь ласка, додайте інший токен, щоб виконати обмін. Немає доступних токенів для обміну Щоб здійснити транзакцію, вам потрібно внести трохи %1$s %2$s Неможливо покрити комісію %s @@ -1257,8 +1270,23 @@ Це картка Testnet. Вона не може обробляти транзакції і повинна використовуватися лише для тестування та розробки. Лише для цілей тестування Баланс може бути застарілим. Оновіть сторінку. + Відкрити Trustline + Відповідно до правил мережі, щоб поповнити цей токен, спочатку потрібно відкрити Trustline — це дозволить вашому гаманцю приймати та зберігати цей актив. + Відкрийте Trustline + Будь ласка, поверніться до браузеру і повторно підключіться через WalletConnect. + Обрана не вірна картка або кільце Адреса + Підключення + Переглянути баланс гаманця та активність + Запит на підключення Вміст + Копіювати дані + Запит від + Тип підпису + До + Запит транзакції + Запит транзакції + Підключення гаманця Відмовитися Ви не завершили резервне копіювання. Бажаєте продовжити? Так, поновити diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d6919003f0..84c5ab2dc5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -4,6 +4,9 @@ Skip anyway Access Code not set Enter Access Code + Wrong access code. Your hot wallet will be deleted after %s more incorrect attempts. + Wrong access code. App will be locked with %s more input errors + Wrong access code.\nPlease wait %s seconds and try again. Confirm your previously entered code to continue Re-enter Access Code Set a %s-digit Access Code to unlock your wallet. @@ -58,6 +61,9 @@ System default Theme App settings + Add Wallet + Select a wallet to log in + Welcome back! You successfully backed up your wallet. These words can’t be recovered if lost. Make sure to keep it somewhere secure. Backup Completed @@ -432,6 +438,7 @@ Stay up to date with the latest features and news Seed phrase backup Create Mobile Wallet + Mobile Wallet This information was generated with AI.\nTap here, if you find any errors. To change the access code tap the card or ring as shown above and do not remove until the end of the operation To change the passcode tap the card as shown above and do not remove until the end of the operation @@ -666,7 +673,7 @@ Continue to my wallet Finalize backup Receive crypto - Scan primary card + Scan primary card or ring Skip for later How does it work? Let\'s generate all the keys on your card or ring and create a secure wallet @@ -719,7 +726,7 @@ No backup devices Notifications One backup device added - Prepare your card + Prepare your card or ring Two backup devices added To get started, simply top up the wallet with any amount To get started, simply top up the wallet with more than %1$s %2$s @@ -1360,8 +1367,8 @@ Swap approval is underway and will be completed shortly Approval in progress The minimum swapping amount is %1$s. Please ensure that the remaining balance after the swap will not be less than %2$s. - You do not have any %s exchangeable coins in your list - No available tokens to swap + You don’t have any tokens in your portfolio that %s can be swapped to. Please add another token to enable the exchange. + No compatible tokens added To make a transaction you need to deposit some %1$s %2$s Unable to cover %s fee The amount to receive must be at least %s diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt index 7029f1c2e0..336bec808f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheetTitle.kt @@ -3,10 +3,7 @@ package com.tangem.core.ui.components.bottomsheets.modal import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -20,6 +17,7 @@ import com.tangem.core.ui.components.buttons.small.TangemIconButton 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.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -30,6 +28,7 @@ import com.tangem.core.ui.res.TangemThemePreview fun TangemModalBottomSheetTitle( modifier: Modifier = Modifier, title: TextReference? = null, + subtitle: TextReference? = null, @DrawableRes startIconRes: Int? = null, onStartClick: (() -> Unit)? = null, @DrawableRes endIconRes: Int? = null, @@ -49,13 +48,23 @@ fun TangemModalBottomSheetTitle( .align(Alignment.CenterStart), ) } - if (title != null) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.align(Alignment.Center), - ) + Column(modifier = Modifier.align(Alignment.Center)) { + if (title != null) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } + if (subtitle != null) { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + } } if (endIconRes != null && onEndClick != null) { TangemIconButton( @@ -79,6 +88,7 @@ private fun Preview_TangemModalBottomSheetTitle( TangemThemePreview { TangemModalBottomSheetTitle( title = params.title, + subtitle = params.subtitle, startIconRes = params.startIconRes, onStartClick = params.onStartClick, endIconRes = params.endIconRes, @@ -90,6 +100,7 @@ private fun Preview_TangemModalBottomSheetTitle( private data class TangemModalBottomSheetTitleData( val title: TextReference?, + val subtitle: TextReference?, val startIconRes: Int?, val onStartClick: (() -> Unit)?, val endIconRes: Int?, @@ -104,6 +115,7 @@ private class TangemModalBottomSheetTitleProvider : PreviewParameterProvider Unit, modifier: Modifier = Modifier, placeholder: TextReference? = null, + description: TextReference? = null, titleColor: Color = TangemTheme.colors.text.secondary, textColor: Color = TangemTheme.colors.text.primary1, isSingleLine: Boolean = false, @@ -71,11 +78,32 @@ fun InputRowEnter( .padding(TangemTheme.dimens.spacing12), ) { Column(modifier = Modifier.weight(1f)) { - Text( - text = title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = titleColor, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = titleColor, + ) + if (description != null) { + TangemTooltip( + modifier = Modifier + .size(16.dp) + .clip(CircleShape), + text = description.resolveReference(), + content = { contentModifier -> + Icon( + modifier = contentModifier.size(16.dp), + painter = painterResource(R.drawable.ic_token_info_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + }, + ) + } + } SimpleTextField( value = text, onValueChange = onValueChange, @@ -121,6 +149,7 @@ private fun InputRowEnterPreview( text = data.text, iconRes = data.iconRes, showDivider = data.showDivider, + description = stringReference(""), onValueChange = {}, modifier = Modifier.background(TangemTheme.colors.background.action), ) 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 5878b03d62..b129ec948f 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 @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.inputrow import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon @@ -8,6 +9,7 @@ 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.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -15,6 +17,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.tooltip.TangemTooltip import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -118,6 +121,7 @@ fun InputRowEnterInfoAmountV2( modifier: Modifier = Modifier, symbol: String? = null, info: TextReference? = null, + description: TextReference? = null, titleColor: Color = TangemTheme.colors.text.secondary, textColor: Color = TangemTheme.colors.text.primary1, infoColor: Color = TangemTheme.colors.text.tertiary, @@ -133,7 +137,9 @@ fun InputRowEnterInfoAmountV2( paddingValues = PaddingValues(horizontal = 12.dp), ) { Column( - modifier = Modifier.fillMaxWidth().padding(16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), ) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) { Text( @@ -141,12 +147,22 @@ fun InputRowEnterInfoAmountV2( style = TangemTheme.typography.subtitle2, color = titleColor, ) - Icon( - modifier = Modifier.size(16.dp), - painter = painterResource(R.drawable.ic_token_info_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) + if (description != null) { + TangemTooltip( + modifier = Modifier + .size(16.dp) + .clip(CircleShape), + text = description.resolveReference(), + content = { contentModifier -> + Icon( + modifier = contentModifier.size(16.dp), + painter = painterResource(R.drawable.ic_token_info_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + }, + ) + } } Row { AmountTextField( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tooltip/TangemTooltip.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tooltip/TangemTooltip.kt new file mode 100644 index 0000000000..65936ee076 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tooltip/TangemTooltip.kt @@ -0,0 +1,86 @@ +package com.tangem.core.ui.components.tooltip + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TangemTooltip(text: String, content: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier) { + val tooltipState = rememberTooltipState(isPersistent = true) + val coroutineScope = rememberCoroutineScope() + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(spacingBetweenTooltipAndAnchor = 8.dp), + state = tooltipState, + modifier = modifier, + tooltip = { + PlainTooltip( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + caretSize = DpSize(width = 14.dp, height = 8.dp), + contentColor = TangemTheme.colors.text.primary2, + containerColor = TangemTheme.colors.icon.secondary, + content = { + Text( + modifier = Modifier.background(TangemTheme.colors.icon.secondary), + text = text, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary2, + ) + }, + ) + }, + content = { + content( + Modifier.clickableSingle( + onClick = { coroutineScope.launch { tooltipState.show() } }, + ), + ) + }, + ) +} + +@Preview +@Composable +private fun TangemTooltip_Preview() { + TangemThemePreview { + Box( + modifier = Modifier + .size(500.dp) + .background(TangemTheme.colors.background.secondary), + contentAlignment = Alignment.Center, + ) { + TangemTooltip( + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .size(64.dp), + text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis.", + content = { contentModifier -> + Icon( + modifier = contentModifier.size(64.dp), + painter = painterResource(R.drawable.ic_token_info_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt index 61be7f90d6..35c915ab68 100644 --- a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -70,8 +70,4 @@ internal class DefaultCardSdkConfigRepository( override fun setLinkedTerminal(isLinked: Boolean?) { sdk.config.linkedTerminal = isLinked } - - override fun setTangemApiProdEnvFlag(flag: Boolean) { - sdk.config.isTangemAttestationProdEnv = flag - } } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt index 137e425ac6..1299a92d0f 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CardCryptoCurrencyFactory.kt @@ -2,7 +2,6 @@ package com.tangem.data.common.currency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -57,23 +56,23 @@ interface CardCryptoCurrencyFactory { /** * Create default coins for multi currency card * - * @param scanResponse scan response + * @param userWallet user wallet */ - fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List + fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List /** * Create primary currency for single currency card * - * @param scanResponse scan response + * @param userWallet user wallet */ @Throws - fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency + fun createPrimaryCurrencyForSingleCurrencyCard(userWallet: UserWallet.Cold): CryptoCurrency /** * Create currencies for single currency card with token (like, NODL) * - * @param scanResponse scan response + * @param userWallet user wallet */ @Throws - fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List + fun createCurrenciesForSingleCurrencyCardWithToken(userWallet: UserWallet.Cold): List } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 016833b806..228f35327f 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -8,7 +8,7 @@ import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.data.common.network.NetworkFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken @@ -46,7 +46,7 @@ class CryptoCurrencyFactory( sdkToken: SdkToken, blockchain: Blockchain, extraDerivationPath: String?, - scanResponse: ScanResponse, + userWallet: UserWallet, ): CryptoCurrency.Token? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") @@ -56,7 +56,7 @@ class CryptoCurrencyFactory( val network = networkFactory.create( blockchain = blockchain, extraDerivationPath = extraDerivationPath, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null val id = getTokenId(network, sdkToken) @@ -76,7 +76,7 @@ class CryptoCurrencyFactory( fun createCoin( blockchain: Blockchain, extraDerivationPath: String?, - scanResponse: ScanResponse, + userWallet: UserWallet, ): CryptoCurrency.Coin? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") @@ -86,15 +86,15 @@ class CryptoCurrencyFactory( val network = networkFactory.create( blockchain = blockchain, extraDerivationPath = extraDerivationPath, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null return createCoin(network) } - fun createCoin(networkId: String, extraDerivationPath: String?, scanResponse: ScanResponse): CryptoCurrency.Coin? { + fun createCoin(networkId: String, extraDerivationPath: String?, userWallet: UserWallet): CryptoCurrency.Coin? { val blockchain = Blockchain.fromNetworkId(networkId) ?: Blockchain.Unknown - return createCoin(blockchain, extraDerivationPath, scanResponse) + return createCoin(blockchain, extraDerivationPath, userWallet) } fun createCoin(network: Network): CryptoCurrency.Coin { @@ -115,7 +115,7 @@ class CryptoCurrencyFactory( token: Token, networkId: String, extraDerivationPath: String?, - scanResponse: ScanResponse, + userWallet: UserWallet, ): CryptoCurrency.Token? { val sdkToken = SdkToken( name = token.name, @@ -129,7 +129,7 @@ class CryptoCurrencyFactory( sdkToken = sdkToken, blockchain = blockchain, extraDerivationPath = extraDerivationPath, - scanResponse = scanResponse, + userWallet = userWallet, ) } @@ -141,7 +141,8 @@ class CryptoCurrencyFactory( decimals = cryptoCurrency.decimals, id = cryptoCurrency.id.rawCurrencyId?.value, ) - val blockchain = Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown + val blockchain = + Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index 58b6c3415b..d2eb6d1e57 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -11,11 +11,9 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency -import com.tangem.domain.wallets.models.requireColdWallet /** * Default implementation of factory for creating list of [CryptoCurrency] for selected card @@ -54,12 +52,12 @@ internal class DefaultCardCryptoCurrencyFactory( // single-currency wallet with token (NODL) if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - val currencies = createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + val currencies = createCurrenciesForSingleCurrencyCardWithToken(userWallet) return mapOf(cardNetwork to currencies) } // single-currency wallet - val primaryCurrency = createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + val primaryCurrency = createPrimaryCurrencyForSingleCurrencyCard(userWallet) return mapOf(cardNetwork to listOf(primaryCurrency)) } @@ -82,11 +80,11 @@ internal class DefaultCardCryptoCurrencyFactory( // single-currency wallet with token (NODL) if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - return createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + return createCurrenciesForSingleCurrencyCardWithToken(userWallet) } // single-currency wallet - return createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse).let(::listOf) + return createPrimaryCurrencyForSingleCurrencyCard(userWallet).let(::listOf) } override suspend fun createCurrenciesForMultiCurrencyCard( @@ -98,44 +96,54 @@ internal class DefaultCardCryptoCurrencyFactory( return getMultiWalletCurrencies(userWallet = userWallet, networks = networks) } - override fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { - require(scanResponse.cardTypesResolver.isMultiwalletAllowed()) { "It isn't multi-currency wallet" } + override fun createDefaultCoinsForMultiCurrencyWallet(userWallet: UserWallet): List { + require(userWallet.isMultiCurrency) { "It isn't multi-currency wallet" } - val card = scanResponse.card + val blockchains = when (userWallet) { + is UserWallet.Cold -> { + val card = userWallet.scanResponse.card - var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { - demoConfig.demoBlockchains - } else { - listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - } + var blockchainsInternal = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } - if (card.isTestCard) { - blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + if (card.isTestCard) { + blockchainsInternal = blockchainsInternal.mapNotNull { it.getTestnetVersion() } + } + + blockchainsInternal + } + + is UserWallet.Hot -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum) } return blockchains.mapNotNull { cryptoCurrencyFactory.createCoin( blockchain = it, extraDerivationPath = null, - scanResponse = scanResponse, + userWallet = userWallet, ) } } - override fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { - require(scanResponse.cardTypesResolver.isSingleWallet()) { "It isn't single-currency wallet" } + override fun createPrimaryCurrencyForSingleCurrencyCard(userWallet: UserWallet.Cold): CryptoCurrency { + require(userWallet.scanResponse.cardTypesResolver.isSingleWallet()) { + "It isn't single-currency wallet" + } - return with(getSingleWalletCurrencies(scanResponse)) { + return with(getSingleWalletCurrencies(userWallet)) { primaryToken ?: coin } } - override fun createCurrenciesForSingleCurrencyCardWithToken(scanResponse: ScanResponse): List { - require(scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + override fun createCurrenciesForSingleCurrencyCardWithToken(userWallet: UserWallet.Cold): List { + require(userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { "It isn't single-currency wallet with token" } - return with(getSingleWalletCurrencies(scanResponse)) { + return with(getSingleWalletCurrencies(userWallet)) { listOfNotNull(coin, primaryToken) } } @@ -151,7 +159,7 @@ internal class DefaultCardCryptoCurrencyFactory( tokens = response.tokens.filter { token -> networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } }, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) .groupBy(CryptoCurrency::network) @@ -169,19 +177,19 @@ internal class DefaultCardCryptoCurrencyFactory( return responseCryptoCurrenciesFactory.createCurrencies( tokens = response.tokens.filter { token -> token.networkId in networkIds }, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) .groupBy { it.network.id.rawId } } - private fun getSingleWalletCurrencies(scanResponse: ScanResponse): SingleWalletCurrencies { - val resolver = scanResponse.cardTypesResolver + private fun getSingleWalletCurrencies(userWallet: UserWallet.Cold): SingleWalletCurrencies { + val resolver = userWallet.cardTypesResolver val blockchain = resolver.getBlockchain() val coin = cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - scanResponse = scanResponse, + userWallet = userWallet, ) requireNotNull(coin) { "Coin for the single currency card cannot be null" } @@ -191,12 +199,15 @@ internal class DefaultCardCryptoCurrencyFactory( sdkToken = token, blockchain = blockchain, extraDerivationPath = null, - scanResponse = scanResponse, + userWallet = userWallet, ) } return SingleWalletCurrencies(coin = coin, primaryToken = primaryToken) } - private data class SingleWalletCurrencies(val coin: CryptoCurrency, val primaryToken: CryptoCurrency?) + private data class SingleWalletCurrencies( + val coin: CryptoCurrency, + val primaryToken: CryptoCurrency?, + ) } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index b7c3cde6f6..692d0b9b62 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -8,7 +8,7 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import timber.log.Timber import javax.inject.Inject import com.tangem.blockchain.common.Token as SdkToken @@ -17,41 +17,41 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( private val networkFactory: NetworkFactory, ) { - fun createCurrency(currencyId: String, response: UserTokensResponse, scanResponse: ScanResponse): CryptoCurrency { + fun createCurrency(currencyId: String, response: UserTokensResponse, userWallet: UserWallet): CryptoCurrency { return response.tokens .asSequence() - .mapNotNull { createCurrency(it, scanResponse) } + .mapNotNull { createCurrency(it, userWallet) } .first { it.id.value == currencyId } } - fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List { - return createCurrencies(tokens = response.tokens, scanResponse = scanResponse) + fun createCurrencies(response: UserTokensResponse, userWallet: UserWallet): List { + return createCurrencies(tokens = response.tokens, userWallet = userWallet) } - fun createCurrencies(tokens: List, scanResponse: ScanResponse): List { + fun createCurrencies(tokens: List, userWallet: UserWallet): List { return tokens .asSequence() - .mapNotNull { createCurrency(it, scanResponse) } + .mapNotNull { createCurrency(it, userWallet) } .distinctBy(CryptoCurrency::id) .toList() } - fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? { + fun createCurrency(responseToken: UserTokensResponse.Token, userWallet: UserWallet): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } - if (scanResponse.cardTypesResolver.isTestCard()) { + if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isTestCard()) { blockchain = blockchain.getTestnetVersion() ?: blockchain } val sdkToken = createSdkToken(responseToken) return if (sdkToken == null) { - createCoin(blockchain, responseToken, scanResponse) + createCoin(blockchain, responseToken, userWallet) } else { - createToken(blockchain, sdkToken, responseToken.derivationPath, scanResponse) + createToken(blockchain, sdkToken, responseToken.derivationPath, userWallet) } } @@ -70,12 +70,12 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( private fun createCoin( blockchain: Blockchain, responseToken: UserTokensResponse.Token, - scanResponse: ScanResponse, + userWallet: UserWallet, ): CryptoCurrency.Coin? { val network = networkFactory.create( blockchain = blockchain, extraDerivationPath = responseToken.derivationPath, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null return CryptoCurrency.Coin( @@ -105,12 +105,12 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( blockchain: Blockchain, sdkToken: Token, responseDerivationPath: String?, - scanResponse: ScanResponse, + userWallet: UserWallet, ): CryptoCurrency.Token? { val network = networkFactory.create( blockchain = blockchain, extraDerivationPath = responseDerivationPath, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null val id = getTokenId(network, sdkToken) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index 1e099509fc..00f4114d9a 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -8,10 +8,9 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import timber.log.Timber import javax.inject.Inject @@ -31,19 +30,18 @@ class NetworkFactory @Inject constructor( * * @param blockchain blockchain * @param extraDerivationPath extra derivation path - * @param scanResponse scan response + * @param userWallet user wallet */ - fun create(blockchain: Blockchain, extraDerivationPath: String?, scanResponse: ScanResponse): Network? { + fun create(blockchain: Blockchain, extraDerivationPath: String?, userWallet: UserWallet): Network? { return create( blockchain = blockchain, derivationPath = createDerivationPath( blockchain = blockchain, extraDerivationPath = extraDerivationPath, - cardDerivationStyleProvider = scanResponse.derivationStyleProvider, + cardDerivationStyleProvider = userWallet.derivationStyleProvider, ), - canHandleTokens = scanResponse.card.canHandleToken( + canHandleTokens = userWallet.canHandleToken( blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, excludedBlockchains = excludedBlockchains, ), ) @@ -54,17 +52,16 @@ class NetworkFactory @Inject constructor( * * @param networkId network id * @param derivationPath derivation path - * @param scanResponse scan response + * @param userWallet user wallet */ - fun create(networkId: Network.ID, derivationPath: Network.DerivationPath, scanResponse: ScanResponse): Network? { + fun create(networkId: Network.ID, derivationPath: Network.DerivationPath, userWallet: UserWallet): Network? { val blockchain = networkId.toBlockchain() return create( blockchain = blockchain, derivationPath = derivationPath, - canHandleTokens = scanResponse.card.canHandleToken( + canHandleTokens = userWallet.canHandleToken( blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, excludedBlockchains = excludedBlockchains, ), ) diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index 944a461dee..c9589eeed9 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -308,7 +308,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { val multiWallet = model.multiWallet // Act - val actual = factory.createDefaultCoinsForMultiCurrencyCard(scanResponse = multiWallet.scanResponse) + val actual = factory.createDefaultCoinsForMultiCurrencyWallet(multiWallet) // Assert val expected = model.expected @@ -355,7 +355,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Act val actual = runCatching { - factory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = singleWallet.scanResponse) + factory.createPrimaryCurrencyForSingleCurrencyCard(singleWallet) } // Assert @@ -405,7 +405,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Act val actual = runCatching { - factory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = userWallet.scanResponse) + factory.createCurrenciesForSingleCurrencyCardWithToken(userWallet) } // Assert @@ -554,7 +554,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { sdkToken = userWallet.scanResponse.cardTypesResolver.getPrimaryToken()!!, blockchain = blockchain, extraDerivationPath = null, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, )!! } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt index 12de89dfc7..65a18339a2 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.common.test.utils.ProvideTestModels import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.configs.GenericCardConfig @@ -12,6 +13,7 @@ import com.tangem.domain.common.configs.MultiWalletCardConfig import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.Nested @@ -43,7 +45,7 @@ class NetworkFactoryTest { val actual = networkFactory.create( blockchain = Blockchain.Ethereum, extraDerivationPath = null, - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), ) // Assert @@ -59,14 +61,14 @@ class NetworkFactoryTest { networkFactory.create( blockchain = model.blockchain, extraDerivationPath = model.extraDerivationPath, - scanResponse = model.scanResponse, + userWallet = model.userWallet, ) } is CreateTestModel.Second -> { networkFactory.create( networkId = model.networkId, derivationPath = model.derivationPath, - scanResponse = model.scanResponse, + userWallet = model.userWallet, ) } is CreateTestModel.Third -> { @@ -89,27 +91,27 @@ class NetworkFactoryTest { CreateTestModel.First( blockchain = Blockchain.Unknown, extraDerivationPath = null, // never-mind - scanResponse = createMultiWalletScanResponse(), // never-mind + userWallet = createUserWallet(), expected = null, ), createFirst( extraDerivationPath = null, - scanResponse = createGenericScanResponse(), // default derivation path is null + userWallet = createUserWallet(createGenericScanResponse()), // default derivation path is null expectedDerivationPath = Network.DerivationPath.None, ), createFirst( extraDerivationPath = null, - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), expectedDerivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0"), // use default ), createFirst( extraDerivationPath = "m/44'/60'/0'/0/0", // as default - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), expectedDerivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0"), ), createFirst( extraDerivationPath = "m/84'/0'/0'/0/0", - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), expectedDerivationPath = Network.DerivationPath.Custom(value = "m/84'/0'/0'/0/0"), ), // endregion @@ -118,23 +120,23 @@ class NetworkFactoryTest { CreateTestModel.Second( networkId = Network.ID(value = "1", derivationPath = Network.DerivationPath.None), derivationPath = Network.DerivationPath.None, // never-mind - scanResponse = createMultiWalletScanResponse(), // never-mind + userWallet = createUserWallet(), // never-mind expected = null, ), createSecond( - scanResponse = createGenericScanResponse(), // default derivation path is null + userWallet = createUserWallet(createGenericScanResponse()), // default derivation path is null derivationPath = Network.DerivationPath.None, ), createSecond( - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), derivationPath = Network.DerivationPath.None, ), createSecond( - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0"), ), createSecond( - scanResponse = createMultiWalletScanResponse(), + userWallet = createUserWallet(), derivationPath = Network.DerivationPath.Custom(value = "m/84'/0'/0'/0/0"), ), // endregion @@ -176,13 +178,13 @@ class NetworkFactoryTest { private fun createFirst( extraDerivationPath: String?, - scanResponse: ScanResponse, + userWallet: UserWallet, expectedDerivationPath: Network.DerivationPath, ): CreateTestModel.First { return CreateTestModel.First( blockchain = Blockchain.Ethereum, extraDerivationPath = extraDerivationPath, - scanResponse = scanResponse, + userWallet = userWallet, expected = MockCryptoCurrencyFactory().ethereum.network.copy( id = Network.ID( value = Blockchain.Ethereum.id, @@ -195,13 +197,13 @@ class NetworkFactoryTest { } private fun createSecond( - scanResponse: ScanResponse, + userWallet: UserWallet, derivationPath: Network.DerivationPath, ): CreateTestModel.Second { return CreateTestModel.Second( networkId = Network.ID(value = Blockchain.Ethereum.id, derivationPath = derivationPath), derivationPath = derivationPath, - scanResponse = scanResponse, + userWallet = userWallet, expected = MockCryptoCurrencyFactory().ethereum.network.copy( id = Network.ID( value = Blockchain.Ethereum.id, @@ -232,6 +234,10 @@ class NetworkFactoryTest { ) } + private fun createUserWallet(scanResponse: ScanResponse = createMultiWalletScanResponse()): UserWallet.Cold { + return MockUserWalletFactory.create(scanResponse) + } + private fun createMultiWalletScanResponse(): ScanResponse { return MockScanResponseFactory.create( cardConfig = MultiWalletCardConfig, @@ -254,14 +260,14 @@ class NetworkFactoryTest { data class First( val blockchain: Blockchain, val extraDerivationPath: String?, - val scanResponse: ScanResponse, + val userWallet: UserWallet, override val expected: Network?, ) : CreateTestModel data class Second( val networkId: Network.ID, val derivationPath: Network.DerivationPath, - val scanResponse: ScanResponse, + val userWallet: UserWallet, override val expected: Network?, ) : CreateTestModel diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 49054eb06e..50cc20c0f3 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -95,7 +95,7 @@ internal class DefaultCustomTokensRepository( networkFactory.create( networkId = networkId, derivationPath = derivationPath, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ), ) { "Network [$networkId] not found while finding token" @@ -106,8 +106,8 @@ internal class DefaultCustomTokensRepository( symbol = null, ) - val supportedTokenNetworkIds = userWallet.scanResponse.card - .supportedBlockchains(userWallet.scanResponse.cardTypesResolver, excludedBlockchains) + val supportedTokenNetworkIds = userWallet + .supportedBlockchains(excludedBlockchains = excludedBlockchains) .filter(Blockchain::canHandleTokens) .map(Blockchain::toNetworkId) @@ -151,7 +151,7 @@ internal class DefaultCustomTokensRepository( networkFactory.create( networkId = networkId, derivationPath = derivationPath, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ), ) { "Network [$networkId] not found while creating coin" @@ -188,7 +188,7 @@ internal class DefaultCustomTokensRepository( networkFactory.create( networkId = networkId, derivationPath = derivationPath, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ), ) { "Network [$networkId] not found while creating custom token" @@ -261,7 +261,7 @@ internal class DefaultCustomTokensRepository( networkFactory.create( blockchain = blockchain, extraDerivationPath = null, - scanResponse = scanResponse, + userWallet = userWallet, ) } else { null diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 55f8660e21..acb7776dd7 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -31,7 +31,6 @@ import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.models.network.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListSource import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher @@ -142,13 +141,13 @@ internal class DefaultManageTokensRepository( managedCryptoCurrencyFactory.createWithCustomTokens( coinsResponse = updatedCoinsResponse, tokensResponse = tokensResponse, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) } else { managedCryptoCurrencyFactory.create( coinsResponse = updatedCoinsResponse, tokensResponse = tokensResponse, - scanResponse = userWallet?.requireColdWallet()?.scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) } @@ -178,7 +177,7 @@ internal class DefaultManageTokensRepository( testnetTokensConfig }, tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId), - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) return BatchFetchResult.Success( @@ -190,8 +189,8 @@ internal class DefaultManageTokensRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard( - userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( + userWallet = userWallet, ), isGroupedByNetwork = false, isSortedByBalance = false, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 769560c5bf..fa40d4b14c 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -15,13 +15,12 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import timber.log.Timber internal class ManagedCryptoCurrencyFactory( @@ -32,20 +31,20 @@ internal class ManagedCryptoCurrencyFactory( fun create( coinsResponse: CoinsResponse, tokensResponse: UserTokensResponse?, - scanResponse: ScanResponse?, + userWallet: UserWallet?, ): List { return coinsResponse.coins.mapNotNull { coin -> - createToken(coin, tokensResponse, coinsResponse.imageHost, scanResponse) + createToken(coin, tokensResponse, coinsResponse.imageHost, userWallet) } } fun createWithCustomTokens( coinsResponse: CoinsResponse, tokensResponse: UserTokensResponse, - scanResponse: ScanResponse, + userWallet: UserWallet, ): List { - val customTokens = createCustomTokens(tokensResponse, scanResponse) - val tokens = create(coinsResponse, tokensResponse, scanResponse) + val customTokens = createCustomTokens(tokensResponse, userWallet) + val tokens = create(coinsResponse, tokensResponse, userWallet) return customTokens + tokens } @@ -53,10 +52,10 @@ internal class ManagedCryptoCurrencyFactory( fun createTestnetWithCustomTokens( testnetTokensConfig: TestnetTokensConfig, tokensResponse: UserTokensResponse?, - scanResponse: ScanResponse, + userWallet: UserWallet, ): List { val customTokens = tokensResponse - ?.let { createCustomTokens(it, scanResponse) } + ?.let { createCustomTokens(it, userWallet) } ?: emptyList() val testnetTokens = testnetTokensConfig.tokens.map { testnetToken -> ManagedCryptoCurrency.Token( @@ -69,10 +68,10 @@ internal class ManagedCryptoCurrencyFactory( networkId = network.id, contractAddress = network.address, decimals = network.decimalCount, - scanResponse = scanResponse, + userWallet = userWallet, ) } ?: emptyList(), - addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, scanResponse), + addedIn = findAddedInNetworks(testnetToken.id, tokensResponse, userWallet), ) } @@ -81,28 +80,28 @@ internal class ManagedCryptoCurrencyFactory( private fun createCustomTokens( tokensResponse: UserTokensResponse, - scanResponse: ScanResponse, + userWallet: UserWallet, ): List = tokensResponse.tokens .mapNotNull { token -> - maybeCreateCustomToken(token, scanResponse) + maybeCreateCustomToken(token, userWallet) } private fun maybeCreateCustomToken( token: UserTokensResponse.Token, - scanResponse: ScanResponse, + userWallet: UserWallet, ): ManagedCryptoCurrency? { val blockchain = Blockchain.fromNetworkId(token.networkId) ?.takeUnless { it in excludedBlockchains } ?: return null - if (!checkIsCustomToken(token, blockchain, scanResponse.derivationStyleProvider)) { + if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) { return null } val network = networkFactory.create( blockchain = blockchain, extraDerivationPath = token.derivationPath, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null val contractAddress = token.contractAddress @@ -135,7 +134,7 @@ internal class ManagedCryptoCurrencyFactory( coinResponse: CoinsResponse.Coin, tokensResponse: UserTokensResponse?, imageHost: String?, - scanResponse: ScanResponse?, + userWallet: UserWallet?, ): ManagedCryptoCurrency? { if (coinResponse.networks.isEmpty() || !coinResponse.active) return null @@ -146,7 +145,7 @@ internal class ManagedCryptoCurrencyFactory( networkId = network.networkId, contractAddress = network.contractAddress, decimals = network.decimalCount?.toInt(), - scanResponse = scanResponse, + userWallet = userWallet, ) } .ifEmpty { return null } @@ -157,7 +156,7 @@ internal class ManagedCryptoCurrencyFactory( symbol = coinResponse.symbol, iconUrl = getIconUrl(coinResponse.id, imageHost), availableNetworks = availableNetworks, - addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, scanResponse), + addedIn = findAddedInNetworks(coinResponse.id, tokensResponse, userWallet), ) } @@ -165,7 +164,7 @@ internal class ManagedCryptoCurrencyFactory( networkId: String, contractAddress: String?, decimals: Int?, - scanResponse: ScanResponse?, + userWallet: UserWallet?, extraDerivationPath: String? = null, ): SourceNetwork? { val blockchain = Blockchain.fromNetworkId(networkId) @@ -175,10 +174,8 @@ internal class ManagedCryptoCurrencyFactory( val network = networkFactory.create( blockchain = blockchain, extraDerivationPath = extraDerivationPath, - derivationStyleProvider = scanResponse?.derivationStyleProvider, - canHandleTokens = scanResponse?.let { - it.card.canHandleToken(blockchain, it.cardTypesResolver, excludedBlockchains) - } ?: false, // use card specific check if available + derivationStyleProvider = userWallet?.derivationStyleProvider, + canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) ?: false, ) ?: return null return when { @@ -207,7 +204,7 @@ internal class ManagedCryptoCurrencyFactory( private fun findAddedInNetworks( currencyId: String, tokensResponse: UserTokensResponse?, - scanResponse: ScanResponse?, + userWallet: UserWallet?, ): Set { if (tokensResponse == null) return emptySet() @@ -220,10 +217,8 @@ internal class ManagedCryptoCurrencyFactory( networkFactory.create( blockchain = blockchain, extraDerivationPath = token.derivationPath, - derivationStyleProvider = scanResponse?.derivationStyleProvider, - canHandleTokens = scanResponse?.let { - it.card.canHandleToken(blockchain, it.cardTypesResolver, excludedBlockchains) - } ?: true, + derivationStyleProvider = userWallet?.derivationStyleProvider, + canHandleTokens = userWallet?.canHandleToken(blockchain, excludedBlockchains) ?: true, ) } else { null diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index f6d9ebbfbd..1780f9e5c4 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -24,7 +24,6 @@ import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.pagination.* import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -259,13 +258,13 @@ internal class DefaultMarketsTokenRepository( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) } else { val currencyNetwork = networkFactory.create( blockchain = blockchain, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) ?: return null cryptoCurrencyFactory.createToken( diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt index 4b46ecc55d..b4fc8494be 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -5,7 +5,6 @@ import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -48,7 +47,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( val network = networkFactory.create( networkId = status.id, derivationPath = status.id.derivationPath, - scanResponse = userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) ?: return@mapNotNullTo null NetworkStatus(network = network, value = status.value) diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index 7e30b4e8a7..d929ff58fe 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -67,7 +67,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.first().network @@ -75,7 +75,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.last().network @@ -94,12 +94,12 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } } @@ -130,7 +130,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.first().network @@ -138,7 +138,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.last().network @@ -146,7 +146,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = updatedSimpleStatuses.first().id, derivationPath = updatedSimpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns updatedStatuses.first().network @@ -154,7 +154,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = updatedSimpleStatuses.last().id, derivationPath = updatedSimpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns updatedStatuses.last().network // endregion @@ -177,12 +177,12 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } @@ -202,12 +202,12 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = updatedSimpleStatuses.first().id, derivationPath = updatedSimpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) networkFactory.create( networkId = updatedSimpleStatuses.last().id, derivationPath = updatedSimpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } } @@ -232,7 +232,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.first().network @@ -240,7 +240,7 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.last().network // endregion @@ -263,12 +263,12 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } @@ -312,14 +312,14 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.first().network every { networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns statuses.last().network // endregion @@ -336,7 +336,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder(inverse = true) { userWalletsStore.getSyncOrNull(any()) - networkFactory.create(networkId = any(), derivationPath = any(), scanResponse = any()) + networkFactory.create(networkId = any(), derivationPath = any(), userWallet = any()) } // Act 2 (emit) @@ -353,12 +353,12 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } } @@ -410,12 +410,12 @@ internal class DefaultMultiNetworkStatusProducerTest { networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) networkFactory.create( networkId = simpleStatuses.last().id, derivationPath = simpleStatuses.last().id.derivationPath, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } } @@ -429,10 +429,10 @@ internal class DefaultMultiNetworkStatusProducerTest { val userWallet = MockUserWalletFactory.create(scanResponse = scanResponse) - val ethNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).ethereum.network.copy( + val ethNetwork = MockCryptoCurrencyFactory(userWallet = userWallet).ethereum.network.copy( canHandleTokens = true, ) - val cardanoNetwork = MockCryptoCurrencyFactory(scanResponse = scanResponse).cardano.network + val cardanoNetwork = MockCryptoCurrencyFactory(userWallet = userWallet).cardano.network } } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 6887f4016e..4f779de5d6 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -29,7 +29,6 @@ import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.requireColdWallet -import com.tangem.features.nft.NFTFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -50,7 +49,6 @@ internal class DefaultNFTRepository @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, private val userWalletsStore: UserWalletsStore, - private val nftFeatureToggles: NFTFeatureToggles, private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, resources: Resources, @@ -210,7 +208,7 @@ internal class DefaultNFTRepository @Inject constructor( networkFactory.create( blockchain = it, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) } .filter { it.canHandleNFTs(userWalletId) } @@ -545,13 +543,9 @@ internal class DefaultNFTRepository @Inject constructor( private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean { val scanResponse = userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().scanResponse - val blockchain = Blockchain.fromNetworkId(backendId) - return when { - blockchain == null -> false - blockchain.isEvm() && !nftFeatureToggles.isNFTEVMEnabled -> false - blockchain == Blockchain.Solana && !nftFeatureToggles.isNFTSolanaEnabled -> false - else -> blockchain.canHandleNFTs() && - scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains) - } + val blockchain = Blockchain.fromNetworkId(backendId) ?: return false + + return blockchain.canHandleNFTs() && + scanResponse.card.canHandleToken(blockchain, scanResponse.cardTypesResolver, excludedBlockchains) } } \ No newline at end of file diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt index d91a537605..beeca8c0c2 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/di/NFTDataModule.kt @@ -9,7 +9,6 @@ import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.nft.NFTFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -32,7 +31,6 @@ internal object NFTDataModule { dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, userWalletsStore: UserWalletsStore, - nftFeatureToggles: NFTFeatureToggles, networkFactory: NetworkFactory, ): NFTRepository = DefaultNFTRepository( nftPersistenceStoreFactory = nftPersistenceStoreFactory, @@ -41,7 +39,6 @@ internal object NFTDataModule { dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, userWalletsStore = userWalletsStore, - nftFeatureToggles = nftFeatureToggles, networkFactory = networkFactory, resources = context.resources, ) diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index 01a57f9c01..82c038ad68 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { /** Core modules */ implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.core.deepLinks.global) implementation(projects.core.analytics) /** Common modules */ diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 4df4a7a45c..6229d12711 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -27,7 +27,6 @@ import com.tangem.domain.onramp.model.HotCryptoCurrency import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runCatching @@ -93,7 +92,7 @@ internal class DefaultHotCryptoRepository( ?: error("UserWalletId [$userWalletId] not found") HotCryptoCurrencyConverter( - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, imageHost = it.imageHost, excludedBlockchains = excludedBlockchains, ) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index 8b279d736b..64fd63a63a 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -52,7 +52,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext import org.joda.time.DateTime import timber.log.Timber @@ -303,8 +302,12 @@ internal class DefaultOnrampRepository( OnrampQuote.Data( fromAmount = fromOnrampAmount, toAmount = convertToAmount(response.toAmount, cryptoCurrency), - minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency), - maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency), + minFromAmount = response.minFromAmount?.let { + convertToAmount(it, cryptoCurrency) + }, + maxFromAmount = response.maxFromAmount?.let { + convertToAmount(it, cryptoCurrency) + }, paymentMethod = paymentMethod, provider = provider, countryCode = response.countryCode, 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 2acf7a68bf..302d0eab9c 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 @@ -10,22 +10,22 @@ 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.wallets.models.UserWallet import com.tangem.utils.converter.Converter import java.math.BigDecimal /** * Converter from [HotCryptoResponse.Token] to [HotCryptoCurrency] * - * @property scanResponse scan response + * @property userWallet scan response * @property imageHost image host * @param excludedBlockchains excluded blockchains * [REDACTED_AUTHOR] */ internal class HotCryptoCurrencyConverter( - private val scanResponse: ScanResponse, + private val userWallet: UserWallet, private val imageHost: String?, excludedBlockchains: ExcludedBlockchains, ) : Converter { @@ -81,7 +81,7 @@ internal class HotCryptoCurrencyConverter( return networkFactory.create( blockchain = blockchain, extraDerivationPath = null, - scanResponse = scanResponse, + userWallet = userWallet, ) } diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt index 5b68110f08..99ad446932 100644 --- a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -4,6 +4,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -17,17 +18,17 @@ import java.net.URLDecoder internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { - private data class QrScanningEvent(val type: SourceType, val qrCode: String) + private data class QrScanningEvent(val qrCode: RawQrResult) private val scannedEvents = MutableSharedFlow(replay = 1) - override suspend fun emitResult(type: SourceType, qrCode: String) { - scannedEvents.emit(QrScanningEvent(type, qrCode)) + override suspend fun emitResult(qrCode: RawQrResult) { + scannedEvents.emit(QrScanningEvent(qrCode)) } @OptIn(ExperimentalCoroutinesApi::class) override fun subscribeToScanningResults(type: SourceType) = scannedEvents - .filter { it.type == type } + .filter { it.qrCode.requestSource == type } .map { it.qrCode } .onEach { yield() // if we have more than one sub, we must allow them to collect emitted value diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 4c145ce44c..26e9538d23 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.getOrElse -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain @@ -15,8 +14,6 @@ import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toMigratedCoinId import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.converters.YieldBalanceListConverter import com.tangem.data.staking.converters.YieldConverter import com.tangem.data.staking.converters.action.ActionStatusConverter @@ -27,16 +24,13 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionStatusCo import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.StakingIdFactory -import com.tangem.data.staking.utils.StakingIdFactory.Companion.integrationIdMap import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction -import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter import com.tangem.datasource.local.token.converter.TokenConverter @@ -68,19 +62,16 @@ import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber +import java.math.BigDecimal import kotlin.time.Duration.Companion.seconds @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingYieldsStore: StakingYieldsStore, - private val stakingBalanceStore: StakingBalanceStore, private val stakingBalanceStoreV2: YieldsBalancesStore, - private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, private val getUserWalletUseCase: GetUserWalletUseCase, @@ -106,12 +97,8 @@ internal class DefaultStakingRepository( private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) } private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) } - override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { - rawNetworkId.plus(rawCurrencyId) - } - override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? { - return integrationIdMap.getOrDefault(getIntegrationKey(cryptoCurrencyId), null) + return stakingIdFactory.createIntegrationId(currencyId = cryptoCurrencyId) } override suspend fun fetchEnabledYields() { @@ -119,7 +106,7 @@ internal class DefaultStakingRepository( when (val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false)) { is ApiResponse.Success -> stakingYieldsStore.store( stakingTokensWithYields.data.data.filter { - it.isAvailable ?: false + it.isAvailable == true }, ) else -> { @@ -234,7 +221,7 @@ internal class DefaultStakingRepository( ) when { prefetchedYield != null && isSupportedInMobileApp -> { - send(StakingAvailability.Available(prefetchedYield.id)) + send(StakingAvailability.Available(prefetchedYield)) } prefetchedYield == null && isSupportedInMobileApp -> { send(StakingAvailability.TemporaryUnavailable) @@ -278,7 +265,7 @@ internal class DefaultStakingRepository( return when { prefetchedYield != null && isSupportedInMobileApp -> { - StakingAvailability.Available(prefetchedYield.id) + StakingAvailability.Available(prefetchedYield) } prefetchedYield == null && isSupportedInMobileApp -> { StakingAvailability.TemporaryUnavailable @@ -397,107 +384,6 @@ internal class DefaultStakingRepository( } } - override suspend fun fetchSingleYieldBalance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - refresh: Boolean, - ) = withContext(dispatchers.io) { - cacheRegistry.invokeOnExpire( - key = getYieldBalancesKey(userWalletId), - skipCache = refresh, - block = { - val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network) - - if (integrationId == null || address.isNullOrBlank()) { - cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) - Timber.w( - "IntegrationId or address is null fetching ${cryptoCurrency.name} staking balance", - ) - return@invokeOnExpire - } - - val requestBody = getBalanceRequestData(address, integrationId) - - safeApiCall( - call = { - val result = stakeKitApi.getSingleYieldBalance( - integrationId = requestBody.integrationId, - body = requestBody, - ).bind() - - stakingBalanceStore.store( - userWalletId = userWalletId, - stakingID = StakingBalanceStore.StakingID( - integrationId = requestBody.integrationId, - address = address, - ), - item = YieldBalanceWrapperDTO( - balances = result, - integrationId = requestBody.integrationId, - addresses = requestBody.addresses, - ), - ) - }, - onError = { - stakingBalanceStore.storeSingleYieldBalance( - userWalletId = userWalletId, - item = YieldBalance.Error(integrationId = requestBody.integrationId, address = address), - ) - }, - ) - }, - ) - } - - override fun getSingleYieldBalanceFlow( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): Flow = channelFlow { - launch(dispatchers.io) { - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] - ?: error("Could not get integrationId") - - stakingBalanceStore.get( - userWalletId = userWalletId, - stakingID = StakingBalanceStore.StakingID(integrationId = integrationId, address = address), - ) - .distinctUntilChanged() - .collectLatest { - if (it != null) { - send(it) - } else { - FirebaseCrashlytics.getInstance() - .log("No yield balance available for currency ${cryptoCurrency.id.value}") - send(YieldBalance.Error(integrationId, address)) - } - } - } - - withContext(dispatchers.io) { - fetchSingleYieldBalance(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) - } - }.cancellable() - - override suspend fun getSingleYieldBalanceSyncLegacy( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): YieldBalance = withContext(dispatchers.io) { - fetchSingleYieldBalance(userWalletId, cryptoCurrency) - - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - - val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] - ?: error("Could not get integrationId") - - stakingBalanceStore.getSyncOrNull( - userWalletId = userWalletId, - stakingID = StakingBalanceStore.StakingID(integrationId = integrationId, address = address), - ) - ?: YieldBalance.Error(integrationId, address) - } - override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, @@ -512,129 +398,6 @@ internal class DefaultStakingRepository( ?: YieldBalance.Error(integrationId = stakingId.integrationId, address = stakingId.address) } - @Suppress("LongMethod") - override suspend fun fetchMultiYieldBalance( - userWalletId: UserWalletId, - cryptoCurrencies: List, - refresh: Boolean, - ) = withContext(dispatchers.io) { - if (refresh) { - stakingBalanceStore.refresh( - userWalletId = userWalletId, - stakingIds = cryptoCurrencies.mapStakingId(userWalletId), - ) - } - - val yieldDTOs = withTimeoutOrNull(YIELDS_WATITING_TIMEOUT) { - runCatching { stakingYieldsStore.get().firstOrNull() }.getOrNull() - } - - if (yieldDTOs == null) { - Timber.i("No enabled yields for $userWalletId") - stakingBalanceStore.store(userWalletId, emptySet()) - - return@withContext - } - - cacheRegistry.invokeOnExpire( - key = getYieldBalancesKey(userWalletId), - skipCache = refresh, - block = { - val yields = YieldConverter.convertListIgnoreErrors( - input = yieldDTOs, - onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, - ) - - val availableCurrencies = cryptoCurrencies - .mapNotNull { currency -> - val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) - val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - - if (integrationId != null && yields.any { it.id == integrationId }) { - addresses to integrationId - } else { - null - } - } - .flatMap { (addresses, integrationId) -> - addresses.map { address -> address to integrationId } - } - .map { getBalanceRequestData(it.first.value, it.second) } - .ifEmpty { - stakingBalanceStore.store(userWalletId, emptySet()) - - cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) - - return@invokeOnExpire - } - - val yieldBalances = safeApiCall( - call = { - stakeKitApi - .getMultipleYieldBalances(availableCurrencies) - .bind() - }, - onError = { - Timber.e(it, "Unable to fetch yield balances") - cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) - emptySet() - }, - ) - - stakingBalanceStore.store(userWalletId, yieldBalances) - }, - ) - } - - private suspend fun List.mapStakingId( - userWalletId: UserWalletId, - ): List { - return this - .mapNotNull { currency -> - val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) - val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - - if (integrationId != null) { - addresses to integrationId - } else { - null - } - } - .flatMap { (addresses, integrationId) -> - addresses.map { address -> - StakingBalanceStore.StakingID( - integrationId = integrationId, - address = address.value, - ) - } - } - } - - override fun getMultiYieldBalanceUpdates( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow { - return flow { - stakingBalanceStore.get( - userWalletId = userWalletId, - stakingIds = cryptoCurrencies.mapStakingId(userWalletId), - ) - .map(YieldBalanceListConverter::convert) - .collect { emit(it) } - } - } - - override suspend fun getMultiYieldBalanceSyncLegacy( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): YieldBalanceList = withContext(dispatchers.io) { - fetchMultiYieldBalance(userWalletId, cryptoCurrencies) - - stakingBalanceStore.getSyncOrNull(userWalletId, cryptoCurrencies.mapStakingId(userWalletId)) - ?.let(YieldBalanceListConverter::convert) - ?: YieldBalanceList.Error - } - override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrencies: List, @@ -650,15 +413,24 @@ internal class DefaultStakingRepository( } override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { - return withContext(dispatchers.io) { - stakingBalanceStore.getSyncOrNull(userWalletId) - ?.let { - it.isNotEmpty() && - it.any { yieldBalance -> - (yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true - } + return withContext(dispatchers.default) { + val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false + + val hasDataYieldBalance by lazy { + balances.any { yieldBalance -> + (yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true } - ?: false + } + + balances.isNotEmpty() && hasDataYieldBalance + } + } + + override fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? { + return when { + stakingIdFactory.isPolygonIntegrationId(integrationId) && + stakingActionType == StakingActionType.CLAIM_REWARDS -> BigDecimal.ONE + else -> null } } @@ -714,7 +486,9 @@ internal class DefaultStakingRepository( } override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval { - return when (getIntegrationKey(cryptoCurrency.id)) { + val integrationId = stakingIdFactory.createIntegrationId(currencyId = cryptoCurrency.id) + + return when (integrationId) { Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId(), Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId(), -> StakingApproval.Needed(ETHEREUM_POLYGON_APPROVE_SPENDER) @@ -763,22 +537,6 @@ internal class DefaultStakingRepository( } } - private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody { - return YieldBalanceRequestBody( - addresses = Address( - address = address, - additionalAddresses = null, // todo fill additional addresses metadata if needed - explorerUrl = "", // todo fill exporer url [REDACTED_JIRA] - ), - args = YieldBalanceRequestBody.YieldBalanceRequestArgs( - validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA] - ), - integrationId = integrationId, - ) - } - - private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" - private fun getTronResource(network: Network): TronResource? { val blockchain = Blockchain.fromNetworkId(network.backendId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 67dbb87a81..45d3cac792 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -3,7 +3,6 @@ package com.tangem.data.staking.di import com.squareup.moshi.Moshi import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.DefaultStakingActionRepository import com.tangem.data.staking.DefaultStakingErrorResolver import com.tangem.data.staking.DefaultStakingRepository @@ -17,7 +16,6 @@ import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitEr import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.StakingActionsStore -import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingActionRepository import com.tangem.domain.staking.repositories.StakingErrorResolver @@ -42,9 +40,7 @@ internal object StakingDataModule { fun provideStakingRepository( stakeKitApi: StakeKitApi, stakingYieldsStore: StakingYieldsStore, - stakingBalanceStore: StakingBalanceStore, yieldsBalancesStore: YieldsBalancesStore, - cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, walletManagersFacade: WalletManagersFacade, getUserWalletUseCase: GetUserWalletUseCase, @@ -55,9 +51,7 @@ internal object StakingDataModule { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingYieldsStore = stakingYieldsStore, - stakingBalanceStore = stakingBalanceStore, stakingBalanceStoreV2 = yieldsBalancesStore, - cacheRegistry = cacheRegistry, dispatchers = dispatchers, walletManagersFacade = walletManagersFacade, getUserWalletUseCase = getUserWalletUseCase, diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index ce8be6ef59..2e048f8797 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -52,6 +52,8 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( ) : MultiYieldBalanceFetcher { override suspend fun invoke(params: MultiYieldBalanceFetcher.Params): Either { + Timber.i("Start fetching yield balances for params:\n$params") + checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { return it.left() } @@ -60,6 +62,8 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( return it.left() } + Timber.i("Staking IDs to fetch:\n${stakingIds.joinToString("\n")}") + return Either.catchOn(dispatchers.default) { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = stakingIds) @@ -126,6 +130,13 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( val availableStakingIds = groupedStakingIds[true].orEmpty() val unavailableStakingIds = groupedStakingIds[false].orEmpty() + Timber.i( + """ + Available staking IDs: ${availableStakingIds.joinToString()} + Unavailable staking IDs: ${unavailableStakingIds.joinToString()} + """.trimIndent(), + ) + if (unavailableStakingIds.isNotEmpty()) { yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = unavailableStakingIds.toSet()) } @@ -138,7 +149,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( – stakingIds: ${stakingIds.joinToString()} """.trimIndent(), ) - Timber.d(exception) + Timber.i(exception) throw exception } } @@ -174,6 +185,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( .toSet() } + Timber.i("Successfully fetched yield balances for $userWalletId:\n${yieldBalances.joinToString("\n")}") yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) if (!allResponsesReceived(requests, yieldBalances)) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt index 887db9af5f..bd38f407a4 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -1,5 +1,7 @@ package com.tangem.data.staking.single +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -7,6 +9,7 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.indexOfFirstOrNull import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -14,6 +17,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.mapNotNull +import timber.log.Timber /** * Default implementation of [SingleYieldBalanceProducer] @@ -29,6 +33,7 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( @Assisted private val params: SingleYieldBalanceProducer.Params, private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, private val stakingIdFactory: StakingIdFactory, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, ) : SingleYieldBalanceProducer { @@ -42,14 +47,55 @@ internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( private var stakingId: StakingID? = null override fun produce(): Flow { + Timber.i("Producing yield balance for params:\n$params") + return multiYieldBalanceSupplier( params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), ) .mapNotNull { balances -> - val currentStakingId = getStakingId() ?: return@mapNotNull YieldBalance.Unsupported + val currentStakingId = getStakingId() - balances.firstOrNull { it.getStakingId() == currentStakingId } - ?: YieldBalance.Unsupported + if (currentStakingId == null) { + Timber.i("Staking ID is null for params: $params") + return@mapNotNull YieldBalance.Unsupported + } + + val currentBalances = balances.filter { it.getStakingId() == currentStakingId } + + if (currentBalances.size > 1) { + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent( + exception = IllegalStateException("Multiple balances found for staking ID"), + params = mapOf( + "stakingId" to currentStakingId.toString(), + "balances" to currentBalances.joinToString(",") { it.toString() }, + ), + ), + ) + + Timber.e( + "Multiple balances found for staking ID $currentStakingId:\n%s", + currentBalances.joinToString("\n"), + ) + + val dataIndex = currentBalances.indexOfFirstOrNull { it is YieldBalance.Data } + + if (dataIndex != null) { + currentBalances[dataIndex] + } else { + currentBalances.first() + } + } else { + val balance = currentBalances.firstOrNull() + + if (balance != null) { + Timber.i("Yield balance found for $currentStakingId:\n$balance") + balance + } else { + Timber.i("No yield balance found for $currentStakingId:\n${YieldBalance.Unsupported}") + YieldBalance.Unsupported + } + } } .distinctUntilChanged() .flowOn(dispatchers.default) diff --git a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt index 2f28162955..808ba7a7ae 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/store/DefaultYieldsBalancesStore.kt @@ -109,8 +109,15 @@ internal class DefaultYieldsBalancesStore( private suspend fun storeInPersistence(userWalletId: UserWalletId, values: Set) { persistenceStore.updateData { current -> current.toMutableMap().apply { - this[userWalletId.stringValue] = current[userWalletId.stringValue] - ?.addOrReplace(items = values) { old, new -> old.getStakingId() == new.getStakingId() } + this[userWalletId.stringValue] = this[userWalletId.stringValue] + ?.addOrReplace(items = values) { old, new -> + val oldId = old.getStakingId() + val newId = new.getStakingId() + + if (oldId == null || newId == null) return@addOrReplace false + + oldId == newId + } ?: values } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt index d1b760b601..4633d2572a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt @@ -35,6 +35,8 @@ internal class StakingIdFactory @Inject constructor( return integrationIdMap[integrationKey] } + fun isPolygonIntegrationId(integrationId: String): Boolean = integrationId == ETHEREUM_POLYGON_INTEGRATION_ID + @Suppress("UnusedPrivateMember", "unused") companion object { @@ -53,7 +55,7 @@ internal class StakingIdFactory @Inject constructor( private const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking" // uncomment items as implementation is ready - val integrationIdMap = mapOf( + private val integrationIdMap = mapOf( Blockchain.TON.toDefaultKey() to TON_INTEGRATION_ID, Blockchain.Solana.toDefaultKey() to SOLANA_INTEGRATION_ID, Blockchain.Cosmos.toDefaultKey() to COSMOS_INTEGRATION_ID, diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt index b05ad2e885..c594ea3515 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.getEmittedValues +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.data.staking.toDomain import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.domain.staking.model.StakingID @@ -32,12 +33,14 @@ internal class DefaultSingleYieldBalanceProducerTest { private val multiNetworkStatusSupplier = mockk() private val stakingIdFactory = mockk() + private val analyticsExceptionHandler = mockk(relaxUnitFun = true) private val dispatchers = TestingCoroutineDispatcherProvider() private val producer = DefaultSingleYieldBalanceProducer( params = params, stakingIdFactory = stakingIdFactory, multiYieldBalanceSupplier = multiNetworkStatusSupplier, + analyticsExceptionHandler = analyticsExceptionHandler, dispatchers = dispatchers, ) 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 index 8827a8bc7c..b81d5b6231 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapErrorResolver.kt @@ -13,6 +13,7 @@ internal class DefaultSwapErrorResolver( is ApiResponseError.HttpException -> { expressErrorConverter.convert(throwable.errorBody.orEmpty()) } + is ExpressError -> throwable else -> ExpressError.UnknownError } } 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 index 9adca6af89..7b2442b6ac 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -1,19 +1,28 @@ package com.tangem.data.swap +import com.squareup.moshi.Moshi import com.tangem.data.common.api.safeApiCall +import com.tangem.data.swap.converter.SwapDataConverter +import com.tangem.data.swap.converter.SwapStatusConverter 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.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails +import com.tangem.datasource.api.express.models.response.TxDetails +import com.tangem.datasource.crypto.DataSignatureVerifier +import com.tangem.datasource.di.NetworkMoshi 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.ExpressError import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.SwapRepositoryV2 -import com.tangem.domain.swap.models.SwapPairModel -import com.tangem.domain.swap.models.SwapQuoteModel +import com.tangem.domain.swap.models.* import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.wallets.models.UserWallet @@ -24,19 +33,26 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext import timber.log.Timber +import java.io.IOException import java.math.BigDecimal +import java.util.UUID import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") 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, + private val dataSignatureVerifier: DataSignatureVerifier, + @NetworkMoshi moshi: Moshi, ) : SwapRepositoryV2 { + private val swapDataConverter = SwapDataConverter() private val tokenInfoConverter = TokenInfoConverter() + private val exchangeStatusConverter = SwapStatusConverter() + private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) override suspend fun getPairs( userWallet: UserWallet, @@ -153,6 +169,115 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ) } + override suspend fun getSwapData( + userWallet: UserWallet, + fromCryptoCurrencyStatus: CryptoCurrencyStatus, + toCryptoCurrencyStatus: CryptoCurrencyStatus, + fromAmount: String, + toAddress: String?, + expressProvider: ExpressProvider, + rateType: ExpressRateType, + ): SwapDataModel = withContext(coroutineDispatcher.io) { + val requestId = UUID.randomUUID().toString() + val fromCryptoCurrency = fromCryptoCurrencyStatus.currency + val toCryptoCurrency = toCryptoCurrencyStatus.currency + + val refundData = when (expressProvider.type) { + ExpressProviderType.CEX, + ExpressProviderType.DEX_BRIDGE, + ExpressProviderType.DEX, + -> SwapRefundData( + refundAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value, + refundExtraId = null, // currently always null + ) + else -> null + } + + val response = tangemExpressApi.getExchangeData( + fromContractAddress = fromCryptoCurrency.getContractAddress(), + toContractAddress = toCryptoCurrency.getContractAddress(), + fromNetwork = fromCryptoCurrency.network.backendId, + toNetwork = toCryptoCurrency.network.backendId, + fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + toAddress = toAddress ?: toCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + fromDecimals = fromCryptoCurrency.decimals, + toDecimals = toCryptoCurrency.decimals, + fromAmount = fromAmount, + providerId = expressProvider.name, + rateType = rateType.name.lowercase(), + requestId = requestId, + refundAddress = refundData?.refundAddress, + refundExtraId = refundData?.refundExtraId, + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).getOrThrow() + + if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { + val txDetails = parseTxDetails(response.txDetailsJson) + ?: throw ExpressError.UnknownError + if (txDetails.requestId != requestId) { + throw ExpressError.InvalidRequestIdError() + } + if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) { + throw ExpressError.InvalidPayoutAddressError() + } + swapDataConverter.convert( + ExchangeDataResponseWithTxDetails( + dataResponse = response, + txDetails = txDetails, + ), + ) + } else { + throw ExpressError.InvalidSignatureError() + } + } + + override suspend fun swapTransactionSent( + userWallet: UserWallet, + fromCryptoCurrencyStatus: CryptoCurrencyStatus, + toAddress: String, + txId: String, + txHash: String, + txExtraId: String?, + ) { + withContext(coroutineDispatcher.io) { + tangemExpressApi.exchangeSent( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = ExchangeSentRequestBody( + txId = txId, + fromNetwork = fromCryptoCurrencyStatus.currency.network.backendId, + fromAddress = fromCryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), + payinAddress = toAddress, + payinExtraId = txExtraId, + txHash = txHash, + ), + ).getOrThrow() + } + } + + override suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): SwapStatusModel = + withContext(coroutineDispatcher.io) { + exchangeStatusConverter.convert( + tangemExpressApi + .getExchangeStatus( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + txId = txId, + ) + .getOrThrow(), + ) + } + private suspend fun CoroutineScope.getPairsInternal( userWallet: UserWallet, initialCurrency: CryptoCurrency, @@ -200,7 +325,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( }, ) - private suspend fun CoroutineScope.createPairModelOnly( + private suspend fun createPairModelOnly( currencyFrom: CryptoCurrency?, currencyTo: CryptoCurrency?, userWalletId: UserWalletId, @@ -229,6 +354,15 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } } + private fun parseTxDetails(txDetailsJson: String): TxDetails? { + return try { + txDetailsMoshiAdapter.fromJson(txDetailsJson) + } catch (e: IOException) { + Timber.e(e, "error parsing txDetailsJson") + null + } + } + private fun CryptoCurrency.getContractAddress(): String { return when (this) { is CryptoCurrency.Token -> this.contractAddress diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index 2ceb2cab7e..c540a958d5 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -21,7 +21,6 @@ import com.tangem.domain.swap.models.SwapTransactionListModel import com.tangem.domain.swap.models.SwapTransactionModel import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow @@ -119,7 +118,7 @@ internal class DefaultSwapTransactionRepository( currencyTxs?.mapNotNull { listConverter.convertBack( value = it, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, txStatuses = txStatuses, ) } diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt new file mode 100644 index 0000000000..34f26aed27 --- /dev/null +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/SwapDataConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.data.swap.converter + +import com.tangem.datasource.api.express.models.response.ExchangeDataResponse +import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails +import com.tangem.datasource.api.express.models.response.TxDetails +import com.tangem.datasource.api.express.models.response.TxType +import com.tangem.domain.swap.models.SwapDataModel +import com.tangem.domain.swap.models.SwapDataTransactionModel +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SwapDataConverter : Converter { + + override fun convert(value: ExchangeDataResponseWithTxDetails): SwapDataModel { + val data = value.dataResponse + return SwapDataModel( + toTokenAmount = requireNotNull(data.toAmount.toBigDecimalOrNull()?.movePointLeft(data.toDecimals)), + transaction = convertTransaction(value.txDetails, data), + ) + } + + private fun convertTransaction( + transactionDto: TxDetails, + dataResponse: ExchangeDataResponse, + ): SwapDataTransactionModel { + val fromAmount = requireNotNull( + dataResponse.fromAmount.toBigDecimalOrNull()?.movePointLeft(dataResponse.fromDecimals), + ) + val toAmount = requireNotNull( + dataResponse.toAmount.toBigDecimalOrNull()?.movePointLeft(dataResponse.toDecimals), + ) + + return if (transactionDto.txType == TxType.SWAP) { + val otherNativeFeeWei = transactionDto.otherNativeFee?.let { + if (it == "0") { + BigDecimal.ZERO + } else { + requireNotNull(it.toBigDecimalOrNull()) { "wrong amount format, use only digits" } + } + } + SwapDataTransactionModel.DEX( + fromAmount = fromAmount, + toAmount = toAmount, + txValue = transactionDto.txValue, + txId = dataResponse.txId, + txTo = transactionDto.txTo, + txFrom = requireNotNull(transactionDto.txFrom), + txData = requireNotNull(transactionDto.txData), + txExtraId = transactionDto.txExtraId, + otherNativeFeeWei = otherNativeFeeWei, + gas = transactionDto.gas?.toBigIntegerOrNull() ?: error("gas is empty"), + ) + } else { + SwapDataTransactionModel.CEX( + fromAmount = fromAmount, + toAmount = toAmount, + txValue = transactionDto.txValue, + txId = dataResponse.txId, + txTo = transactionDto.txTo, + externalTxId = requireNotNull(transactionDto.externalTxId), + externalTxUrl = requireNotNull(transactionDto.externalTxUrl), + txExtraIdName = transactionDto.txExtraIdName, + txExtraId = transactionDto.txExtraId, + ) + } + } +} \ No newline at end of file diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt index 78c257249a..edd79d5616 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionConverter.kt @@ -3,8 +3,8 @@ package com.tangem.data.swap.converter.transaction import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.swap.models.SwapStatusDTO import com.tangem.data.swap.models.SwapTransactionDTO -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.swap.models.SwapTransactionModel +import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.converter.TwoWayConverter internal class SavedSwapTransactionConverter( @@ -35,14 +35,14 @@ internal class SavedSwapTransactionConverter( fun convertBack( value: SwapTransactionDTO, - scanResponse: ScanResponse, + userWallet: UserWallet, txStatuses: Map, ): SwapTransactionModel { val status = txStatuses[value.txId] val refundCurrency = status?.refundTokensResponse?.let { id -> responseCryptoCurrenciesFactory.createCurrency( responseToken = id, - scanResponse = scanResponse, + userWallet = userWallet, ) } val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt index 393c3c421a..4b38d62d51 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -6,8 +6,8 @@ import com.tangem.data.swap.models.SwapStatusDTO import com.tangem.data.swap.models.SwapTransactionDTO import com.tangem.data.swap.models.SwapTransactionListDTO import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.swap.models.SwapTransactionListModel +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.converter.Converter @@ -35,7 +35,7 @@ internal class SavedSwapTransactionListConverter( fun convertBack( value: SwapTransactionListDTO, - scanResponse: ScanResponse, + userWallet: UserWallet, txStatuses: Map, ): SwapTransactionListModel? { val fromToken = value.fromTokensResponse @@ -45,16 +45,16 @@ internal class SavedSwapTransactionListConverter( } else { val fromCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency( responseToken = fromToken, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null val toCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency( responseToken = toToken, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null return SwapTransactionListModel( transactions = value.transactions.map { tx -> - savedSwapTransactionConverter.convertBack(tx, scanResponse, txStatuses) + savedSwapTransactionConverter.convertBack(tx, userWallet, txStatuses) }, userWalletId = value.userWalletId, fromCryptoCurrencyId = value.fromCryptoCurrencyId, 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 index 486080c4e4..635a89c736 100644 --- 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 @@ -8,6 +8,7 @@ import com.tangem.data.swap.DefaultSwapRepositoryV2 import com.tangem.data.swap.DefaultSwapTransactionRepository import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse +import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.express.ExpressRepository @@ -43,6 +44,8 @@ internal object SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, appPreferencesStore: AppPreferencesStore, currencyStatusOperations: BaseCurrencyStatusOperations, + dataSignatureVerifier: DataSignatureVerifier, + @NetworkMoshi moshi: Moshi, ): SwapRepositoryV2 { return DefaultSwapRepositoryV2( tangemExpressApi = tangemExpressApi, @@ -50,6 +53,8 @@ internal object SwapDataModule { coroutineDispatcher = coroutineDispatcher, appPreferencesStore = appPreferencesStore, currencyStatusOperations = currencyStatusOperations, + dataSignatureVerifier = dataSignatureVerifier, + moshi = moshi, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt index fcb4cdd145..35d59aaf19 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -16,7 +16,6 @@ import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params import com.tangem.domain.wallets.models.UserWallet @@ -60,7 +59,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher( if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) { - createDefaultUserTokensResponse(scanResponse = userWallet.scanResponse) + createDefaultUserTokensResponse(userWallet = userWallet) } else { safeApiCall( call = { @@ -99,7 +98,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher( val userWalletId = userWallet.walletId val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - ?: createDefaultUserTokensResponse(scanResponse = userWallet.requireColdWallet().scanResponse) + ?: createDefaultUserTokensResponse(userWallet = userWallet) if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) { Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId") @@ -121,9 +120,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher( expressServiceLoader.update(userWallet = userWallet, userTokens = tokens) } - private fun createDefaultUserTokensResponse(scanResponse: ScanResponse): UserTokensResponse { + private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { return userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse), + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet), isGroupedByNetwork = false, isSortedByBalance = false, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt index 7f655d07f8..32cc3a4f0f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -48,7 +48,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr responseCryptoCurrenciesFactory.createCurrencies( response = response, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ).toSet() } .onEmpty { emit(fallback) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 4f3a819ef0..6d25a6c018 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison import com.tangem.blockchainsdk.utils.* import com.tangem.data.common.api.safeApiCall @@ -97,31 +98,35 @@ internal class DefaultCurrenciesRepository( } } - override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) { - withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, - ) + override suspend fun addCurrencies( + userWalletId: UserWalletId, + currencies: List, + ): List = withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = getSavedUserTokensResponseSync(key = userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, + ) - val currenciesToAdd = populateCurrenciesWithMissedCoins( - currencies = currencies, - ).let { - filterAlreadyAddedCurrencies(savedCurrencies.tokens, it) - } - val updatedResponse = savedCurrencies.copy( - tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken), - ) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = updatedResponse, - ) + val currenciesToAdd = filterAlreadyAddedCurrencies( + savedCurrencies = savedCurrencies.tokens, + currenciesToAdd = populateCurrenciesWithMissedCoins(currencies = currencies), + ) - fetchExpressAssetsByNetworkIds( - userWallet = userWalletsStore.getSyncStrict(key = userWalletId), - userTokens = updatedResponse, - ) - } + val updatedResponse = savedCurrencies.copy( + tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken), + ) + + userTokensSaver.storeAndPush( + userWalletId = userWalletId, + response = updatedResponse, + ) + + fetchExpressAssetsByNetworkIds( + userWallet = userWalletsStore.getSyncStrict(key = userWalletId), + userTokens = updatedResponse, + ) + + currenciesToAdd } private fun filterAlreadyAddedCurrencies( @@ -179,7 +184,8 @@ internal class DefaultCurrenciesRepository( ) val token = userTokensResponseFactory.createResponseToken(currency) - val updatedResponse = savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }) + val updatedResponse = + savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }) userTokensSaver.storeAndPush( userWalletId = userWalletId, response = updatedResponse, @@ -224,10 +230,11 @@ internal class DefaultCurrenciesRepository( ): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = userWalletsStore.getSyncStrict(userWalletId) + userWallet.requireColdWallet() ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) val currency = cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard( - userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) fetchExpressAssetsByNetworkIds( @@ -249,11 +256,12 @@ internal class DefaultCurrenciesRepository( val scanResponse = userWallet.requireColdWallet().scanResponse val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(scanResponse = scanResponse) + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet) } else { - cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(scanResponse = scanResponse).run { - listOf(this) - } + cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet) + .run { + listOf(this) + } } fetchExpressAssetsByNetworkIds( @@ -272,10 +280,11 @@ internal class DefaultCurrenciesRepository( ): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = userWalletsStore.getSyncStrict(userWalletId) + userWallet.requireColdWallet() ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) val currency = cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken( - scanResponse = userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) .find { it.id == id } requireNotNull(currency) { "Unable to find currency with provided ID: $id" } @@ -315,7 +324,10 @@ internal class DefaultCurrenciesRepository( }, ) - responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse) + responseCryptoCurrenciesFactory.createCurrencies( + storedTokens, + userWallet = userWallet, + ) } override suspend fun getMultiCurrencyWalletCachedCurrenciesSync(userWalletId: UserWalletId) = @@ -330,7 +342,10 @@ internal class DefaultCurrenciesRepository( }, ) - responseCryptoCurrenciesFactory.createCurrencies(storedTokens, userWallet.requireColdWallet().scanResponse) + responseCryptoCurrenciesFactory.createCurrencies( + storedTokens, + userWallet = userWallet, + ) } override suspend fun getMultiCurrencyWalletCurrency( @@ -355,7 +370,7 @@ internal class DefaultCurrenciesRepository( responseCryptoCurrenciesFactory.createCurrency( currencyId = id, response = response, - scanResponse = userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) } @@ -389,7 +404,7 @@ internal class DefaultCurrenciesRepository( val coin = responseCryptoCurrenciesFactory.createCurrency( responseToken = storedCoin, - scanResponse = userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) coin as? CryptoCurrency.Coin ?: error("Unable to create currency") @@ -428,20 +443,30 @@ internal class DefaultCurrenciesRepository( } } - override fun isSendBlockedByPendingTransactions( + override suspend fun isSendBlockedByPendingTransactions( + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - coinStatus: CryptoCurrencyStatus?, ): Boolean { val blockchain = cryptoCurrencyStatus.currency.network.toBlockchain() - val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet + val isBitcoinBlockchain = + blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet return when { cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain -> { - val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } + val outgoingTransactions = + cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } outgoingTransactions.isNotEmpty() } + blockchain.isEvm() -> false blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet -> false - else -> coinStatus?.value?.hasCurrentNetworkTransactions == true + else -> { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + ) ?: return false + + walletManager.wallet.recentTransactions.any { it.status == TransactionStatus.Unconfirmed } + } } } @@ -469,6 +494,7 @@ internal class DefaultCurrenciesRepository( balance = balance, ) } + is FeePaidSdkCurrency.FeeResource -> FeePaidCurrency.FeeResource(currency = feePaidCurrency.currency) } } @@ -496,7 +522,8 @@ internal class DefaultCurrenciesRepository( .coins .firstOrNull() ?: error("Token not found") - val network = foundToken.networks.firstOrNull { it.networkId == networkId } ?: error("Network not found") + val network = foundToken.networks.firstOrNull { it.networkId == networkId } + ?: error("Network not found") CryptoCurrencyFactory.Token( symbol = foundToken.symbol, name = foundToken.name, @@ -509,7 +536,7 @@ internal class DefaultCurrenciesRepository( token = token, networkId = networkId, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) ?: error("Unable to create token") } @@ -537,7 +564,6 @@ internal class DefaultCurrenciesRepository( userWallet: UserWallet, currencyRawId: CryptoCurrency.RawID, ): Flow> { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] return when { userWallet.isMultiCurrency -> { getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> @@ -547,23 +573,25 @@ internal class DefaultCurrenciesRepository( responseCryptoCurrenciesFactory.createCurrencies( response = storedTokens.copy(tokens = filterResponse), - scanResponse = userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) } } - else -> { - val currencies = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId) - } else { - val currency = - getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId) - if (currency.id.rawCurrencyId == currencyRawId) { - listOf(currency) + else -> { + val currencies = + if (userWallet.requireColdWallet().scanResponse.cardTypesResolver.isSingleWalletWithToken()) { + getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId) } else { - emptyList() + val currency = + getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId) + + if (currency.id.rawCurrencyId == currencyRawId) { + listOf(currency) + } else { + emptyList() + } } - } flow { emit(currencies) } @@ -573,7 +601,7 @@ internal class DefaultCurrenciesRepository( override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { val blockchain = Blockchain.fromNetworkId(network.backendId) - return blockchain?.isNetworkFeeZero() ?: false + return blockchain?.isNetworkFeeZero() == true } override suspend fun syncTokens(userWalletId: UserWalletId) { @@ -590,15 +618,15 @@ internal class DefaultCurrenciesRepository( ) } - override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { - return userWalletsStore.getSyncStrict(userWalletId).requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY] + override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? { + return (userWalletsStore.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> responseCryptoCurrenciesFactory.createCurrencies( response = storedTokens, - scanResponse = userWallet.requireColdWallet().scanResponse, + userWallet = userWallet, ) } } @@ -688,7 +716,10 @@ internal class DefaultCurrenciesRepository( ?: createDefaultUserTokensResponse(userWallet = userWallet) if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { - Timber.w(e, "Requested currencies could not be found in the remote store for: $userWalletId") + Timber.w( + e, + "Requested currencies could not be found in the remote store for: $userWalletId", + ) userTokensSaver.push(userWalletId, response) } else { @@ -700,8 +731,8 @@ internal class DefaultCurrenciesRepository( private fun createDefaultUserTokensResponse(userWallet: UserWallet) = userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard( - userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( + userWallet = userWallet, ), isGroupedByNetwork = false, isSortedByBalance = false, @@ -720,9 +751,11 @@ internal class DefaultCurrenciesRepository( !userWallet.isMultiCurrency && isMultiCurrencyWalletExpected -> { "Multi currency wallet expected, but single currency wallet was found: $userWalletId" } + userWallet.isMultiCurrency && !isMultiCurrencyWalletExpected -> { "Single currency wallet expected, but multi currency wallet was found: $userWalletId" } + else -> null } diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt index 6a794d8e44..5d5bb9d5bd 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt @@ -125,7 +125,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null every { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) } returns defaultCoins coEvery { customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -141,7 +141,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { coVerifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = mockUserWallet.scanResponse) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) expressServiceLoader.update(userWallet = mockUserWallet, userTokens = userTokensResponse.toLeastTokens()) @@ -187,7 +187,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { } coVerify(inverse = true) { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) } } @@ -229,7 +229,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { coVerify(inverse = true) { userTokensResponseStore.getSyncOrNull(userWalletId = any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) } } @@ -267,7 +267,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) } returns defaultCoins coEvery { customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -335,7 +335,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { coVerify(inverse = true) { userTokensSaver.push(userWalletId = any(), response = any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) } } @@ -377,7 +377,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) } returns defaultCoins coEvery { customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -394,7 +394,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { userWalletsStore.getSyncStrict(key = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(mockUserWallet.scanResponse) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) @@ -447,7 +447,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { } coVerify(inverse = true) { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyCard(scanResponse = any()) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) } } diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt index a453c8e2b7..07a7ff54eb 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesProducerTest.kt @@ -72,7 +72,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { } verify(inverse = true) { - responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any()) + responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any()) } } @@ -114,14 +114,14 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { every { responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns cryptoCurrencies.toList() every { responseCryptoCurrenciesFactory.createCurrencies( response = updatedUserTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns updatedCryptoCurrencies.toList() @@ -143,7 +143,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } @@ -161,7 +161,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { verifyOrder { responseCryptoCurrenciesFactory.createCurrencies( response = updatedUserTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } } @@ -185,7 +185,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { every { responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns cryptoCurrencies.toList() @@ -207,7 +207,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } @@ -251,7 +251,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { every { responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } returns cryptoCurrencies.toList() @@ -282,7 +282,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { verifyOrder { responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, - scanResponse = userWallet.scanResponse, + userWallet = userWallet, ) } } @@ -307,7 +307,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { } verify(inverse = true) { - responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any()) + responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any()) } } @@ -335,7 +335,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { verify(inverse = true) { userTokensResponseStore.get(any()) - responseCryptoCurrenciesFactory.createCurrencies(response = any(), scanResponse = any()) + responseCryptoCurrenciesFactory.createCurrencies(response = any(), userWallet = any()) } } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 30d55b6061..9ae6b3d6b1 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -36,7 +36,7 @@ import java.math.BigInteger internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, private val walletManagersStore: WalletManagersStore, - private val coroutineDispatcherProvider: CoroutineDispatcherProvider, + private val dispatchers: CoroutineDispatcherProvider, ) : TransactionRepository { override suspend fun createTransaction( @@ -47,7 +47,7 @@ internal class DefaultTransactionRepository( userWalletId: UserWalletId, network: Network, txExtras: TransactionExtras?, - ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled = withContext(dispatchers.io) { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -84,7 +84,7 @@ internal class DefaultTransactionRepository( userWalletId: UserWalletId, network: Network, nonce: BigInteger?, - ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled = withContext(dispatchers.io) { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -138,7 +138,7 @@ internal class DefaultTransactionRepository( spenderAddress: String, userWalletId: UserWalletId, network: Network, - ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled = withContext(dispatchers.io) { val blockchain = network.toBlockchain() val extras = createTransactionDataExtras( @@ -171,7 +171,7 @@ internal class DefaultTransactionRepository( destinationAddress: String, userWalletId: UserWalletId, network: Network, - ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled = withContext(dispatchers.io) { val blockchain = network.toBlockchain() // For now transfer one nft asset at a time @@ -229,7 +229,7 @@ internal class DefaultTransactionRepository( destination: String, userWalletId: UserWalletId, network: Network, - ): Result = withContext(coroutineDispatcherProvider.io) { + ): Result = withContext(dispatchers.io) { val blockchain = network.toBlockchain() val walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, @@ -260,7 +260,7 @@ internal class DefaultTransactionRepository( signer: TransactionSigner, userWalletId: UserWalletId, network: Network, - ) = withContext(coroutineDispatcherProvider.io) { + ) = withContext(dispatchers.io) { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -276,7 +276,7 @@ internal class DefaultTransactionRepository( userWalletId: UserWalletId, network: Network, sendMode: TransactionSender.MultipleTransactionSendMode, - ) = withContext(coroutineDispatcherProvider.io) { + ) = withContext(dispatchers.io) { val blockchain = network.toBlockchain() val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -366,13 +366,9 @@ internal class DefaultTransactionRepository( signer: TransactionSigner, userWalletId: UserWalletId, network: Network, - ): Result = withContext(coroutineDispatcherProvider.io) { + ) = withContext(dispatchers.io) { val preparer = getPreparer(network, userWalletId) - - when (val prepareForSend = preparer.prepareForSend(transactionData, signer)) { - is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error) - is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data) - } + preparer.prepareForSend(transactionData, signer) } override suspend fun prepareForSendMultiple( @@ -380,13 +376,9 @@ internal class DefaultTransactionRepository( signer: TransactionSigner, userWalletId: UserWalletId, network: Network, - ): Result> = withContext(coroutineDispatcherProvider.io) { + ) = withContext(dispatchers.io) { val preparer = getPreparer(network, userWalletId) - - when (val prepareForSend = preparer.prepareForSendMultiple(transactionData, signer)) { - is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error) - is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data) - } + preparer.prepareForSendMultiple(transactionData, signer) } private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer { diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index d5fffe95ed..6184d3fa48 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -24,12 +24,12 @@ internal object TransactionDataModule { fun providesTransactionRepository( walletManagersFacade: WalletManagersFacade, walletManagersStore: WalletManagersStore, - coroutineDispatcherProvider: CoroutineDispatcherProvider, + dispatchers: CoroutineDispatcherProvider, ): TransactionRepository { return DefaultTransactionRepository( walletManagersFacade = walletManagersFacade, walletManagersStore = walletManagersStore, - coroutineDispatcherProvider = coroutineDispatcherProvider, + dispatchers = dispatchers, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index db2117b06a..30457ea047 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -166,6 +166,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val rsaPublicKey = visaLibLoader.getOrCreateConfig().rsaPublicKey return when (env) { ApiEnvironment.DEV, + ApiEnvironment.DEV_2, ApiEnvironment.STAGE, ApiEnvironment.MOCK, -> rsaPublicKey.dev diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index dfdfe71d2c..8b9e7e791c 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -51,6 +51,7 @@ dependencies { /* Other */ implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + implementation(deps.jodatime) implementation(projects.domain.blockaid) implementation(projects.domain.blockaid.models) implementation(projects.libs.crypto) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 8e39df4b21..40593c27e5 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -16,6 +16,7 @@ import com.tangem.data.walletconnect.respond.DefaultWcRespondService import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore @@ -72,7 +73,9 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun defaultWcPairUseCase(): WcPairService = DefaultWcPairService() + fun defaultWcPairUseCase(sessionsManager: DefaultWcSessionsManager): WcPairService = DefaultWcPairService( + sessionsManager, + ) @Provides @Singleton @@ -89,7 +92,8 @@ internal object WalletConnectDataModule { dispatchers: CoroutineDispatcherProvider, legacyStore: WalletConnectSessionsRepository, getWallets: GetWalletsUseCase, - associateNetworks: AssociateNetworksDelegate, + wcNetworksConverter: WcNetworksConverter, + analytics: AnalyticsEventHandler, ): DefaultWcSessionsManager { val scope = CoroutineScope(SupervisorJob() + dispatchers.io) return DefaultWcSessionsManager( @@ -97,7 +101,8 @@ internal object WalletConnectDataModule { dispatchers = dispatchers, legacyStore = legacyStore, getWallets = getWallets, - associateNetworks = associateNetworks, + wcNetworksConverter = wcNetworksConverter, + analytics = analytics, scope = scope, ) } @@ -129,11 +134,11 @@ internal object WalletConnectDataModule { @SdkMoshi moshi: Moshi, sessionsManager: WcSessionsManager, factories: WcEthNetwork.Factories, - namespaceConverter: WcEthNetwork.NamespaceConverter, walletManagersFacade: WalletManagersFacade, + wcNetworksConverter: WcNetworksConverter, ): WcEthNetwork = WcEthNetwork( moshi = moshi, - namespaceConverter = namespaceConverter, + networksConverter = wcNetworksConverter, sessionsManager = sessionsManager, factories = factories, walletManagersFacade = walletManagersFacade, @@ -143,7 +148,7 @@ internal object WalletConnectDataModule { @Singleton fun wcSolanaNetwork( @SdkMoshi moshi: Moshi, - namespaceConverter: WcSolanaNetwork.NamespaceConverter, + wcNetworksConverter: WcNetworksConverter, sessionsManager: WcSessionsManager, factories: WcSolanaNetwork.Factories, walletManagersFacade: WalletManagersFacade, @@ -151,7 +156,7 @@ internal object WalletConnectDataModule { moshi = moshi, sessionsManager = sessionsManager, factories = factories, - namespaceConverter = namespaceConverter, + networksConverter = wcNetworksConverter, walletManagersFacade = walletManagersFacade, ) @@ -160,9 +165,27 @@ internal object WalletConnectDataModule { fun caipNamespaceDelegate( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, + wcNetworksConverter: WcNetworksConverter, ): CaipNamespaceDelegate = CaipNamespaceDelegate( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, + wcNetworksConverter = wcNetworksConverter, + ) + + @Provides + @Singleton + fun wcNetworksConverter( + namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + tokensFeatureToggles: TokensFeatureToggles, + ): WcNetworksConverter = WcNetworksConverter( + namespaceConverters = namespaceConverters, + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + tokensFeatureToggles = tokensFeatureToggles, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index d2558e2958..ed0e0bd7d9 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -5,6 +5,7 @@ import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase @@ -27,7 +28,7 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( override val walletAddress: String get() = context.accountAddress - override suspend fun approve(): Either { + override suspend fun approve(): Either { return respondService.respond(rawSdkRequest, "") } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt index cdff1f2b9e..36d3087190 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthMessageSignUseCase.kt @@ -19,6 +19,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.SignUseCase +import com.tangem.domain.walletconnect.error.parseTangemSdkError import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcSignState @@ -59,7 +60,7 @@ internal class WcEthMessageSignUseCase @AssistedInject constructor( ?: return val signedHash = signUseCase(hashToSign, userWallet, network) - .onLeft { emit(state.toResult(it.left())) } + .onLeft { emit(state.toResult(parseTangemSdkError(it).left())) } .getOrNull() ?: return val respond = prepareToSendMessageData(signedHash, hashToSign, walletManager) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index 6bcb815d18..66cbe92413 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -10,6 +10,7 @@ import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -23,7 +24,7 @@ internal class WcEthNetwork( private val moshi: Moshi, private val sessionsManager: WcSessionsManager, private val factories: Factories, - private val namespaceConverter: NamespaceConverter, + private val networksConverter: WcNetworksConverter, private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { @@ -37,20 +38,34 @@ internal class WcEthNetwork( override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { val name = toWcMethodName(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null - val method: WcEthMethod = name.toMethod(request, session.wallet) ?: return null - val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null - val walletManagerAddress = walletManagersFacade.getDefaultAddress(session.wallet.walletId, network).orEmpty() + val wallet = session.wallet + val chainId = request.chainId.orEmpty() + val method: WcEthMethod = name.toMethod(request, wallet) ?: return null + suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) + val accountAddress = when (method) { is WcEthMethod.MessageSign -> method.account is WcEthMethod.SendTransaction -> method.transaction.from is WcEthMethod.SignTransaction -> method.transaction.from is WcEthMethod.SignTypedData -> method.account - is WcEthMethod.AddEthereumChain -> walletManagerAddress + is WcEthMethod.AddEthereumChain -> + anyExistNetwork() + ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } + .orEmpty() } + val walletNetwork = when (method) { + is WcEthMethod.SignTypedData, + is WcEthMethod.MessageSign, + is WcEthMethod.SendTransaction, + is WcEthMethod.SignTransaction, + -> networksConverter.findWalletNetworkForRequest(request, session, accountAddress) + is WcEthMethod.AddEthereumChain -> anyExistNetwork() + } ?: return null + val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, - network = network, + network = walletNetwork, accountAddress = accountAddress, ) return when (method) { @@ -62,7 +77,7 @@ internal class WcEthNetwork( } } - private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest, wallet: UserWallet): WcEthMethod? { + private suspend fun WcEthMethodName.toMethod(request: WcSdkSessionRequest, wallet: UserWallet): WcEthMethod? { val rawParams = request.request.params return when (this) { WcEthMethodName.EthSign, @@ -85,8 +100,8 @@ internal class WcEthNetwork( WcEthMethodName.AddEthereumChain -> moshi.fromJson>(rawParams) ?.firstOrNull() ?.let { - val newNetwork = namespaceConverter - .toNetwork(it.chainId, wallet) ?: return null + val newNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(it.chainId, wallet) + ?: return null WcEthMethod.AddEthereumChain(rawChain = it, network = newNetwork) } } 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 4b80dbfbd4..0621f51855 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 @@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData -import com.tangem.blockchain.common.Amount as BlockchainAmount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -17,6 +16,7 @@ import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.Amount import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.usecase.method.* @@ -24,13 +24,16 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* +import com.tangem.blockchain.common.Amount as BlockchainAmount +@Suppress("LongParameterList") internal class WcEthSendTransactionUseCase @AssistedInject constructor( @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SendTransaction, override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, private val sendTransaction: SendTransactionUseCase, + private val ethTxHelper: WcEthTxHelper, blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), WcTransactionUseCase, @@ -38,10 +41,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( WcMutableFee { private var approvalAmount: WcApprovedAmount? = null - private var dAppFee = WcEthTxHelper.getDAppFee( - network = context.network, - txParams = method.transaction, - ) + private var dAppFee: Fee? = null override val securityStatus: LceFlow = blockAidDelegate.getSecurityStatus( @@ -52,7 +52,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( accountAddress = context.accountAddress, ).map { lce -> lce.map { result -> - val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) + val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result) ?: return@map BlockAidTransactionCheck.Result.Plain(result) val tokenInfo = amount.tokenInfo this@WcEthSendTransactionUseCase.approvalAmount = WcApprovedAmount( @@ -80,8 +80,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( override suspend fun SignCollector.onSign(state: WcSignState) { val hash = sendTransaction(state.signModel, wallet, network) .onLeft { error -> - val sendError = IllegalArgumentException(error.toString()) // todo(wc) use domain error - emit(state.toResult(sendError.left())) + emit(state.toResult(parseSendError(error).left())) } .getOrNull() ?: return val respondResult = respondService.respond(rawSdkRequest, hash.formatHex()) @@ -112,7 +111,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( emit(newState) } - override fun dAppFee(): Fee.Ethereum.Legacy? { + override fun dAppFee(): Fee? { return dAppFee } @@ -121,8 +120,9 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( } override fun invoke(): Flow> = flow { - val transactionData = WcEthTxHelper.createTransactionData( - dAppFee = dAppFee(), + dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network) + val transactionData = ethTxHelper.createTransactionData( + dAppFee = dAppFee, network = context.network, txParams = method.transaction, ) ?: return@flow 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 b86988cbae..eda9598750 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 @@ -3,7 +3,6 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData -import com.tangem.blockchain.common.Amount as BlockchainAmount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex @@ -17,22 +16,22 @@ import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.tokens.model.Amount import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.usecase.method.* import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* +import com.tangem.blockchain.common.Amount as BlockchainAmount +@Suppress("LongParameterList") internal class WcEthSignTransactionUseCase @AssistedInject constructor( override val respondService: WcRespondService, override val analytics: AnalyticsEventHandler, private val prepareForSend: PrepareForSendUseCase, + private val ethTxHelper: WcEthTxHelper, @Assisted override val context: WcMethodUseCaseContext, @Assisted override val method: WcEthMethod.SignTransaction, blockAidDelegate: BlockAidVerificationDelegate, @@ -42,10 +41,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( WcMutableFee { private var approvalAmount: WcApprovedAmount? = null - private var dAppFee = WcEthTxHelper.getDAppFee( - network = context.network, - txParams = method.transaction, - ) + private var dAppFee: Fee? = null override val securityStatus = blockAidDelegate.getSecurityStatus( network = network, @@ -55,7 +51,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( accountAddress = context.accountAddress, ).map { lce -> lce.map { result -> - val amount = WcEthTxHelper.getApprovedAmount(method.transaction.data, result) + val amount = ethTxHelper.getApprovedAmount(method.transaction.data, result) ?: return@map BlockAidTransactionCheck.Result.Plain(result) val tokenInfo = amount.tokenInfo if (!amount.isUnlimited) { @@ -82,7 +78,7 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( val hash = prepareForSend(state.signModel, wallet, network) .map { it.toHexString().formatHex() } .onLeft { error -> - emit(state.toResult(error.left())) + emit(state.toResult(parseSendError(error).left())) } .getOrNull() ?: return @@ -119,15 +115,16 @@ internal class WcEthSignTransactionUseCase @AssistedInject constructor( } override fun invoke(): Flow> = flow { - val transactionData = WcEthTxHelper.createTransactionData( - dAppFee = dAppFee(), + dAppFee = ethTxHelper.getDAppFee(method.transaction, wallet, network) + val transactionData = ethTxHelper.createTransactionData( + dAppFee = dAppFee, network = context.network, txParams = method.transaction, ) ?: return@flow emitAll(delegate.invoke(transactionData)) } - override fun dAppFee(): Fee.Ethereum.Legacy? { + override fun dAppFee(): Fee? { return dAppFee } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt index 8e961267e1..c857512c54 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSignTypedDataUseCase.kt @@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.SignUseCase +import com.tangem.domain.walletconnect.error.parseTangemSdkError import com.tangem.domain.walletconnect.model.WcEthMethod import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcSignState @@ -51,7 +52,7 @@ internal class WcEthSignTypedDataUseCase @AssistedInject constructor( ?: return val signedHash = signUseCase(hashToSign, userWallet, network) - .onLeft { emit(state.toResult(it.left())) } + .onLeft { emit(state.toResult(parseTangemSdkError(it).left())) } .getOrNull() ?: return val respond = prepareToSendMessageData(signedHash, hashToSign, walletManager) 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 268633a213..59e52249da 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 @@ -7,40 +7,43 @@ import com.domain.blockaid.models.transaction.simultation.SimulationData import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.tokenmethods.ApprovalERC20TokenCallData import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.HEX_PREFIX import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.hexToBigDecimal +import com.tangem.blockchain.extensions.hexToBigInteger import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.common.extensions.hexToBytes +import com.tangem.data.common.currency.getCoinId import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase +import com.tangem.domain.transaction.usecase.GetEthSpecificFeeUseCase import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.domain.walletconnect.model.WcEthTransactionParams -import java.math.BigDecimal +import com.tangem.domain.wallets.models.UserWallet +import javax.inject.Inject -internal object WcEthTxHelper { - private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8") +internal class WcEthTxHelper @Inject constructor( + private val getSingleCryptoCurrency: GetSingleCryptoCurrencyStatusUseCase, + private val ethSpecificFee: GetEthSpecificFeeUseCase, +) { - fun getDAppFee(network: Network, txParams: WcEthTransactionParams): Fee.Ethereum.Legacy? { - val gasLimit = txParams.gas?.hexToBigDecimal() ?: return null - val gasPrice = txParams.gasPrice?.hexToBigDecimal() ?: return null - - val blockchain = network.toBlockchain() - - var feeDecimal = (gasLimit * gasPrice) - .movePointLeft(blockchain.decimals()) - if (blockchain == Blockchain.Mantle) { - feeDecimal = feeDecimal.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER) - } - - val feeAmount = Amount(feeDecimal, blockchain) - return Fee.Ethereum.Legacy(feeAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) + suspend fun getDAppFee(txParams: WcEthTransactionParams, userWallet: UserWallet, network: Network): Fee? { + val gasLimit = txParams.gas?.hexToBigInteger() ?: return null + val gasPrice = txParams.gasPrice?.hexToBigInteger() + val coinId = getCoinId(network, network.toBlockchain().toCoinId()) + val currency = getSingleCryptoCurrency.invokeMultiWalletSync(userWallet.walletId, coinId) + .map { it.currency } + .getOrNull() ?: return null + return ethSpecificFee(userWallet, currency, gasLimit, gasPrice) + .map { it.minimum } + .getOrNull() } fun createTransactionData( - dAppFee: Fee.Ethereum.Legacy?, + dAppFee: Fee?, network: Network, txParams: WcEthTransactionParams, ): TransactionData.Uncompiled? { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt index fe252ca79c..3f739b3f64 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt @@ -16,4 +16,7 @@ internal data class WcSolanaSignMessageRequest( internal data class WcSolanaSignTransactionRequest( @Json(name = "transaction") val transaction: String, + + @Json(name = "feePayer") + val feePayer: String?, ) \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt index e82c5601ea..b8adc0c608 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaMessageSignUseCase.kt @@ -12,6 +12,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.transaction.usecase.SignUseCase +import com.tangem.domain.walletconnect.error.parseTangemSdkError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase import com.tangem.domain.walletconnect.usecase.method.WcSignState @@ -40,7 +41,7 @@ internal class WcSolanaMessageSignUseCase @AssistedInject constructor( val userWallet = session.wallet val signedHash = signUseCase(hashToSign, userWallet, network) - .onLeft { emit(state.toResult(it.left())) } + .onLeft { emit(state.toResult(parseTangemSdkError(it).left())) } .getOrNull() ?: return val respond = "{ signature: \"${signedHash.encodeBase58()}\" }" diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index 38dc6265be..f994aba0f3 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -6,14 +6,14 @@ import com.tangem.blockchain.extensions.decodeBase58 import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.toHexString -import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter.Companion.fromJson import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network -import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.model.WcSolanaMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -21,13 +21,12 @@ import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import jakarta.inject.Inject -import timber.log.Timber internal class WcSolanaNetwork( private val moshi: Moshi, private val sessionsManager: WcSessionsManager, private val factories: Factories, - private val namespaceConverter: NamespaceConverter, + private val networksConverter: WcNetworksConverter, private val walletManagersFacade: WalletManagersFacade, ) : WcRequestToUseCaseConverter { @@ -37,17 +36,33 @@ internal class WcSolanaNetwork( return name } + @Suppress("CyclomaticComplexMethod") override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { val name = toWcMethodName(request) ?: return null val method: WcSolanaMethod = name.toMethod(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null - val network = namespaceConverter.toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null - val accountAddress = getAccountAddress(session, network) + val wallet = session.wallet + val chainId = request.chainId.orEmpty() + suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) + suspend fun anyAddress() = anyExistNetwork() + ?.let { network -> walletManagersFacade.getDefaultAddress(wallet.walletId, network).orEmpty() } + .orEmpty() + + val accountAddress = when (method) { + is WcSolanaMethod.SignAllTransaction -> anyAddress() + is WcSolanaMethod.SignMessage -> anyAddress() + is WcSolanaMethod.SignTransaction -> method.address ?: anyAddress() + } + val walletNetwork = networksConverter + .findWalletNetworkForRequest(request, session, accountAddress) + ?: anyExistNetwork() + ?: return null + val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, - network = network, - accountAddress = accountAddress.orEmpty(), + network = walletNetwork, + accountAddress = accountAddress, ) return when (method) { is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method) @@ -56,15 +71,6 @@ internal class WcSolanaNetwork( } } - private suspend fun getAccountAddress(session: WcSession, network: Network): String? { - return try { - walletManagersFacade.getDefaultAddress(session.wallet.walletId, network) - } catch (exception: Exception) { - Timber.e(exception) - null - } - } - internal class NamespaceConverter @Inject constructor( override val excludedBlockchains: ExcludedBlockchains, ) : WcNamespaceConverter { @@ -107,7 +113,7 @@ internal class WcSolanaNetwork( ) } WcSolanaMethodName.SignTransaction -> moshi.fromJson(rawParams) - ?.let { request -> WcSolanaMethod.SignTransaction(request.transaction) } + ?.let { request -> WcSolanaMethod.SignTransaction(request.transaction, request.feePayer) } WcSolanaMethodName.SendAllTransaction -> moshi.fromJson>(rawParams)?.let { list -> WcSolanaMethod.SignAllTransaction(list) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt index b60d6ff124..8e91c2c8dd 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAllTransactionUseCase.kt @@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck import com.tangem.domain.walletconnect.usecase.method.WcListTransactionUseCase @@ -47,7 +48,7 @@ internal class WcSolanaSignAllTransactionUseCase @AssistedInject constructor( override suspend fun SignCollector>.onSign(state: WcSignState>) { val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> - emit(state.toResult(error.left())) + emit(state.toResult(parseSendError(error).left())) } .getOrNull() ?: return diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt index 13e0fb026a..282125a721 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt @@ -11,6 +11,7 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.error.parseSendError import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.method.BlockAidTransactionCheck import com.tangem.domain.walletconnect.usecase.method.WcSignState @@ -45,7 +46,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( override suspend fun SignCollector.onSign(state: WcSignState) { val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> - emit(state.toResult(error.left())) + emit(state.toResult(parseSendError(error).left())) } .getOrNull() ?: return 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 f07d69157a..cdf7ca5bc0 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 @@ -2,7 +2,6 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet.Model.Namespace -import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -25,16 +24,6 @@ internal class AssociateNetworksDelegate( private val tokensFeatureToggles: TokensFeatureToggles, ) { - suspend fun associate(wallet: UserWallet, namespaces: Map): Set { - val walletNetworks = getWalletNetworks(userWalletId = wallet.walletId) - val namespacesSet = namespaces.values.flatMap { proposal -> proposal.chains ?: listOf() }.toSet() - return namespacesSet.mapNotNullTo(mutableSetOf()) { chainId -> - val wcNetwork = namespaceConverters - .firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } ?: return@mapNotNullTo null - walletNetworks.find { network -> wcNetwork.id == network.id } - } - } - @Throws(WcPairError.UnsupportedBlockchains::class) suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } @@ -48,6 +37,7 @@ internal class AssociateNetworksDelegate( } } + @Suppress("ComplexCondition") private suspend fun mapNetworksForWallet( wallet: UserWallet, requiredNamespaces: Set, @@ -57,6 +47,7 @@ internal class AssociateNetworksDelegate( val walletNetworks = getWalletNetworks(userWalletId = wallet.walletId) val unknownRequired = mutableSetOf() + val unknownOptional = mutableSetOf() val missingRequired = mutableSetOf() val required = mutableSetOf() val available = mutableSetOf() @@ -68,9 +59,9 @@ internal class AssociateNetworksDelegate( unknownRequired.add(missingNetworkName(chainId)) return@forEach } - val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } + val walletNetwork = walletNetworks.find { network -> wcNetwork.rawId == network.rawId } - if (walletNetwork == null || isCustomCoin(walletNetwork)) { + if (walletNetwork == null) { missingRequired.add(wcNetwork) } else { required.add(walletNetwork) @@ -78,9 +69,12 @@ internal class AssociateNetworksDelegate( } optionalNamespaces.forEach { chainId -> val wcNetwork = namespaceConverters.firstNotNullOfOrNull { it.toNetwork(chainId, wallet) } - ?: return@forEach - val walletNetwork = walletNetworks.find { network -> wcNetwork.id == network.id } - if (walletNetwork != null && !isCustomCoin(walletNetwork)) { + if (wcNetwork == null) { + unknownOptional.add(missingNetworkName(chainId)) + return@forEach + } + val walletNetwork = walletNetworks.find { network -> wcNetwork.rawId == network.rawId } + if (walletNetwork != null) { available.add(walletNetwork) } else { notAdded.add(wcNetwork) @@ -89,6 +83,9 @@ internal class AssociateNetworksDelegate( if (unknownRequired.isNotEmpty()) { throw WcPairError.UnsupportedBlockchains(unknownRequired, sessionProposal.name) } + if (unknownOptional.isNotEmpty() && required.isEmpty() && available.isEmpty() && missingRequired.isEmpty()) { + throw WcPairError.UnsupportedBlockchains(unknownOptional, sessionProposal.name) + } return ProposalNetwork( wallet = wallet, missingRequired = missingRequired, @@ -109,6 +106,8 @@ internal class AssociateNetworksDelegate( } .filterIsInstance() .map(CryptoCurrency.Coin::network) + // flatten all derivation + .distinctBy { it.rawId } } private fun Map.setOfChainId(): Set = diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt index 2cf3fb99c1..2aba95bd34 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/CaipNamespaceDelegate.kt @@ -1,48 +1,47 @@ package com.tangem.data.walletconnect.pair import com.reown.walletkit.client.Wallet -import com.tangem.data.walletconnect.model.CAIP10 import com.tangem.data.walletconnect.utils.WcNamespaceConverter +import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.domain.models.network.Network +import com.tangem.data.walletconnect.model.CAIP10 +import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId internal class CaipNamespaceDelegate( private val namespaceConverters: Set, private val walletManagersFacade: WalletManagersFacade, + private val wcNetworksConverter: WcNetworksConverter, ) { suspend fun associate( sessionProposal: Wallet.Model.SessionProposal, - userWallet: UserWallet, - networks: List, + sessionForApprove: WcSessionApprove, ): Map { + val userWallet = sessionForApprove.wallet val result = mutableMapOf() - networks.map { network -> - val address = walletManagersFacade.getDefaultAddress(userWallet.walletId, network) - val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) } - requireNotNull(chainId) - requireNotNull(address) - CAIP10(chainId = chainId, accountAddress = address) - }.forEach { account -> - val namespaceKey = account.chainId.namespace - val session = result.getOrPut(namespaceKey) { Session() } - val requiredNamespaces = sessionProposal.requiredNamespaces - val optionalNamespaces = sessionProposal.optionalNamespaces - val methods = buildSet { - requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) } - optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) } + wcNetworksConverter.convertNetworksForApprove(sessionForApprove) + .mapNotNull { createCAIP10(userWallet.walletId, it) } + .forEach { account -> + val namespaceKey = account.chainId.namespace + val session = result.getOrPut(namespaceKey) { Session() } + val requiredNamespaces = sessionProposal.requiredNamespaces + val optionalNamespaces = sessionProposal.optionalNamespaces + val methods = buildSet { + requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) } + optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) } + } + val events = buildSet { + requiredNamespaces[namespaceKey]?.events?.let { addAll(it) } + optionalNamespaces[namespaceKey]?.events?.let { addAll(it) } + } + session.chains.add(account.chainId.raw) + session.accounts.add(account.raw) + session.methods.addAll(methods) + session.events.addAll(events) } - val events = buildSet { - requiredNamespaces[namespaceKey]?.events?.let { addAll(it) } - optionalNamespaces[namespaceKey]?.events?.let { addAll(it) } - } - session.chains.add(account.chainId.raw) - session.accounts.add(account.raw) - session.methods.addAll(methods) - session.events.addAll(events) - } return result.mapValues { (_, session) -> Wallet.Model.Namespace.Session( chains = session.chains.toList(), @@ -53,6 +52,13 @@ internal class CaipNamespaceDelegate( } } + private suspend fun createCAIP10(userWalletId: UserWalletId, network: Network): CAIP10? { + val address = walletManagersFacade.getDefaultAddress(userWalletId, network) + val chainId = namespaceConverters.firstNotNullOfOrNull { it.toCAIP2(network) } + if (chainId == null || address == null) return null + return CAIP10(chainId = chainId, accountAddress = address) + } + private data class Session( val chains: MutableSet = mutableSetOf(), val accounts: MutableSet = mutableSetOf(), diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt index bb0c819b4b..d0749f0362 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt @@ -1,17 +1,45 @@ package com.tangem.data.walletconnect.pair +import androidx.core.net.toUri import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.repository.WcSessionsManager import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.receiveAsFlow import javax.inject.Inject -class DefaultWcPairService @Inject constructor() : WcPairService { +class DefaultWcPairService @Inject constructor( + private val sessionsManager: WcSessionsManager, +) : WcPairService { private val _pairFlow: Channel = Channel(Channel.BUFFERED) - override val pairFlow: Flow = _pairFlow.receiveAsFlow() + override val pairFlow: Flow = _pairFlow + .receiveAsFlow() + .filter(::filterDeeplinkRequestWithSessionTopic) override fun pair(request: WcPairRequest) { _pairFlow.trySend(request) } + + // some dApp sends deeplink with session request + // we filter session exist, but start dApp pair flow if unexist + private suspend fun filterDeeplinkRequestWithSessionTopic(request: WcPairRequest): Boolean { + when (request.source) { + WcPairRequest.Source.QR, + WcPairRequest.Source.CLIPBOARD, + WcPairRequest.Source.ETC, + -> return true + WcPairRequest.Source.DEEPLINK -> Unit + } + + val isExistSession = existSessionTopic(request.uri).getOrNull() ?: false + return !isExistSession + } + + private suspend fun existSessionTopic(uri: String) = runCatching { + val sessionTopic = uri.toUri().getQueryParameter("sessionTopic") ?: return@runCatching false + val isExistSession = sessionsManager.findSessionByTopic(sessionTopic) != null + return@runCatching isExistSession + } } \ No newline at end of file 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 885f896a41..e02b9f623c 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 @@ -8,10 +8,10 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -22,6 +22,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* +import org.joda.time.DateTime import timber.log.Timber @Suppress("LongParameterList") @@ -37,6 +38,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( private val onCallTerminalAction = Channel() + @Suppress("LongMethod") override operator fun invoke(): Flow { val (uri: String, source: WcPairRequest.Source) = pairRequest return flow { @@ -47,6 +49,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( val sdkSessionProposal = sdkDelegate.pair(uri) .onLeft { Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest") + analytics.send(WcAnalyticEvents.PairFailed) emit(WcPairState.Error(it)) } .getOrNull() ?: return@flow @@ -76,7 +79,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( } // finish flow if rejected above if (sessionForApprove == null) { - analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession)) + analytics.send(WcAnalyticEvents.SessionDisconnected(proposalState.dAppSession.dAppMetaData)) sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) return@flow } @@ -92,6 +95,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( sdkModel = WcSdkSessionConverter.convert(settledSession.session), securityStatus = proposalState.dAppSession.securityStatus, networks = sessionForApprove.network.toSet(), + connectingTime = DateTime.now().millis, ) sessionsManager.saveSession(newSession) analytics.send( @@ -132,8 +136,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ): Either { val namespaces = caipNamespaceDelegate.associate( sdkSessionProposal, - sessionForApprove.wallet, - sessionForApprove.network, + sessionForApprove, ) val sessionApprove = Wallet.Params.SessionApprove( proposerPublicKey = sdkSessionProposal.proposerPublicKey, @@ -155,7 +158,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( analytics.send( WcAnalyticEvents.PairRequested( network = requestedNetworks, - verificationInfo.name, + verificationInfo, ), ) val appMetaData = WcAppMetaData( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index e87d12d41f..8393816677 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -6,6 +6,7 @@ import arrow.core.right import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WC_TAG +import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber @@ -13,7 +14,7 @@ import kotlin.coroutines.resume internal class DefaultWcRespondService : WcRespondService { - override suspend fun respond(request: WcSdkSessionRequest, response: String): Either = + override suspend fun respond(request: WcSdkSessionRequest, response: String): Either = suspendCancellableCoroutine { continuation -> WalletKit.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( @@ -26,12 +27,19 @@ internal class DefaultWcRespondService : WcRespondService { onSuccess = { if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).i("Successful respond for request $request") - continuation.resume(Unit.right()) + val result = when (val response = it.jsonRpcResponse) { + is Wallet.Model.JsonRpcResponse.JsonRpcError -> WcRequestError.WcRespondError( + code = response.code, + message = response.message, + ).left() + is Wallet.Model.JsonRpcResponse.JsonRpcResult -> response.result.right() + } + continuation.resume(result) }, onError = { if (continuation.isCompleted) return@respondSessionRequest Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") - continuation.resume(it.throwable.left()) + continuation.resume(WcRequestError.UnknownError(it.throwable).left()) }, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt index be259755f2..47fe01e948 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/WcRespondService.kt @@ -1,9 +1,10 @@ package com.tangem.data.walletconnect.respond import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest interface WcRespondService { - suspend fun respond(request: WcSdkSessionRequest, response: String): Either + suspend fun respond(request: WcSdkSessionRequest, response: String): Either fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String = "") } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index b82c70ec75..28afaab46a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -6,11 +6,10 @@ import arrow.core.right import com.domain.blockaid.models.dapp.CheckDAppResult import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit -import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate -import com.tangem.data.walletconnect.utils.WC_TAG -import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.data.walletconnect.utils.WcSdkSessionConverter +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionDTO import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository @@ -21,15 +20,18 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* +import org.joda.time.DateTime import timber.log.Timber import kotlin.coroutines.resume +@Suppress("LongParameterList") internal class DefaultWcSessionsManager( private val store: WalletConnectStore, private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, private val dispatchers: CoroutineDispatcherProvider, - private val associateNetworks: AssociateNetworksDelegate, + private val wcNetworksConverter: WcNetworksConverter, + private val analytics: AnalyticsEventHandler, private val scope: CoroutineScope, ) : WcSessionsManager, WcSdkObserver { @@ -61,30 +63,28 @@ internal class DefaultWcSessionsManager( } override suspend fun saveSession(session: WcSession) { - store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId, session.securityStatus)) + store.saveSession( + WcSessionDTO( + topic = session.sdkModel.topic, + walletId = session.wallet.walletId, + securityStatus = session.securityStatus, + connectingTime = session.connectingTime ?: DateTime.now().millis, + ), + ) } override suspend fun removeSession(session: WcSession): Either { val topic = session.sdkModel.topic val sdkCall = sdkDisconnectSession(topic) .onRight { onSessionDelete.trySend(Wallet.Model.SessionDelete.Success(topic = topic, reason = "")) } + analytics.send(WcAnalyticEvents.SessionDisconnected(session.sdkModel.appMetaData)) return sdkCall } override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { - val storedSession = sessions.firstOrNull() + sessions.firstOrNull() ?.values?.flatten() ?.firstOrNull { it.sdkModel.topic == topic } - ?: return@withContext null - val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null - val wallet = storedSession.wallet - val networks = associateNetworks.associate(wallet, sdkSession.namespaces) - WcSession( - wallet = wallet, - sdkModel = WcSdkSessionConverter.convert(sdkSession), - securityStatus = storedSession.securityStatus, - networks = networks, - ) } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { @@ -123,15 +123,16 @@ internal class DefaultWcSessionsManager( inStore: Set, wallets: List, ): List { - val wcSessions = inStore.mapNotNull { session -> - val wallet = wallets.find { it.walletId == session.walletId } ?: return@mapNotNull null - val sdkSession = inSdk.find { it.topic == session.topic } ?: return@mapNotNull null - val networks = associateNetworks.associate(wallet, sdkSession.namespaces) + val wcSessions = inStore.mapNotNull { storeSession -> + val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null + val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null + val networks = wcNetworksConverter.findWalletNetworks(wallet, sdkSession) WcSession( wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), - securityStatus = session.securityStatus, + securityStatus = storeSession.securityStatus, networks = networks, + connectingTime = storeSession.connectingTime, ) } return wcSessions diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt new file mode 100644 index 0000000000..3421317947 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt @@ -0,0 +1,34 @@ +package com.tangem.data.walletconnect.sign + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.domain.models.network.Network +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +internal class BlockAidChainNameConverter @Inject constructor() : Converter { + + @Suppress("CyclomaticComplexMethod") + override fun convert(value: Network): String { + return when (Blockchain.fromNetworkId(value.backendId)) { + Blockchain.Arbitrum -> "arbitrum" + Blockchain.Avalanche -> "avalanche" + Blockchain.AvalancheTestnet -> "avalanche-fuji" + Blockchain.Binance, Blockchain.BSC -> "bsc" + Blockchain.Ethereum -> "ethereum" + Blockchain.EthereumTestnet -> "ethereum-sepolia" + Blockchain.Polygon -> "polygon" + Blockchain.Solana -> "mainnet" + Blockchain.Gnosis -> "gnosis" + Blockchain.Optimism -> "optimism" + Blockchain.ZkSyncEra -> "zksync" + Blockchain.ZkSyncEraTestnet -> "zksync-sepolia" + Blockchain.Base -> "base" + Blockchain.BaseTestnet -> "base-sepolia" + Blockchain.Blast, Blockchain.BlastTestnet -> "blast" + Blockchain.ApeChain, Blockchain.ApeChainTestnet -> "apechain" + Blockchain.Scroll -> "scroll" + else -> value.name + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt index 138f17d2c0..5c9e430c26 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/SignStateConverter.kt @@ -1,16 +1,12 @@ package com.tangem.data.walletconnect.sign import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep object SignStateConverter { - internal fun preSign(signModel: M) = WcSignState(signModel, WcSignStep.PreSign) - internal fun signing(signModel: M) = WcSignState(signModel, WcSignStep.Signing) - internal fun result(result: Either, signModel: M) = - WcSignState(signModel, WcSignStep.Result(result)) - internal fun WcSignState.toPreSign(signModel: M = this.signModel) = copy( signModel = signModel, domainStep = WcSignStep.PreSign, @@ -21,8 +17,9 @@ object SignStateConverter { signModel = signModel, ) - internal fun WcSignState.toResult(result: Either, signModel: M = this.signModel) = copy( - domainStep = WcSignStep.Result(result), - signModel = signModel, - ) + internal fun WcSignState.toResult(result: Either, signModel: M = this.signModel) = + copy( + domainStep = WcSignStep.Result(result), + signModel = signModel, + ) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index 68bd9a22b5..2dea8ba415 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -6,6 +6,8 @@ import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning import com.tangem.domain.walletconnect.WcAnalyticEvents +import com.tangem.domain.walletconnect.model.WcRequestError +import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcSignStep import kotlinx.coroutines.Job @@ -62,18 +64,19 @@ internal class WcSignUseCaseDelegate( fun signFlow() = flow { onSign(state.updateAndGet { it.toSigning() }) } .onEach { newState -> state.update { newState } } .catch { exception -> - val errorResult = state.value.toResult(exception.left()) + val errorResult = state.value + .toResult(WcRequestError.UnknownError(exception).left()) state.update { errorResult } } .onEach { state -> val step = state.domainStep as? WcSignStep.Result ?: return@onEach val event = step.result.fold( - ifLeft = { + ifLeft = { error -> WcAnalyticEvents.SignatureRequestFailed( session = context.session, rawRequest = context.rawSdkRequest, network = context.network, - it.message.orEmpty(), + errorCode = error.code() ?: error::class.simpleName.orEmpty(), ) }, ifRight = { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index 9274fdc9c8..4a774ac505 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -1,6 +1,7 @@ package com.tangem.data.walletconnect.utils import com.domain.blockaid.models.transaction.* +import com.tangem.data.walletconnect.sign.BlockAidChainNameConverter import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow @@ -16,6 +17,7 @@ import javax.inject.Inject internal class BlockAidVerificationDelegate @Inject constructor( private val blockAidVerifier: BlockAidVerifier, + private val blockAidChainNameConverter: BlockAidChainNameConverter, ) { fun getSecurityStatus( @@ -47,7 +49,7 @@ internal class BlockAidVerificationDelegate @Inject constructor( }?.let { params -> blockAidVerifier.verifyTransaction( TransactionData( - chain = network.name, + chain = blockAidChainNameConverter.convert(network), accountAddress = accountAddress, method = rawSdkRequest.request.method, domainUrl = session.sdkModel.appMetaData.url, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt index b1a98f54da..86d099131c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNamespaceConverter.kt @@ -3,11 +3,10 @@ package com.tangem.data.walletconnect.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.network.NetworkFactory -import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.data.walletconnect.model.NamespaceKey import com.tangem.domain.models.network.Network +import com.tangem.data.walletconnect.model.CAIP2 import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet internal interface WcNamespaceConverter { @@ -27,7 +26,7 @@ internal interface WcNamespaceConverter { return NetworkFactory(excludedBlockchains).create( blockchain = blockchain, extraDerivationPath = null, - scanResponse = wallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = wallet, ) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt new file mode 100644 index 0000000000..b82dfa5d04 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -0,0 +1,102 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.tangem.data.common.currency.isCustomCoin +import com.tangem.data.walletconnect.model.CAIP10 +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier +import com.tangem.domain.tokens.TokensFeatureToggles +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionApprove +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import javax.inject.Inject + +internal class WcNetworksConverter @Inject constructor( + private val namespaceConverters: Set, + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, + private val tokensFeatureToggles: TokensFeatureToggles, +) { + + suspend fun findWalletNetworkForRequest( + request: WcSdkSessionRequest, + session: WcSession, + requestAddress: String, + ): Network? { + val wallet = session.wallet + val allCoinNetwork = filterWalletNetworkForRequest(request.chainId.orEmpty(), session.wallet) + + val requestNetwork = allCoinNetwork.find { network -> + val address = walletManagersFacade.getDefaultAddress(wallet.walletId, network) + requestAddress.lowercase() == address?.lowercase() + } + return requestNetwork + } + + /** + * return network with not custom derivationPath or first custom or any + */ + suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): Network? { + val networks = filterWalletNetworkForRequest(rawChainId, wallet) + return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull() + } + + /** + * return all exist derivation networks + */ + suspend fun filterWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): List { + val walletNetworks = getWalletNetworks(wallet.walletId) + + val blockchain = namespaceConverters + .firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf() + + val allCoinNetwork = walletNetworks.filter { it.rawId == blockchain.id } + return allCoinNetwork + } + + suspend fun findWalletNetworks(wallet: UserWallet, sdkSession: Wallet.Model.Session): Set { + val walletNetworks = getWalletNetworks(wallet.walletId) + val existNetworks = sdkSession.namespaces.values + .map { it.accounts }.flatten().toSet() + .mapNotNull { CAIP10.fromRaw(it) } + .mapNotNullTo(mutableSetOf()) { caip10 -> + val blockchain = namespaceConverters + .firstNotNullOfOrNull { it.toBlockchain(caip10.chainId) } + ?: return@mapNotNullTo null + walletNetworks + // find all derivation + .filter { it.rawId == blockchain.id } + // find equal address + .firstOrNull { + val walletAddress = walletManagersFacade.getDefaultAddress(wallet.walletId, it) + walletAddress?.lowercase() == caip10.accountAddress.lowercase() + } + } + + return existNetworks + } + + suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List { + val walletNetworks = getWalletNetworks(sessionForApprove.wallet.walletId) + return sessionForApprove.network + .map { network -> walletNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } } + .flatten() + } + + private suspend fun getWalletNetworks(userWalletId: UserWalletId): List { + return if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) { + multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), + ).orEmpty() + } else { + currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + }.filterIsInstance().map(CryptoCurrency.Coin::network) + } +} \ 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 81dd6ad575..286a997186 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 @@ -95,6 +95,7 @@ internal class DefaultWcPairUseCaseTest { sdkModel = WcSdkSessionConverter.convert(this), securityStatus = CheckDAppResult.SAFE, networks = setOf(), + connectingTime = null, ) private fun useCaseFactory() = DefaultWcPairUseCase( @@ -113,8 +114,7 @@ internal class DefaultWcPairUseCaseTest { coEvery { caipNamespaceDelegate.associate( sessionProposal = sdkProposal, - userWallet = sessionForApprove.wallet, - networks = sessionForApprove.network, + sessionForApprove = sessionForApprove, ) } returns mapOf() } @@ -144,7 +144,7 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right() - coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit + coEvery { sessionsManager.saveSession(any()) } returns Unit coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } val useCase = useCaseFactory() @@ -162,9 +162,12 @@ internal class DefaultWcPairUseCaseTest { assertEquals(approveLoading, awaitItem()) coVerifyOrder { sdkDelegate.approve(sdkApprove) - sessionsManager.saveSession(sessionForSave) + sessionsManager.saveSession(any()) } - assertEquals(result, awaitItem()) + val actual: WcPairState = awaitItem() + assert(actual is WcPairState.Approving.Result) + actual as WcPairState.Approving.Result + assertEquals(result.session, actual.session) awaitComplete() } } diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt index 4251b79b72..34e1e8c848 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -10,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.* import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession @@ -32,6 +33,7 @@ internal class WcSignUseCaseDelegateTest { object : FinalActionCollector {} private val initSignModel = TestSignModel() private val analytics: AnalyticsEventHandler = mockk(relaxed = true) + private val simpleResult = "hex".right() private val rawRequestMock = WcSdkSessionRequest( topic = "", chainId = "", @@ -49,6 +51,7 @@ internal class WcSignUseCaseDelegateTest { wallet = MockUserWalletFactory.create(), networks = setOf(), securityStatus = CheckDAppResult.FAILED_TO_VERIFY, + connectingTime = 0L, sdkModel = WcSdkSession( topic = "", appMetaData = WcAppMetaData( @@ -64,14 +67,14 @@ internal class WcSignUseCaseDelegateTest { private val initState = WcSignState(initSignModel, WcSignStep.PreSign) private val signing = initState.toSigning() - private val result = signing.toResult(Unit.right()) - private val testException = RuntimeException("test") + private val result = signing.toResult(simpleResult) + private val testException = WcRequestError.UnknownError(RuntimeException("test")) private val successSign: suspend FlowCollector>.( currentState: WcSignState, ) -> Unit = { state -> delay(2) - emit(state.toResult(Unit.right())) + emit(state.toResult(simpleResult)) } private val failedSign: suspend FlowCollector>.( @@ -161,8 +164,9 @@ internal class WcSignUseCaseDelegateTest { @Test fun `failed sign and catch unknown exception`() = runTest { - val exception = RuntimeException("asd") - val expectedErrorState = signing.toResult(exception.left()) + val exception = RuntimeException("test") + val testException = WcRequestError.UnknownError(exception) + val expectedErrorState = signing.toResult(testException.left()) finalActionCollector = object : FinalActionCollector { override suspend fun SignCollector.onSign(state: WcSignState) { delay(2) diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt index 7e6414e4ed..3172b275c4 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.card import arrow.core.Either +import arrow.core.right import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.ByteArrayKey @@ -22,16 +23,17 @@ class GetExtendedPublicKeyForCurrencyUseCase( private val derivationsRepository: DerivationsRepository, private val walletManagersFacade: WalletManagersFacade, ) { + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { return Either.catch { - val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) - ?: error("Wallet not found") + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) + ?: error("Wallet not found for userWalletId=$userWalletId and network=$network") val blockchain = network.toBlockchain() val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain) val hdKey = if (isSecp256k1Blockchain) { - userWallet.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found") + walletManager.wallet.publicKey.derivationType?.hdKey ?: error("No derivation found") } else { error("No derivation found") } @@ -50,7 +52,7 @@ class GetExtendedPublicKeyForCurrencyUseCase( val pendingDerivations = getPendingDerivations(childKey, parentKey) val derivedKeys = deriveKeys( userWalletId = userWalletId, - seedKey = userWallet.wallet.publicKey.seedKey, + seedKey = walletManager.wallet.publicKey.seedKey, paths = pendingDerivations, ) @@ -73,15 +75,17 @@ class GetExtendedPublicKeyForCurrencyUseCase( /** * @return true if xpub generation is supported, false otherwise */ - suspend fun isSupported(userWalletId: UserWalletId, network: Network): Boolean { - val userWallet = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) - ?: error("Wallet not found") + suspend fun isSupported(userWalletId: UserWalletId, network: Network): Either = Either.catch { + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) + ?: error("Wallet not found for user wallet $userWalletId and network ${network.id}") val blockchain = network.toBlockchain() val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain) - val isHdKey = userWallet.wallet.publicKey.derivationType?.hdKey + val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey - return isSecp256k1Blockchain && isHdKey != null + val isSupported = isSecp256k1Blockchain && isHdKey != null + + return isSupported.right() } private suspend fun deriveKeys( diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt index aaa5711ded..92cb369c80 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/NetworkHasDerivationUseCase.kt @@ -4,13 +4,18 @@ import arrow.core.Either import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.models.network.Network -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet class NetworkHasDerivationUseCase { - operator fun invoke(scanResponse: ScanResponse, network: Network): Either { - val blockchain = network.toBlockchain() - val derivationPath = network.derivationPath.value - return Either.catch { derivationPath != null && scanResponse.hasDerivation(blockchain, derivationPath) } + operator fun invoke(userWallet: UserWallet, network: Network): Either = Either.catch { + when (userWallet) { + is UserWallet.Cold -> { + val blockchain = network.toBlockchain() + val derivationPath = network.derivationPath.value + derivationPath != null && userWallet.scanResponse.hasDerivation(blockchain, derivationPath) + } + is UserWallet.Hot -> true + } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt index c985aac75d..ecf06c78d8 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -36,7 +36,4 @@ interface CardSdkConfigRepository { /** Set linked terminal by [isLinked] */ fun setLinkedTerminal(isLinked: Boolean?) - - /** Set [flag] that determines whether to use Prod environment for Tangem API */ - fun setTangemApiProdEnvFlag(flag: Boolean) } \ 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 index 7d153e9bf1..6d3effb0c0 100644 --- 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 @@ -2,7 +2,7 @@ package com.tangem.domain.express.models import java.math.BigDecimal -sealed class ExpressError { +sealed class ExpressError : Throwable() { abstract val code: Int 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 index 3c77da80fb..343c0bda7b 100644 --- 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 @@ -21,4 +21,16 @@ enum class ExpressProviderType(val typeName: String) { @Json(name = "onramp") ONRAMP(typeName = "ONRAMP"), + ; + + companion object { + fun ExpressProviderType.shouldStoreSwapTransaction() = when (this) { + CEX, + DEX_BRIDGE, + -> true + DEX, + ONRAMP, + -> false + } + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt index e4ff4eea14..5c0fbdcc83 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/DerivationStyleProvider.kt @@ -23,4 +23,8 @@ internal class TangemDerivationStyleProvider( private fun firstBatchesOfWallet1(card: CardDTO): Boolean { return card.batchId == "AC01" || card.batchId == "AC02" || card.batchId == "CB95" } +} + +internal class TangemHotDerivationStyleProvider : DerivationStyleProvider { + override fun getDerivationStyle(): DerivationStyle? = DerivationStyle.V3 } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt index 3d96490408..75671ca751 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt @@ -7,7 +7,9 @@ import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.models.UserWallet // TODO: refactor [REDACTED_JIRA] @@ -17,6 +19,20 @@ import com.tangem.domain.models.scan.CardDTO val FirmwareVersion.Companion.SolanaTokensAvailable get() = FirmwareVersion(4, 52) +fun UserWallet.supportedBlockchains(excludedBlockchains: ExcludedBlockchains): List { + return when (this) { + is UserWallet.Cold -> { + scanResponse.card.supportedBlockchains( + excludedBlockchains = excludedBlockchains, + cardTypesResolver = this.cardTypesResolver, + ) + } + is UserWallet.Hot -> { + Blockchain.entries.filter { it !in excludedBlockchains } + } + } +} + fun CardDTO.supportedBlockchains( cardTypesResolver: CardTypesResolver, excludedBlockchains: ExcludedBlockchains, @@ -55,6 +71,33 @@ fun CardDTO.supportedTokens( return tokensSupportedByCard.filter { isTestCard == it.isTestnet() } } +fun UserWallet.canHandleToken(supportedTokens: List, blockchain: Blockchain): Boolean { + return when (this) { + is UserWallet.Cold -> { + scanResponse.card.canHandleToken( + supportedTokens = supportedTokens, + blockchain = blockchain, + cardTypesResolver = scanResponse.cardTypesResolver, + ) + } + is UserWallet.Hot -> blockchain in supportedTokens + } +} + +fun UserWallet.canHandleToken(blockchain: Blockchain, excludedBlockchains: ExcludedBlockchains): Boolean { + return when (this) { + is UserWallet.Cold -> { + scanResponse.card.canHandleToken( + blockchain = blockchain, + excludedBlockchains = excludedBlockchains, + cardTypesResolver = scanResponse.cardTypesResolver, + ) + } + + is UserWallet.Hot -> blockchain !in excludedBlockchains + } +} + /** * The same as [CardDTO.supportedTokens] but with supportedTokens input, if previously calculated */ @@ -101,7 +144,8 @@ fun CardDTO.canHandleBlockchain( ): Boolean { val cardConfig = CardConfig.createConfig(this) val primaryCurveForBlockchain = cardConfig.primaryCurve(blockchain) - val isContainsBlockchain = blockchain in supportedBlockchains(cardTypesResolver, excludedBlockchains) + val isContainsBlockchain = + blockchain in supportedBlockchains(cardTypesResolver, excludedBlockchains) val isWalletForCurveExists = wallets.any { it.curve == primaryCurveForBlockchain } // fixme: check for first wallets with 1 curve and remove condition return if (cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt index 8eaadd231c..cdf757a38a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt @@ -74,7 +74,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( } } -private fun makePublicKey( +fun makePublicKey( seedKey: ByteArray, blockchain: Blockchain, derivationPath: DerivationPath, diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt index 60c72030bc..9837067daf 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExt.kt @@ -8,6 +8,7 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TangemCardTypesResolver import com.tangem.domain.common.TangemDerivationStyleProvider +import com.tangem.domain.common.TangemHotDerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig @@ -23,6 +24,12 @@ val ScanResponse.cardTypesResolver: CardTypesResolver walletData = walletData, ) +val UserWallet.derivationStyleProvider: DerivationStyleProvider + get() = when (this) { + is UserWallet.Cold -> this.scanResponse.derivationStyleProvider + is UserWallet.Hot -> TangemHotDerivationStyleProvider() + } + val ScanResponse.derivationStyleProvider: DerivationStyleProvider get() = card.derivationStyleProvider diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 093dbc7445..488a604d5f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -3,7 +3,6 @@ package com.tangem.domain.exchange import arrow.core.Either import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.models.UserWallet @@ -15,21 +14,19 @@ import kotlinx.coroutines.flow.Flow */ interface RampStateManager { - suspend fun availableForBuy( - scanResponse: ScanResponse, - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): ScenarioUnavailabilityReason + suspend fun availableForBuy(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): ScenarioUnavailabilityReason /** * Check if [CryptoCurrency] is available for sell * - * @param userWallet user wallet - * @param status crypto currency status + * @param userWalletId the ID of the user's wallet + * @param status crypto currency status + * @param sendUnavailabilityReason the reason why sending is unavailable or null */ suspend fun availableForSell( - userWallet: UserWallet, + userWalletId: UserWalletId, status: CryptoCurrencyStatus, + sendUnavailabilityReason: ScenarioUnavailabilityReason?, ): Either suspend fun availableForSwap( @@ -42,4 +39,15 @@ interface RampStateManager { fun getSellInitializationStatus(): Flow> fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow> + + /** + * Returns the reason why sending is unavailable for the given user wallet and cryptocurrency status + * + * @param userWalletId the ID of the user's wallet + * @param cryptoCurrencyStatus the status of the cryptocurrency + */ + suspend fun getSendUnavailabilityReason( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): ScenarioUnavailabilityReason } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index ebbb56ce04..91d56ba37a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -41,7 +41,6 @@ import com.tangem.domain.walletmanager.utils.* import com.tangem.domain.walletmanager.utils.WalletManagerFactory import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex @@ -371,21 +370,31 @@ class DefaultWalletManagersFacade( initMutex.withLock { val userWallet = getUserWallet(userWalletId) - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - var walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, blockchain = blockchain, derivationPath = derivationPath, ) - if (walletManager == null) { - walletManager = walletManagerFactory.createWalletManager( - scanResponse = userWallet.scanResponse, - blockchain = blockchain, - derivationPath = derivationPath?.let { DerivationPath(rawPath = it) }, - ) - walletManager ?: return null + val path = derivationPath?.let { DerivationPath(rawPath = it) } + if (walletManager == null) { + when (userWallet) { + is UserWallet.Hot -> { + walletManager = walletManagerFactory.createWalletManagerForHot( + hotWallet = userWallet, + blockchain = blockchain, + derivationPath = path, + ) + } + is UserWallet.Cold -> { + walletManager = walletManagerFactory.createWalletManager( + scanResponse = userWallet.scanResponse, + blockchain = blockchain, + derivationPath = path, + ) + } + } + walletManager ?: return null walletManagersStore.store(userWalletId, walletManager) } return walletManager diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt index c5b825b82c..fb837c6259 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -6,9 +6,11 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.extensions.makePublicKey import com.tangem.domain.common.extensions.makeWalletManagerForApp import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import timber.log.Timber internal class WalletManagerFactory( @@ -34,6 +36,42 @@ internal class WalletManagerFactory( } } + suspend fun createWalletManagerForHot( + hotWallet: UserWallet.Hot, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? { + val curve = blockchain.getSupportedCurves().first() + val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } + ?: return null + return try { + val factory = blockchainSDKFactory.getWalletManagerFactorySync() ?: return null + + if (derivationPath == null) { + factory.createLegacyWalletManager( + blockchain = blockchain, + walletPublicKey = selectedWallet.publicKey, + curve = selectedWallet.curve, + ) + } else { + factory.createWalletManager( + blockchain = blockchain, + publicKey = makePublicKey( + seedKey = selectedWallet.publicKey, + blockchain = blockchain, + derivationPath = derivationPath, + derivedWalletKeys = selectedWallet.derivedKeys, + isWallet2 = true, + ) ?: return null, + curve = selectedWallet.curve, + ) + } + } catch (e: Throwable) { + Timber.w(e, "Failed to create wallet manager for $blockchain") + null + } + } + private fun getDerivationParams( derivationPath: DerivationPath?, derivationStyleProvider: DerivationStyleProvider, 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 4ee5733900..9e00d534b7 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 @@ -10,7 +10,6 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher 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.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.TokensFeatureToggles @@ -24,7 +23,6 @@ class SaveManagedTokensUseCase( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val derivationsRepository: DerivationsRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, @@ -119,20 +117,12 @@ class SaveManagedTokensUseCase( userWalletId: UserWalletId, existingCurrencies: List, ) { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) - } else { - stakingRepository.fetchMultiYieldBalance( + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - cryptoCurrencies = existingCurrencies, - refresh = true, - ) - } + currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { 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 96a60f0cc0..250a7f6b63 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 @@ -8,8 +8,6 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher 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 import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -27,11 +25,9 @@ class SaveMarketTokensUseCase( private val derivationsRepository: DerivationsRepository, private val marketsTokenRepository: MarketsTokenRepository, private val currenciesRepository: CurrenciesRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke( @@ -41,16 +37,15 @@ class SaveMarketTokensUseCase( removedNetworks: Set, ): Either = Either.catch { if (removedNetworks.isNotEmpty()) { - currenciesRepository.removeCurrencies( - userWalletId = userWalletId, - currencies = removedNetworks.mapNotNull { - marketsTokenRepository.createCryptoCurrency( - userWalletId = userWalletId, - token = tokenMarketParams, - network = it, - ) - }, - ) + val removedCurrencies = removedNetworks.mapNotNull { + marketsTokenRepository.createCryptoCurrency( + userWalletId = userWalletId, + token = tokenMarketParams, + network = it, + ) + } + + currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removedCurrencies) } if (addedNetworks.isNotEmpty()) { @@ -67,13 +62,16 @@ class SaveMarketTokensUseCase( ) } - currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = addedCurrencies) + val savedCurrencies = currenciesRepository.addCurrencies( + userWalletId = userWalletId, + currencies = addedCurrencies, + ) - refreshUpdatedNetworks(userWalletId, addedCurrencies) + refreshUpdatedNetworks(userWalletId, savedCurrencies) - refreshUpdatedYieldBalances(userWalletId, addedCurrencies) + refreshUpdatedYieldBalances(userWalletId, savedCurrencies) - refreshUpdatedQuotes(addedCurrencies) + refreshUpdatedQuotes(savedCurrencies) } } @@ -90,20 +88,12 @@ class SaveMarketTokensUseCase( userWalletId: UserWalletId, existingCurrencies: List, ) { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) - } else { - stakingRepository.fetchMultiYieldBalance( + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params( userWalletId = userWalletId, - cryptoCurrencies = existingCurrencies, - refresh = true, - ) - } + currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt index 777d3355d8..6c96413785 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/ArtworkModel.kt @@ -3,4 +3,22 @@ package com.tangem.domain.models data class ArtworkModel( val verifiedArtwork: ByteArray? = null, val defaultUrl: String, -) \ No newline at end of file +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ArtworkModel + + if (!verifiedArtwork.contentEquals(other.verifiedArtwork)) return false + if (defaultUrl != other.defaultUrl) return false + + return true + } + + override fun hashCode(): Int { + var result = verifiedArtwork?.contentHashCode() ?: 0 + result = 31 * result + defaultUrl.hashCode() + return result + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt new file mode 100644 index 0000000000..0417cea67b --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.common.card.EllipticCurve +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.serialization.MobileWalletAsStringSerializer +import kotlinx.serialization.Serializable + +@Serializable(with = MobileWalletAsStringSerializer::class) +@JsonClass(generateAdapter = true) +data class MobileWallet( + @Json(name = "publicKey") + val publicKey: ByteArray, + @Json(name = "chainCode") + val chainCode: ByteArray?, + @Json(name = "curve") + val curve: EllipticCurve, + @Json(name = "derivedKeys") + val derivedKeys: Map, +) { + + val extendedPublicKey: ExtendedPublicKey? + get() = chainCode?.let { + ExtendedPublicKey( + publicKey = publicKey, + chainCode = it, + ) + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/MobileWalletAsStringSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/MobileWalletAsStringSerializer.kt new file mode 100644 index 0000000000..26d5c38155 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/serialization/MobileWalletAsStringSerializer.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.models.serialization + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.json.TangemSdkAdapter +import com.tangem.domain.models.MobileWallet +import com.tangem.domain.models.scan.serialization.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal object MobileWalletAsStringSerializer : KSerializer { + + private val moshi = Moshi.Builder() + .add(WalletDerivedKeysMapAdapter()) + .add(ScanResponseDerivedKeysMapAdapter()) + .add(ByteArrayKeyAdapter()) + .add(ExtendedPublicKeysMapAdapter()) + .add(DerivationPathAdapterWithMigration()) + .add(TangemSdkAdapter.DateAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .addLast(KotlinJsonAdapterFactory()) + .build() + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MobileWallet", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): MobileWallet { + return moshi.adapter(MobileWallet::class.java).fromJson(decoder.decodeString())!! + } + + override fun serialize(encoder: Encoder, value: MobileWallet) { + encoder.encodeString(moshi.adapter(MobileWallet::class.java).toJson(value)!!) + } +} \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt index dde5f19064..59a0ddefa4 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt @@ -15,8 +15,8 @@ sealed class OnrampQuote { override val fromAmount: OnrampAmount, override val countryCode: String, val toAmount: OnrampAmount, - val minFromAmount: OnrampAmount, - val maxFromAmount: OnrampAmount, + val minFromAmount: OnrampAmount?, + val maxFromAmount: OnrampAmount?, ) : OnrampQuote() data class AmountError( diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt index a97a4a7161..af9fb4f3ec 100644 --- a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt @@ -6,4 +6,14 @@ data class QrResult( var address: String = "", var amount: BigDecimal? = null, var memo: String? = null, -) \ No newline at end of file +) + +data class RawQrResult( + val qrCode: String, + val resultSource: QrResultSource, + val requestSource: SourceType, +) + +enum class QrResultSource { + CLIPBOARD, CAMERA, GALLERY +} \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt index f24ef288bf..323e57d915 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt @@ -2,14 +2,15 @@ package com.tangem.domain.qrscanning.repository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.models.SourceType import kotlinx.coroutines.flow.Flow interface QrScanningEventsRepository { - suspend fun emitResult(type: SourceType, qrCode: String) + suspend fun emitResult(qrCode: RawQrResult) - fun subscribeToScanningResults(type: SourceType): Flow + fun subscribeToScanningResults(type: SourceType): Flow fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult } \ No newline at end of file diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt index ebe0e5980f..872a95af6c 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt @@ -3,15 +3,15 @@ package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository class EmitQrScannedEventUseCase( private val repository: QrScanningEventsRepository, ) { - suspend operator fun invoke(type: SourceType, qrCode: String): Either { + suspend operator fun invoke(qrCode: RawQrResult): Either { return try { - repository.emitResult(type, qrCode) + repository.emitResult(qrCode) Unit.right() } catch (e: Exception) { e.left() diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt index 3c42400420..dd1157a899 100644 --- a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt @@ -3,15 +3,25 @@ package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map class ListenToQrScanningUseCase( val repository: QrScanningEventsRepository, ) { operator fun invoke(type: SourceType): Either> { + return try { + repository.subscribeToScanningResults(type).map { it.qrCode }.right() + } catch (e: Exception) { + e.left() + } + } + + fun listen(type: SourceType): Either> { return try { repository.subscribeToScanningResults(type).right() } catch (e: Exception) { diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt index 182d5fbec6..189d86172e 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingAvailability.kt @@ -1,8 +1,10 @@ package com.tangem.domain.staking.model +import com.tangem.domain.staking.model.stakekit.Yield + sealed class StakingAvailability { - data class Available(val integrationId: String) : StakingAvailability() + data class Available(val yield: Yield) : StakingAvailability() data object Unavailable : StakingAvailability() diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index 2c61d8395f..a75bc31c60 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -6,12 +6,10 @@ import arrow.core.raise.either import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( - private val stakingRepository: StakingRepository, private val stakingErrorResolver: StakingErrorResolver, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, ) { @@ -19,27 +17,17 @@ class FetchStakingYieldBalanceUseCase( suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - isRefactoringEnabled: Boolean, - refresh: Boolean = false, ): Either { return either { catch( block = { - if (isRefactoringEnabled) { - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), - ) - } else { - stakingRepository.fetchSingleYieldBalance( + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params( userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - refresh = refresh, - ) - } + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ), + ) }, catch = { stakingErrorResolver.resolve(it) }, ) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt new file mode 100644 index 0000000000..34d0c03b27 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionRequirementAmountUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.staking.repositories.StakingRepository +import java.math.BigDecimal + +class GetActionRequirementAmountUseCase( + private val stakingRepository: StakingRepository, +) { + + operator fun invoke(integrationId: String, actionType: StakingActionType): Either = + Either.catch { + stakingRepository.getActionRequirementAmount(integrationId, actionType) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt index 59123af3b9..47cdf88900 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt @@ -21,5 +21,19 @@ interface MultiYieldBalanceFetcher : FlowFetcher, - ) + ) { + + override fun toString(): String { + val currencyIdWithNetworkMap = currencyIdWithNetworkMap.entries.joinToString { + "${it.key.value} - ${it.value}" + } + + return """ + MultiYieldBalanceFetcher.Params( + userWalletId = $userWalletId, + currencyIdWithNetworkMap: $currencyIdWithNetworkMap + ) + """.trimIndent() + } + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index e399fcd26c..b2ba1b88a7 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -14,17 +14,17 @@ import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import java.math.BigDecimal @Suppress("TooManyFunctions") interface StakingRepository { - fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String - fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? suspend fun fetchEnabledYields() @@ -49,37 +49,8 @@ interface StakingRepository { stakingActionStatus: StakingActionStatus, ): List - suspend fun fetchSingleYieldBalance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - refresh: Boolean = false, - ) - - fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow - - suspend fun getSingleYieldBalanceSyncLegacy( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): YieldBalance - suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance - suspend fun fetchMultiYieldBalance( - userWalletId: UserWalletId, - cryptoCurrencies: List, - refresh: Boolean = false, - ) - - fun getMultiYieldBalanceUpdates( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow - - suspend fun getMultiYieldBalanceSyncLegacy( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): YieldBalanceList - suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrencies: List, @@ -100,4 +71,9 @@ interface StakingRepository { fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean + + /** + * Return action requirement amount + */ + fun getActionRequirementAmount(integrationId: String, stakingActionType: StakingActionType): BigDecimal? } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt index 6b6391b82d..567a55c1ec 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt @@ -17,7 +17,18 @@ interface SingleYieldBalanceProducer : FlowProducer { val userWalletId: UserWalletId, val currencyId: CryptoCurrency.ID, val network: Network, - ) + ) { + + override fun toString(): String { + return """ + SingleYieldBalanceProducer.Params( + userWalletId = $userWalletId, + currencyId = $currencyId, + network = $network + ) + """.trimIndent() + } + } interface Factory : FlowProducer.Factory } \ No newline at end of file diff --git a/domain/swap/build.gradle.kts b/domain/swap/build.gradle.kts index c0231df3f5..2cfa89ac20 100644 --- a/domain/swap/build.gradle.kts +++ b/domain/swap/build.gradle.kts @@ -23,9 +23,11 @@ dependencies { /** Util */ implementation(projects.core.utils) + implementation(projects.core.datasource) /** Other */ implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) + implementation(deps.jodatime) } \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt new file mode 100644 index 0000000000..098e6cd413 --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDataModel.kt @@ -0,0 +1,47 @@ +package com.tangem.domain.swap.models + +import java.math.BigDecimal +import java.math.BigInteger + +data class SwapDataModel( + val toTokenAmount: BigDecimal, + val transaction: SwapDataTransactionModel, +) + +sealed class SwapDataTransactionModel { + + abstract val fromAmount: BigDecimal + abstract val toAmount: BigDecimal + abstract val txValue: String + abstract val txId: String + abstract val txTo: String + abstract val txExtraId: String? + + /** + * @param txValue amount for tx, should use native coin decimals, this value will send as native amount in tx + */ + data class DEX( + override val fromAmount: BigDecimal, + override val toAmount: BigDecimal, + override val txValue: String, + override val txId: String, + override val txTo: String, + override val txExtraId: String?, + val txFrom: String, + val txData: String, + val otherNativeFeeWei: BigDecimal?, + val gas: BigInteger, + ) : SwapDataTransactionModel() + + data class CEX( + override val fromAmount: BigDecimal, + override val toAmount: BigDecimal, + override val txValue: String, + override val txId: String, + override val txTo: String, + override val txExtraId: String?, + val externalTxId: String, + val externalTxUrl: String, + val txExtraIdName: String?, + ) : SwapDataTransactionModel() +} \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt index 9c60cc2ec0..4f51ce02f1 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapDirection.kt @@ -13,4 +13,12 @@ import com.tangem.domain.swap.models.SwapDirection.Reverse enum class SwapDirection { Direct, Reverse, + ; + + companion object { + inline fun SwapDirection.withSwapDirection(onDirect: () -> T, onReverse: () -> T): T = when (this) { + Direct -> onDirect() + Reverse -> onReverse() + } + } } \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapRefundData.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapRefundData.kt new file mode 100644 index 0000000000..7d9c82d65d --- /dev/null +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapRefundData.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.swap.models + +/** + * Refund data + * + * @param refundAddress address refund send to + * @param refundExtraId refund token id + */ +data class SwapRefundData( + val refundAddress: String? = null, + val refundExtraId: String? = null, +) \ 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 index 7bb2c091ba..a5124d565c 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -3,8 +3,10 @@ package com.tangem.domain.swap import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapDataModel import com.tangem.domain.swap.models.SwapPairModel import com.tangem.domain.swap.models.SwapQuoteModel +import com.tangem.domain.swap.models.SwapStatusModel import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import java.math.BigDecimal @@ -12,6 +14,7 @@ import java.math.BigDecimal /** * Swap repository */ +@Suppress("LongParameterList") interface SwapRepositoryV2 { /** @@ -44,7 +47,6 @@ interface SwapRepositoryV2 { * @param provider selected express provider * @param rateType rate type */ - @Suppress("LongParameterList") suspend fun getSwapQuote( userWallet: UserWallet, fromCryptoCurrency: CryptoCurrency, @@ -53,4 +55,52 @@ interface SwapRepositoryV2 { provider: ExpressProvider, rateType: ExpressRateType, ): SwapQuoteModel + + /** + * Returns swap data [SwapDataModel] ready to sign and send on selected quote + * + * @param userWallet selected user wallet + * @param fromCryptoCurrencyStatus currency status being swapped from + * @param toCryptoCurrencyStatus currency status being swapped to + * @param fromAmount swap amount + * @param toAddress destination address (optional, if null send to self) + * @param expressProvider selected swap provider + * @param rateType selected provider rate type + */ + suspend fun getSwapData( + userWallet: UserWallet, + fromCryptoCurrencyStatus: CryptoCurrencyStatus, + toCryptoCurrencyStatus: CryptoCurrencyStatus, + fromAmount: String, + toAddress: String?, + expressProvider: ExpressProvider, + rateType: ExpressRateType, + ): SwapDataModel + + /** + * Send ExpressApi info that swap transaction occurred + * + * @param userWallet selected user wallet + * @param fromCryptoCurrencyStatus currency status being swapped from + * @param toAddress swap destination address + * @param txId transaction id in ExpressApi + * @param txHash transaction hash in blockchain + * @param txExtraId extra transaction id in ExpressApi + */ + suspend fun swapTransactionSent( + userWallet: UserWallet, + fromCryptoCurrencyStatus: CryptoCurrencyStatus, + toAddress: String, + txId: String, + txHash: String, + txExtraId: String?, + ) + + /** + * Returns status [SwapStatusModel] on active swap + * + * @param userWallet selected user wallet + * @param txId transaction id in ExpressApi + */ + suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): SwapStatusModel } \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt new file mode 100644 index 0000000000..d1f98ede4c --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.swap.usecase + +import arrow.core.Either +import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.models.SwapDataModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +@Suppress("LongParameterList") +class GetSwapDataUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapErrorResolver: SwapErrorResolver, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + fromCryptoCurrencyStatus: CryptoCurrencyStatus, + fromAmount: String, + toCryptoCurrencyStatus: CryptoCurrencyStatus, + toAddress: String?, + expressProvider: ExpressProvider, + rateType: ExpressRateType, + ): Either = Either.catch { + swapRepositoryV2.getSwapData( + userWallet = userWallet, + fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, + fromAmount = fromAmount, + toCryptoCurrencyStatus = toCryptoCurrencyStatus, + toAddress = toAddress, + expressProvider = expressProvider, + rateType = rateType, + ) + }.mapLeft { throwable -> + swapErrorResolver.resolve(throwable) + } +} \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt new file mode 100644 index 0000000000..59ca0bb5d0 --- /dev/null +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -0,0 +1,67 @@ +package com.tangem.domain.swap.usecase + +import arrow.core.Either +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressProviderType.Companion.shouldStoreSwapTransaction +import com.tangem.domain.swap.SwapErrorResolver +import com.tangem.domain.swap.SwapRepositoryV2 +import com.tangem.domain.swap.SwapTransactionRepository +import com.tangem.domain.swap.models.SwapDataTransactionModel +import com.tangem.domain.swap.models.SwapStatus +import com.tangem.domain.swap.models.SwapStatusModel +import com.tangem.domain.swap.models.SwapTransactionModel +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +@Suppress("LongParameterList") +class SwapTransactionSentUseCase( + private val swapRepositoryV2: SwapRepositoryV2, + private val swapTransactionRepository: SwapTransactionRepository, + private val swapErrorResolver: SwapErrorResolver, +) { + + suspend operator fun invoke( + userWallet: UserWallet, + fromCryptoCurrencyStatus: CryptoCurrencyStatus, + toCryptoCurrencyStatus: CryptoCurrencyStatus, + swapDataTransactionModel: SwapDataTransactionModel, + provider: ExpressProvider, + txHash: String, + ) = Either.catch { + swapRepositoryV2.swapTransactionSent( + userWallet = userWallet, + fromCryptoCurrencyStatus = fromCryptoCurrencyStatus, + toAddress = swapDataTransactionModel.txTo, + txId = swapDataTransactionModel.txId, + txHash = txHash, + txExtraId = swapDataTransactionModel.txExtraId, + ) + if (provider.type.shouldStoreSwapTransaction()) { + val timestamp = System.currentTimeMillis() + swapTransactionRepository.storeTransaction( + userWalletId = userWallet.walletId, + fromCryptoCurrency = fromCryptoCurrencyStatus.currency, + toCryptoCurrency = toCryptoCurrencyStatus.currency, + transaction = SwapTransactionModel( + txId = swapDataTransactionModel.txId, + provider = provider, + timestamp = timestamp, + fromCryptoAmount = swapDataTransactionModel.fromAmount, + toCryptoAmount = swapDataTransactionModel.toAmount, + status = SwapStatusModel( + providerId = provider.providerId, + status = SwapStatus.New, + txId = swapDataTransactionModel.txId, + txExternalUrl = (swapDataTransactionModel as? SwapDataTransactionModel.CEX)?.externalTxUrl, + txExternalId = (swapDataTransactionModel as? SwapDataTransactionModel.CEX)?.externalTxId, + averageDuration = null, + ), + ), + ) + } + swapTransactionRepository.storeLastSwappedCryptoCurrencyId( + userWalletId = userWallet.walletId, + cryptoCurrencyId = toCryptoCurrencyStatus.currency.id, + ) + }.mapLeft(swapErrorResolver::resolve) +} \ No newline at end of file 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 48c2a1afd3..47c305d0bc 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 @@ -8,7 +8,6 @@ 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.MultiQuoteStatusFetcher -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -26,7 +25,6 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class AddCryptoCurrenciesUseCase( private val currenciesRepository: CurrenciesRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, @@ -156,21 +154,13 @@ class AddCryptoCurrenciesUseCase( } private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = addedCurrency.id, - network = addedCurrency.network, - ), - ) - } else { - stakingRepository.fetchSingleYieldBalance( + singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params( userWalletId = userWalletId, - cryptoCurrency = addedCurrency, - refresh = true, - ) - } + currencyId = addedCurrency.id, + network = addedCurrency.network, + ), + ) } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { 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 2d45549280..2697e84cfd 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 @@ -9,7 +9,6 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher 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 import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -17,14 +16,11 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -@Suppress("LongParameterList") class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, ) { suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { @@ -47,7 +43,6 @@ class FetchCardTokenListUseCase( fetchYieldBalances( userWalletId = userWalletId, currencies = currencies, - refresh = refresh, ) } awaitAll(fetchStatuses, fetchQuotes, yieldBalances) @@ -87,23 +82,12 @@ class FetchCardTokenListUseCase( ) } - private suspend fun fetchYieldBalances( - userWalletId: UserWalletId, - currencies: List, - refresh: Boolean, - ) { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) - } else { - catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, - catch = { /* Ignore error */ }, - ) - } + private suspend fun fetchYieldBalances(userWalletId: UserWalletId, currencies: List) { + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) } } \ No newline at end of file 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 1557ddcf64..d78af2a365 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 @@ -9,7 +9,6 @@ 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.MultiQuoteStatusFetcher -import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -29,7 +28,6 @@ import kotlinx.coroutines.coroutineScope @Suppress("LongParameterList") class FetchCurrencyStatusUseCase( private val currenciesRepository: CurrenciesRepository, - private val stakingRepository: StakingRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, @@ -42,14 +40,9 @@ class FetchCurrencyStatusUseCase( * * @param userWalletId The ID of the user's wallet. * @param id The ID of the cryptocurrency. - * @param refresh Indicates whether to force a refresh of the status data. * @return An [Either] representing success (Right) or an error (Left) in fetching the status. */ - suspend operator fun invoke( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - refresh: Boolean = false, - ): Either { + suspend operator fun invoke(userWalletId: UserWalletId, id: CryptoCurrency.ID): Either { return either { val currency = getCurrency(userWalletId, id) @@ -61,7 +54,7 @@ class FetchCurrencyStatusUseCase( val fetchQuote = async { fetchQuote(currencyId = currency.id) } val fetchStakingBalance = async { - fetchStakingBalance(userWalletId = userWalletId, cryptoCurrency = currency, refresh = refresh) + fetchStakingBalance(userWalletId = userWalletId, cryptoCurrency = currency) } awaitAll(fetchStatus, fetchQuote, fetchStakingBalance).summarizeResult() @@ -143,19 +136,14 @@ class FetchCurrencyStatusUseCase( private suspend fun fetchStakingBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - refresh: Boolean, ): Either { - return if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - singleYieldBalanceFetcher( - params = SingleYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), - ) - } else { - Either.catch { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) } - } + return singleYieldBalanceFetcher( + params = SingleYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ), + ) } private fun List>.summarizeResult(): Either { 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 7ca78e63e9..8738ad6266 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 @@ -11,7 +11,6 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher 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 import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -24,17 +23,12 @@ import kotlinx.coroutines.coroutineScope * network statuses, and quotes for tokens associated with a user's wallet. * * @param currenciesRepository The repository for retrieving currency-related data. - * @param stakingRepository The repository for retrieving staking-related data. */ -// TODO: Add tests -@Suppress("LongParameterList") class FetchTokenListUseCase( private val currenciesRepository: CurrenciesRepository, - private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, - private val tokensFeatureToggles: TokensFeatureToggles, ) { /** @@ -42,22 +36,17 @@ class FetchTokenListUseCase( * network statuses, and quotes for associated tokens. * * @param userWalletId The ID of the user's wallet. - * @param mode The refresh mode to control the fetching process. * @return An [Either] representing success (Right) or an error (Left) in fetching the token list. */ - suspend operator fun invoke( - userWalletId: UserWalletId, - mode: RefreshMode = RefreshMode.NONE, - ): Either = either { - val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies) + suspend operator fun invoke(userWalletId: UserWalletId): Either = either { + val currencies = fetchCurrencies(userWalletId) - invoke(userWalletId = userWalletId, currencies = currencies, mode = mode) + invoke(userWalletId = userWalletId, currencies = currencies) } suspend operator fun invoke( userWalletId: UserWalletId, currencies: List, - mode: RefreshMode = RefreshMode.NONE, ): Either = either { coroutineScope { val fetchStatuses = async { @@ -73,23 +62,16 @@ class FetchTokenListUseCase( } val yieldBalances = async { - fetchYieldBalances( - userWalletId = userWalletId, - currencies = currencies, - refresh = mode.refreshYieldBalances, - ) + fetchYieldBalances(userWalletId = userWalletId, currencies = currencies) } awaitAll(fetchStatuses, fetchQuotes, yieldBalances) } } - private suspend fun Raise.fetchCurrencies( - userWalletId: UserWalletId, - refresh: Boolean, - ): List { + private suspend fun Raise.fetchCurrencies(userWalletId: UserWalletId): List { val currencies = catch( - block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh) }, + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, true) }, ) { raise(TokenListError.DataError(it)) } @@ -121,48 +103,12 @@ class FetchTokenListUseCase( .bind() } - private suspend fun fetchYieldBalances( - userWalletId: UserWalletId, - currencies: List, - refresh: Boolean, - ) { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) - } else { - catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, - catch = { /* Ignore error */ }, - ) - } - } - - /** - * Represents the refresh modes available for fetching token list information. - */ - enum class RefreshMode( - internal val refreshCurrencies: Boolean, - internal val refreshQuotes: Boolean, - internal val refreshYieldBalances: Boolean, - ) { - NONE( - refreshCurrencies = false, - refreshQuotes = false, - refreshYieldBalances = false, - ), - FULL( - refreshCurrencies = true, - refreshQuotes = true, - refreshYieldBalances = true, - ), - SKIP_CURRENCIES( - refreshCurrencies = false, - refreshQuotes = true, - refreshYieldBalances = true, - ), + private suspend fun fetchYieldBalances(userWalletId: UserWalletId, currencies: List) { + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 1226448965..6db724ca58 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,463 +1,137 @@ package com.tangem.domain.tokens -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.promo.PromoRepository +import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.actions.CommonActionsFactory +import com.tangem.domain.tokens.actions.MissedDerivationsActionsFactory +import com.tangem.domain.tokens.actions.OutdatedDataActionsFactory +import com.tangem.domain.tokens.actions.UnreachableActionsFactory import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.isNullOrZero +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import kotlinx.coroutines.withTimeoutOrNull /** - * Use case to determine which TokenActions are available for a [CryptoCurrency] + * Use case for retrieving actions available for a specific cryptocurrency in a user's wallet. * - * @property rampManager Ramp manager to check ramp availability + * @param rampManager the manager for handling ramp state operations + * @param walletManagersFacade the facade for managing wallet operations + * @property stakingRepository the repository for staking-related data + * @property promoRepository the repository for promotional content + * @property dispatchers the coroutine dispatcher provider for managing concurrency */ -@Suppress("LongParameterList", "LargeClass") class GetCryptoCurrencyActionsUseCase( - private val rampManager: RampStateManager, - private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, + rampManager: RampStateManager, + walletManagersFacade: WalletManagersFacade, private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val dispatchers: CoroutineDispatcherProvider, - private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { - suspend operator fun invoke( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Flow { + private val unreachableActionsFactory = UnreachableActionsFactory( + walletManagersFacade = walletManagersFacade, + rampStateManager = rampManager, + ) + + private val outdatedDataActionsFactory = OutdatedDataActionsFactory( + walletManagersFacade = walletManagersFacade, + rampStateManager = rampManager, + ) + + private val commonActionsFactory = CommonActionsFactory( + walletManagersFacade = walletManagersFacade, + rampStateManager = rampManager, + ) + + operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { return when (userWallet) { - is UserWallet.Cold -> { - coldFlow(userWallet, cryptoCurrencyStatus) - } - is UserWallet.Hot -> { - TODO("[REDACTED_TASK_KEY]") - } + is UserWallet.Cold -> coldFlow(userWallet, cryptoCurrencyStatus) + is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") } } - private suspend fun coldFlow( + @OptIn(ExperimentalCoroutinesApi::class) + private fun coldFlow( userWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus, ): Flow { - val networkId = cryptoCurrencyStatus.currency.network.id - val requirements = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { - walletManagersFacade.getAssetRequirements(userWallet.walletId, cryptoCurrencyStatus.currency) - } - return flow { - val networkFlow = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenFlow(userWallet.walletId, networkId) - } else if (!userWallet.isMultiCurrency) { - currencyStatusOperations.getPrimaryCurrencyStatusFlow(userWallet.walletId, includeQuotes = false) - } else { - currencyStatusOperations.getNetworkCoinFlow( - userWalletId = userWallet.walletId, - networkId = networkId, - derivationPath = cryptoCurrencyStatus.currency.network.derivationPath, - includeQuotes = false, - ) - } - val flow = combine( - flow = networkFlow, - flow2 = promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id).conflate(), - flow3 = stakingRepository.getStakingAvailability( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - ).onStart { emit(StakingAvailability.Unavailable) }, - ) { maybeCoinStatus, maybeSwapStories, stakingAvailability -> - createTokenActionsState( - userWallet = userWallet, - coinStatus = maybeCoinStatus.getOrNull(), - cryptoCurrencyStatus = cryptoCurrencyStatus, - requirements = requirements, - shouldShowSwapStories = maybeSwapStories != null, - isStakingAvailable = stakingAvailability is StakingAvailability.Available, - ) - } - - emitAll(flow) - }.flowOn(dispatchers.io) - } - - private suspend fun createTokenActionsState( - userWallet: UserWallet, - coinStatus: CryptoCurrencyStatus?, - cryptoCurrencyStatus: CryptoCurrencyStatus, - requirements: AssetRequirementsCondition?, - shouldShowSwapStories: Boolean, - isStakingAvailable: Boolean, - ): TokenActionsState { - return TokenActionsState( - walletId = userWallet.walletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - states = createListOfActions( - userWallet = userWallet, - coinStatus = coinStatus, - cryptoCurrencyStatus = cryptoCurrencyStatus, - requirements = requirements, - shouldShowSwapStories = shouldShowSwapStories, - isStakingAvailable = isStakingAvailable, - ), - ) - } - - /** - * Creates list of action for expected order - * Actions priority: [Receive Send Swap Buy Sell] - */ - @Suppress("CyclomaticComplexMethod", "LongMethod") - private suspend fun createListOfActions( - userWallet: UserWallet, - coinStatus: CryptoCurrencyStatus?, - cryptoCurrencyStatus: CryptoCurrencyStatus, - requirements: AssetRequirementsCondition?, - shouldShowSwapStories: Boolean, - isStakingAvailable: Boolean, - ): List { - val cryptoCurrency = cryptoCurrencyStatus.currency - if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { - return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) - } - if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, requirements) - } - - if (cryptoCurrencyStatus.value.sources.total != StatusSource.ACTUAL) { - return getActionsForOutdatedData(userWallet, cryptoCurrencyStatus, requirements, isStakingAvailable) - } - - val activeList = mutableListOf() - val disabledList = mutableListOf() - - // markets - // not a custom token - if (cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { - activeList.add(TokenActionsState.ActionState.Analytics(ScenarioUnavailabilityReason.None)) - } - - // copy address - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) - } - - // receive - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - val scenario = getReceiveScenario(requirements) - activeList.add(TokenActionsState.ActionState.Receive(scenario)) - } - - // staking - addStakingActions(cryptoCurrency, isStakingAvailable, activeList, disabledList) - - // send - val sendUnavailabilityReason = getSendUnavailabilityReason( - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) - if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason)) - } else { - disabledList.add(TokenActionsState.ActionState.Send(sendUnavailabilityReason)) - } - - // swap - val swapActionState = getSwapUnavailabilityReason(userWallet, cryptoCurrencyStatus, shouldShowSwapStories) - if (swapActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(swapActionState) - } else { - disabledList.add(swapActionState) - } - - // buy - val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus) - if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(onrampActionState) - } else { - disabledList.add(onrampActionState) - } - - // region sell - rampManager.availableForSell(userWallet = userWallet, status = cryptoCurrencyStatus) - .onRight { - activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) - } - .onLeft { reason -> - disabledList.add(TokenActionsState.ActionState.Sell(reason)) - } - // endregion - - // hide - activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) - - return activeList + disabledList - } - - private suspend fun addStakingActions( - cryptoCurrency: CryptoCurrency, - isStakingAvailable: Boolean, - activeList: MutableList, - disabledList: MutableList, - ) { - if (isStakingAvailable) { - val yield = kotlin.runCatching { - stakingRepository.getYield( - cryptoCurrencyId = cryptoCurrency.id, - symbol = cryptoCurrency.symbol, - ) - }.getOrNull() - activeList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.None, - yield = yield, - ), - ) - } else { - disabledList.add( - TokenActionsState.ActionState.Stake( - unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), - yield = null, - ), - ) - } - } - - private suspend fun getActionsForUnreachableCurrency( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - requirements: AssetRequirementsCondition?, - ): List { - val activeList = mutableListOf() - val disabledList = mutableListOf() - - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) - } - - // buy (is not depend on cache) - val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus) - if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(onrampActionState) - } else { - disabledList.add(onrampActionState) - } - - disabledList.add(TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.Unreachable)) - disabledList.add( - TokenActionsState.ActionState.Swap( - unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, - showBadge = false, - ), - ) - disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - val scenario = getReceiveScenario(requirements) - activeList.add(TokenActionsState.ActionState.Receive(scenario)) - } - disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) - activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) - - return activeList + disabledList - } - - @Suppress("LongMethod") - private suspend fun getActionsForOutdatedData( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - requirements: AssetRequirementsCondition?, - isStakingAvailable: Boolean, - ): List { - val activeList = mutableListOf() - val disabledList = mutableListOf() - val cryptoCurrency = cryptoCurrencyStatus.currency - - // copy address - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - activeList.add(TokenActionsState.ActionState.CopyAddress(ScenarioUnavailabilityReason.None)) - } - - // receive - if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - val scenario = getReceiveScenario(requirements) - val action = TokenActionsState.ActionState.Receive(scenario) - if (scenario == ScenarioUnavailabilityReason.None) { - activeList.add(action) - } else { - disabledList.add(action) - } - } - - // swap - val sources = cryptoCurrencyStatus.value.sources - val isSwapAvailable = with(sources) { - quoteSource.isActual() && networkSource.isActual() - } - - val swapAction = TokenActionsState.ActionState.Swap( - unavailabilityReason = if (isSwapAvailable) { - ScenarioUnavailabilityReason.None - } else if (sources.networkSource == StatusSource.ONLY_CACHE) { - ScenarioUnavailabilityReason.UsedOutdatedData - } else { - // CACHE source always when loading - ScenarioUnavailabilityReason.DataLoading - }, - showBadge = false, - ) - if (swapAction.unavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(swapAction) - } else { - disabledList.add(swapAction) - } - - // buy (is not depend on cache) - val onrampActionState = getOnrampUnavailabilityReason(userWallet, cryptoCurrencyStatus) - if (onrampActionState.unavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(onrampActionState) - } else { - disabledList.add(onrampActionState) - } - - // staking - if (cryptoCurrencyStatus.value.sources.networkSource.isActual()) { - addStakingActions(cryptoCurrency, isStakingAvailable, activeList, disabledList) - } else { - disabledList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.UsedOutdatedData, null)) - } - - // send - val isSendAvailable = cryptoCurrencyStatus.value.sources.networkSource.isActual() - - val sendAction = TokenActionsState.ActionState.Send( - unavailabilityReason = if (isSendAvailable) { - ScenarioUnavailabilityReason.None - } else { - ScenarioUnavailabilityReason.UsedOutdatedData - }, - ) - if (sendAction.unavailabilityReason == ScenarioUnavailabilityReason.None) { - activeList.add(sendAction) - } else { - disabledList.add(sendAction) - } - - // region sell - if (isSendAvailable) { - activeList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None)) - } else { - disabledList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.UsedOutdatedData)) - } - // endregion - - // hide - activeList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) - - return activeList + disabledList - } - - private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason { - return when (requirements) { - AssetRequirementsCondition.PaidTransaction, - is AssetRequirementsCondition.PaidTransactionWithFee, - -> ScenarioUnavailabilityReason.UnassociatedAsset - is AssetRequirementsCondition.IncompleteTransaction, - null, - -> ScenarioUnavailabilityReason.None - is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired - } - } - - private fun getSendUnavailabilityReason( - cryptoCurrencyStatus: CryptoCurrencyStatus, - coinStatus: CryptoCurrencyStatus?, - ): ScenarioUnavailabilityReason { return when { - cryptoCurrencyStatus.value.amount.isNullOrZero() -> { - ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) + cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation -> { + flowOf(value = MissedDerivationsActionsFactory.create()) } - currenciesRepository.isSendBlockedByPendingTransactions( - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) -> { - ScenarioUnavailabilityReason.PendingTransaction( - withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND, - networkName = coinStatus?.currency?.network?.name.orEmpty(), + cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable -> { + flow { + val actions = unreachableActionsFactory.create( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + + emit(actions) + } + } + cryptoCurrencyStatus.value.sources.total != StatusSource.ACTUAL -> { + getStakingAvailabilityFlow( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, ) + .mapLatest { + outdatedDataActionsFactory.create( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingAvailability = it, + ) + } } else -> { - ScenarioUnavailabilityReason.None + combine( + flow = getStakingAvailabilityFlow( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + ), + flow2 = getSwapStoryContent(), + ) { stakingAvailability, swapStoryContent -> + commonActionsFactory.create( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingAvailability = stakingAvailability, + shouldShowSwapStories = swapStoryContent != null, + ) + } } } - } - - private suspend fun getSwapUnavailabilityReason( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - shouldShowSwapStories: Boolean, - ): TokenActionsState.ActionState { - val cryptoCurrency = cryptoCurrencyStatus.currency - val isMultiCurrency = - userWallet is UserWallet.Hot || userWallet is UserWallet.Cold && userWallet.isMultiCurrency - - return if (isMultiCurrency) { - if (cryptoCurrency.isCustom) { - return TokenActionsState.ActionState.Swap( - unavailabilityReason = ScenarioUnavailabilityReason.CustomToken(cryptoCurrency.name), - showBadge = false, + .map { + TokenActionsState( + walletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + states = it.toList(), ) } - if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.NoQuote) { - return TokenActionsState.ActionState.Swap( - unavailabilityReason = ScenarioUnavailabilityReason.TokenNoQuotes(cryptoCurrency.name), - showBadge = false, - ) - } - val reason = rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) - val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories - TokenActionsState.ActionState.Swap( - unavailabilityReason = reason, - showBadge = isShowBadge, - ) - } else { - TokenActionsState.ActionState.Swap( - unavailabilityReason = ScenarioUnavailabilityReason.SingleWallet, - showBadge = false, - ) - } + .flowOn(dispatchers.default) } - private suspend fun getOnrampUnavailabilityReason( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): TokenActionsState.ActionState { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - - val cryptoCurrency = cryptoCurrencyStatus.currency - val reason = rampManager.availableForBuy(userWallet.scanResponse, userWallet.walletId, cryptoCurrency) - return TokenActionsState.ActionState.Buy(unavailabilityReason = reason) + private fun getStakingAvailabilityFlow( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow { + return stakingRepository.getStakingAvailability(userWalletId = userWalletId, cryptoCurrency = currency) + .onStart { emit(StakingAvailability.Unavailable) } + .conflate() + .distinctUntilChanged() } - private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { - return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() - } - - private companion object { - const val REQUEST_EXCHANGE_DATA_TIMEOUT = 1000L + private fun getSwapStoryContent(): Flow { + return promoRepository.getStoryById(StoryContentIds.STORY_FIRST_TIME_SWAP.id) + .conflate() + .distinctUntilChanged() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 4ecbcaebcb..7ba76d1281 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -10,7 +10,7 @@ import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet +import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -44,11 +44,10 @@ class GetNetworkCoinStatusUseCase( derivationPath: Network.DerivationPath, ): Either { val userWalletId = userWallet.walletId - val cardTypesResolver = userWallet.requireColdWallet().cardTypesResolver // TODO [REDACTED_TASK_KEY] val maybeCurrency = if (userWallet.isMultiCurrency) { currencyStatusOperations.getNetworkCoinSync(userWalletId, networkId, derivationPath) - } else if (cardTypesResolver.isSingleWalletWithToken()) { + } else if (userWallet is UserWallet.Cold && userWallet.cardTypesResolver.isSingleWalletWithToken()) { currencyStatusOperations.getNetworkCoinForSingleWalletWithTokenSync(userWalletId, networkId) } else { currencyStatusOperations.getPrimaryCurrencyStatusSync(userWalletId) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index 50ae6bb491..5add2d0b65 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -7,7 +7,5 @@ package com.tangem.domain.tokens */ interface TokensFeatureToggles { - val isStakingLoadingRefactoringEnabled: Boolean - val isWalletBalanceFetcherEnabled: Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/ActionAvailabilityBuilder.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/ActionAvailabilityBuilder.kt new file mode 100644 index 0000000000..8b9bbfce83 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/ActionAvailabilityBuilder.kt @@ -0,0 +1,62 @@ +package com.tangem.domain.tokens.actions + +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState + +/** + * Builder for creating a set of [TokenActionsState.ActionState] based on their availability + * +[REDACTED_AUTHOR] + */ +internal class ActionAvailabilityBuilder { + + private val activeList = mutableSetOf() + private val disabledList = mutableSetOf() + + /** Marks the current [TokenActionsState.ActionState] as active */ + fun TokenActionsState.ActionState.active() { + activeList.add(this) + } + + /** Marks the current [TokenActionsState.ActionState] as disabled */ + fun TokenActionsState.ActionState.disabled() { + disabledList.add(this) + } + + /** Marks a list of [TokenActionsState.ActionState] as disabled */ + fun List.disabled() { + disabledList.addAll(this) + } + + /** + * Adds the current [TokenActionsState.ActionState] to the appropriate list based on its [ScenarioUnavailabilityReason]. + * + * If the [ScenarioUnavailabilityReason] is [ScenarioUnavailabilityReason.None], the action is added to the active list. + * Otherwise, it is added to the disabled list. + */ + fun TokenActionsState.ActionState.addByReason() { + if (unavailabilityReason == ScenarioUnavailabilityReason.None) { + activeList.add(this) + } else { + disabledList.add(this) + } + } + + fun build(): Set { + return activeList + disabledList + } +} + +/** + * This function initializes an [ActionAvailabilityBuilder], applies the given + * [block] to it, and returns the resulting set of [TokenActionsState.ActionState] + */ +internal suspend fun actionAvailabilityBuilder( + block: suspend ActionAvailabilityBuilder.() -> Unit, +): Set { + val builder = ActionAvailabilityBuilder() + + builder.block() + + return builder.build() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt new file mode 100644 index 0000000000..407d308fa6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -0,0 +1,187 @@ +package com.tangem.domain.tokens.actions + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState.ActionState +import com.tangem.domain.transaction.models.AssetRequirementsCondition +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.withTimeoutOrNull + +/** + * Base factory class for creating token actions. + * + * This class provides utility methods to determine the availability of actions and to create specific token actions + * based on the provided conditions. + * + * @param walletManagersFacade the facade for managing wallet operations + * @param rampStateManager the manager for handling ramp state operations + * +[REDACTED_AUTHOR] + */ +internal open class BaseActionsFactory( + private val walletManagersFacade: WalletManagersFacade, + private val rampStateManager: RampStateManager, +) { + + /** Checks if the provided network address [networkAddress] is available */ + protected fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { + return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() + } + + /** + * Retrieves the asset requirements for a specific user wallet and cryptocurrency. + * + * @param userWalletId The ID of the user wallet. + * @param currency The cryptocurrency to check. + * @return The asset requirements condition, or `null` if the operation times out. + */ + protected suspend fun getAssetRequirements( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): AssetRequirementsCondition? { + return withTimeoutOrNull(timeMillis = 1000L) { + walletManagersFacade.getAssetRequirements(userWalletId = userWalletId, currency = currency) + } + } + + /** + * Determines the unavailability reason for the BUY action + * + * @param userWallet the user's cold wallet + * @param currency the cryptocurrency to check + */ + protected suspend fun getOnrampUnavailabilityReason( + userWallet: UserWallet.Cold, + currency: CryptoCurrency, + ): ScenarioUnavailabilityReason { + return rampStateManager.availableForBuy( + userWallet = userWallet, + cryptoCurrency = currency, + ) + } + + /** + * Determines the unavailability reason for the SEND action + * + * @param userWalletId the ID of the user's wallet + * @param cryptoCurrencyStatus the status of the cryptocurrency + */ + protected suspend fun getSendUnavailabilityReason( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): ScenarioUnavailabilityReason { + return rampStateManager.getSendUnavailabilityReason( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + } + + /** + * Determines the unavailability reason for the SELL action + * + * @param userWalletId the ID of the user's wallet + * @param status the status of the cryptocurrency + * @param sendUnavailabilityReason the reason for unavailability of the send action + */ + protected suspend fun getSellUnavailabilityReason( + userWalletId: UserWalletId, + status: CryptoCurrencyStatus, + sendUnavailabilityReason: ScenarioUnavailabilityReason, + ): ScenarioUnavailabilityReason { + return rampStateManager.availableForSell( + userWalletId = userWalletId, + status = status, + sendUnavailabilityReason = sendUnavailabilityReason, + ).fold( + ifLeft = { it }, + ifRight = { ScenarioUnavailabilityReason.None }, + ) + } + + /** Adds a "Copy Address" action to the builder if the address is available [isAddressAvailable] */ + protected fun ActionAvailabilityBuilder.addCopyAction(isAddressAvailable: Boolean) { + if (isAddressAvailable) { + ActionState.CopyAddress(unavailabilityReason = ScenarioUnavailabilityReason.None).active() + } + } + + /** + * Adds a "Receive" action to the builder based on the address availability and asset requirements + * + * @param isAddressAvailable indicates whether the address is available + * @param requirementsDeferred a deferred object containing the asset requirements condition + */ + protected suspend fun ActionAvailabilityBuilder.addReceiveAction( + isAddressAvailable: Boolean, + requirementsDeferred: Deferred?, + ) { + if (isAddressAvailable && requirementsDeferred != null) { + val scenario = getReceiveScenario(requirements = requirementsDeferred.await()) + val action = ActionState.Receive(scenario) + + if (scenario == ScenarioUnavailabilityReason.None) { + action.active() + } else { + action.disabled() + } + } + } + + /** Adds a "Buy" action to the builder based on the unavailability [reason] */ + protected fun ActionAvailabilityBuilder.addBuyAction(reason: ScenarioUnavailabilityReason) { + val action = ActionState.Buy(unavailabilityReason = reason) + + if (reason == ScenarioUnavailabilityReason.None) { + action.active() + } else { + action.disabled() + } + } + + /** Adds a "Hide Token" action to the builder */ + protected fun ActionAvailabilityBuilder.addHideTokenAction() { + ActionState.HideToken(unavailabilityReason = ScenarioUnavailabilityReason.None).active() + } + + /** + * Creates a staking action based on the staking availability + * + * @param currency the cryptocurrency for staking + * @param stakingAvailability the staking availability status + */ + protected fun createStakingAction( + currency: CryptoCurrency, + stakingAvailability: StakingAvailability, + ): ActionState.Stake { + return if (stakingAvailability is StakingAvailability.Available) { + ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.None, + yield = stakingAvailability.yield, + ) + } else { + ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(currency.name), + yield = null, + ) + } + } + + private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason { + return when (requirements) { + AssetRequirementsCondition.PaidTransaction, + is AssetRequirementsCondition.PaidTransactionWithFee, + -> ScenarioUnavailabilityReason.UnassociatedAsset + is AssetRequirementsCondition.IncompleteTransaction, + null, + -> ScenarioUnavailabilityReason.None + is AssetRequirementsCondition.RequiredTrustline -> ScenarioUnavailabilityReason.TrustlineRequired + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt new file mode 100644 index 0000000000..fab3e19501 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -0,0 +1,176 @@ +package com.tangem.domain.tokens.actions + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState.ActionState +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** + * Factory class for creating common token actions + * + * @param walletManagersFacade the facade for managing wallet operations + * @param rampStateManager the manager for handling ramp state operations + * +[REDACTED_AUTHOR] + */ +internal class CommonActionsFactory( + walletManagersFacade: WalletManagersFacade, + private val rampStateManager: RampStateManager, +) : BaseActionsFactory(walletManagersFacade, rampStateManager) { + + /** + * Creates a set of token actions based on the provided parameters + * + * @param userWallet the user's cold wallet + * @param cryptoCurrencyStatus the status of the cryptocurrency + * @param stakingAvailability the staking availability for the cryptocurrency + * @param shouldShowSwapStories a flag indicating whether to show swap stories + */ + suspend fun create( + userWallet: UserWallet.Cold, + cryptoCurrencyStatus: CryptoCurrencyStatus, + stakingAvailability: StakingAvailability, + shouldShowSwapStories: Boolean, + ): Set = coroutineScope { + val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress) + + val requirementsDeferred = if (isAddressAvailable) { + async { + getAssetRequirements(userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency) + } + } else { + null + } + + val onrampUnavailabilityReasonDeferred = async { + getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency) + } + + val sendUnavailabilityReasonDeferred = async { + getSendUnavailabilityReason(userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus) + } + + val swapUnavailabilityReason = if (!cryptoCurrencyStatus.currency.isCustom && + cryptoCurrencyStatus.value !is CryptoCurrencyStatus.NoQuote + ) { + async { + getSwapUnavailabilityReason( + userWalletId = userWallet.walletId, + currency = cryptoCurrencyStatus.currency, + ) + } + } else { + null + } + + actionAvailabilityBuilder { + // region Analytics + if (cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { + ActionState.Analytics(unavailabilityReason = ScenarioUnavailabilityReason.None).active() + } + // endregion + + // region Copy + addCopyAction(isAddressAvailable = isAddressAvailable) + // endregion + + // region Receive + addReceiveAction(isAddressAvailable = isAddressAvailable, requirementsDeferred = requirementsDeferred) + // endregion + + // region Stake + createStakingAction(currency = cryptoCurrencyStatus.currency, stakingAvailability = stakingAvailability) + .addByReason() + // endregion + + val sendUnavailabilityReason = sendUnavailabilityReasonDeferred.await() + + // region Send + ActionState.Send(unavailabilityReason = sendUnavailabilityReason).addByReason() + // endregion + + // region Swap + createSwapAction( + userWallet = userWallet, + cryptoCurrencyStatus = cryptoCurrencyStatus, + swapUnavailableReasonDeferred = swapUnavailabilityReason, + shouldShowSwapStories = shouldShowSwapStories, + ).addByReason() + // endregion + + // region Buy + addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + // endregion + + // region Sell + val sellUnavailabilityReason = getSellUnavailabilityReason( + userWalletId = userWallet.walletId, + status = cryptoCurrencyStatus, + sendUnavailabilityReason = sendUnavailabilityReason, + ) + + ActionState.Sell(unavailabilityReason = sellUnavailabilityReason).addByReason() + // endregion + + // region HideToken + addHideTokenAction() + // endregion + } + } + + private suspend fun createSwapAction( + userWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus, + swapUnavailableReasonDeferred: Deferred?, + shouldShowSwapStories: Boolean, + ): ActionState { + val cryptoCurrency = cryptoCurrencyStatus.currency + val isMultiCurrency = userWallet is UserWallet.Cold && userWallet.isMultiCurrency || + userWallet is UserWallet.Hot + + if (!isMultiCurrency) { + return ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.SingleWallet, + showBadge = false, + ) + } + + return when { + cryptoCurrency.isCustom -> { + ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.CustomToken(cryptoCurrency.name), + showBadge = false, + ) + } + cryptoCurrencyStatus.value is CryptoCurrencyStatus.NoQuote -> { + ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.TokenNoQuotes(cryptoCurrency.name), + showBadge = false, + ) + } + else -> { + val reason = swapUnavailableReasonDeferred!!.await() + + return ActionState.Swap( + unavailabilityReason = reason, + showBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories, + ) + } + } + } + + private suspend fun getSwapUnavailabilityReason( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): ScenarioUnavailabilityReason { + return rampStateManager.availableForSwap(userWalletId = userWalletId, cryptoCurrency = currency) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/MissedDerivationsActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/MissedDerivationsActionsFactory.kt new file mode 100644 index 0000000000..99bad15a0c --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/MissedDerivationsActionsFactory.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.tokens.actions + +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState.ActionState + +/** + * Factory for creating a set of token action states for missed derivations + * +[REDACTED_AUTHOR] + */ +internal object MissedDerivationsActionsFactory { + + /** Creates a set of token actions */ + fun create(): Set { + val action = ActionState.HideToken(unavailabilityReason = ScenarioUnavailabilityReason.None) + + return setOf(action) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt new file mode 100644 index 0000000000..68530436ec --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -0,0 +1,154 @@ +package com.tangem.domain.tokens.actions + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState.ActionState +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** + * Factory for creating a set of token action states when data is outdated + * + * @param walletManagersFacade the facade for managing wallet operations + * @param rampStateManager the manager for handling ramp state operations + * +[REDACTED_AUTHOR] + */ +internal class OutdatedDataActionsFactory( + walletManagersFacade: WalletManagersFacade, + rampStateManager: RampStateManager, +) : BaseActionsFactory(walletManagersFacade, rampStateManager) { + + /** + * Creates a set of token actions based on the provided parameters + * + * @param userWallet the user's cold wallet + * @param cryptoCurrencyStatus the status of the cryptocurrency + * @param stakingAvailability the staking availability for the cryptocurrency + */ + suspend fun create( + userWallet: UserWallet.Cold, + cryptoCurrencyStatus: CryptoCurrencyStatus, + stakingAvailability: StakingAvailability, + ): Set = coroutineScope { + val sources = cryptoCurrencyStatus.value.sources + + val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress) + + val requirementsDeferred = if (isAddressAvailable) { + async { + getAssetRequirements(userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency) + } + } else { + null + } + + val onrampUnavailabilityReasonDeferred = async { + getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency) + } + + val sendUnavailabilityReasonDeferred = if (sources.networkSource == StatusSource.ACTUAL) { + async { + getSendUnavailabilityReason( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + } + } else { + null + } + + actionAvailabilityBuilder { + // region Copy + addCopyAction(isAddressAvailable = isAddressAvailable) + // endregion + + // region Receive + addReceiveAction(isAddressAvailable = isAddressAvailable, requirementsDeferred = requirementsDeferred) + // endregion + + // region Swap + createSwapAction(sources = sources).addByReason() + // endregion + + // region Buy + addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + // endregion + + // region Stake + if (sources.networkSource.isActual()) { + val stakingAction = createStakingAction( + currency = cryptoCurrencyStatus.currency, + stakingAvailability = stakingAvailability, + ) + + stakingAction.addByReason() + } else { + val stakingAction = ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.UsedOutdatedData, + yield = null, + ) + + stakingAction.disabled() + } + // endregion + + val sendUnavailabilityReason = getSendUnavailabilityReason( + sources = sources, + reasonDeferred = sendUnavailabilityReasonDeferred, + ) + + // region Send + ActionState.Send(sendUnavailabilityReason).addByReason() + // endregion + + // region Sell + if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) { + val sellUnavailabilityReason = getSellUnavailabilityReason( + userWalletId = userWallet.walletId, + status = cryptoCurrencyStatus, + sendUnavailabilityReason = sendUnavailabilityReason, + ) + + ActionState.Sell(sellUnavailabilityReason).addByReason() + } else { + ActionState.Sell(sendUnavailabilityReason).disabled() + } + // endregion + + // region HideToken + addHideTokenAction() + // endregion + } + } + + private fun createSwapAction(sources: CryptoCurrencyStatus.Sources): ActionState { + val isSwapAvailable = with(sources) { quoteSource.isActual() && networkSource.isActual() } + + return ActionState.Swap( + unavailabilityReason = when { + isSwapAvailable -> ScenarioUnavailabilityReason.None + sources.networkSource == StatusSource.ONLY_CACHE -> ScenarioUnavailabilityReason.UsedOutdatedData + else -> ScenarioUnavailabilityReason.DataLoading // CACHE source always when loading + }, + showBadge = false, + ) + } + + private suspend fun getSendUnavailabilityReason( + sources: CryptoCurrencyStatus.Sources, + reasonDeferred: Deferred?, + ): ScenarioUnavailabilityReason { + if (sources.networkSource != StatusSource.ACTUAL || reasonDeferred == null) { + return ScenarioUnavailabilityReason.UsedOutdatedData + } + + return reasonDeferred.await() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt new file mode 100644 index 0000000000..9ccbd4b946 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -0,0 +1,73 @@ +package com.tangem.domain.tokens.actions + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState.ActionState +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +/** + * Factory for creating a set of unreachable token action states + * + * @param walletManagersFacade the facade for managing wallet operations + * @param rampStateManager the manager for handling ramp state operations + * +[REDACTED_AUTHOR] + */ +internal class UnreachableActionsFactory( + walletManagersFacade: WalletManagersFacade, + rampStateManager: RampStateManager, +) : BaseActionsFactory(walletManagersFacade, rampStateManager) { + + suspend fun create(userWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus): Set = + coroutineScope { + val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress) + + // region Deferred + val requirementsDeferred = if (isAddressAvailable) { + async { + getAssetRequirements(userWalletId = userWallet.walletId, currency = cryptoCurrencyStatus.currency) + } + } else { + null + } + + val onrampUnavailabilityReasonDeferred = async { + getOnrampUnavailabilityReason(userWallet = userWallet, currency = cryptoCurrencyStatus.currency) + } + // endregion + + actionAvailabilityBuilder { + // region Copy + addCopyAction(isAddressAvailable = isAddressAvailable) + // endregion + + // region Buy + addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + // endregion + + // region Receive + addReceiveAction(isAddressAvailable = isAddressAvailable, requirementsDeferred = requirementsDeferred) + // endregion + + // region Send, Swap, Sell, Stake + listOf( + ActionState.Send(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable), + ActionState.Swap( + unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, + showBadge = false, + ), + ActionState.Sell(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable), + ActionState.Stake(unavailabilityReason = ScenarioUnavailabilityReason.Unreachable, yield = null), + ).disabled() + // endregion + + // region HideToken + addHideTokenAction() + // endregion + } + } +} \ No newline at end of file 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 c2fd5396bb..ae7d98a606 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 @@ -234,8 +234,7 @@ abstract class BaseCurrencyStatusOperations( userWalletId = userWalletId, currency = currency, includeQuotes = includeQuotes, - // If toggle is off, then subscribe on yield balance. If toggle is on, then don't - subscribeOnYieldBalance = !tokensFeatureToggles.isStakingLoadingRefactoringEnabled, + subscribeOnYieldBalance = false, ) } @@ -340,17 +339,13 @@ abstract class BaseCurrencyStatusOperations( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): EitherFlow { - return if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - singleYieldBalanceSupplier( - params = SingleYieldBalanceProducer.Params( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ), - ) - } else { - stakingRepository.getSingleYieldBalanceFlow(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) - } + return singleYieldBalanceSupplier( + params = SingleYieldBalanceProducer.Params( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ), + ) .map> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyYieldBalances.left()) } @@ -404,18 +399,10 @@ abstract class BaseCurrencyStatusOperations( ): Either { return catch( block = { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - stakingRepository.getMultiYieldBalanceSync( - userWalletId = userWalletId, - cryptoCurrencies = cryptoCurrencies, - ) - } else { - stakingRepository.getMultiYieldBalanceSyncLegacy( - userWalletId = userWalletId, - cryptoCurrencies = cryptoCurrencies, - ) - } - .right() + stakingRepository.getMultiYieldBalanceSync( + userWalletId = userWalletId, + cryptoCurrencies = cryptoCurrencies, + ).right() }, catch = { Error.EmptyYieldBalances.left() @@ -428,14 +415,7 @@ abstract class BaseCurrencyStatusOperations( cryptoCurrency: CryptoCurrency, ): Either { return catch( - block = { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - stakingRepository.getSingleYieldBalanceSync(userWalletId, cryptoCurrency) - } else { - stakingRepository.getSingleYieldBalanceSyncLegacy(userWalletId, cryptoCurrency) - } - .right() - }, + block = { stakingRepository.getSingleYieldBalanceSync(userWalletId, cryptoCurrency).right() }, catch = { Error.EmptyYieldBalances.left() }, 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 6802d2efe6..142079eee0 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 @@ -162,7 +162,7 @@ class CachedCurrenciesStatusesOperations( combine( flow = getQuotes(currenciesIds), flow2 = getNetworkStatusesUpdates(userWalletId, networks), - flow3 = getYieldBalances(userWalletId, currencies), + flow3 = getYieldsBalancesUpdates(userWalletId, currencies), flow4 = fetchingState.map { val state = it[userWalletId] ?: return@map false @@ -213,16 +213,12 @@ class CachedCurrenciesStatusesOperations( ) }, async { - if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - multiYieldBalanceFetcher( - params = MultiYieldBalanceFetcher.Params( - userWalletId = userWalletId, - currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, - ), - ) - } else { - stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) - } + multiYieldBalanceFetcher( + params = MultiYieldBalanceFetcher.Params( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) }, ) } @@ -308,25 +304,6 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } - private fun getYieldBalances( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): EitherFlow { - return if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { - getYieldsBalancesUpdates(userWalletId, cryptoCurrencies) - } else { - stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies) - .map> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_DELAY) - true - } - .distinctUntilChanged() - } - } - // temporary code because token list is built using networks list @OptIn(FlowPreview::class) private fun getNetworkStatusesUpdates( @@ -433,7 +410,7 @@ class CachedCurrenciesStatusesOperations( } private fun isFetchingStarted(userWalletId: UserWalletId): Boolean { - return fetchingState.value[userWalletId]?.let { it.isStarted() || it.isFinished() } ?: false + return fetchingState.value[userWalletId]?.let { it.isStarted() || it.isFinished() } == true } private fun setFetchStarted(userWalletId: UserWalletId) { @@ -460,7 +437,6 @@ class CachedCurrenciesStatusesOperations( } companion object { - internal const val RETRY_DELAY = 2000L private val fetchingState = MutableStateFlow(value = emptyMap()) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 63bb8891bd..9225d49a40 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -50,7 +50,7 @@ interface CurrenciesRepository { * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet * ID provided. */ - suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) + suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List): List /** * Removes currency from a specific user wallet. @@ -223,12 +223,12 @@ interface CurrenciesRepository { /** * Determines whether the currency sending is blocked by network pending transaction * + * @param userWalletId the unique identifier of the user wallet * @param cryptoCurrencyStatus currency status - * @param coinStatus main currency status in [cryptoCurrencyStatus] network */ - fun isSendBlockedByPendingTransactions( + suspend fun isSendBlockedByPendingTransactions( + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - coinStatus: CryptoCurrencyStatus?, ): Boolean /** @@ -265,5 +265,5 @@ interface CurrenciesRepository { suspend fun syncTokens(userWalletId: UserWalletId) @Throws - fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver + fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index ef5b7253e7..8750af2aa1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -76,7 +76,7 @@ class WalletBalanceFetcher internal constructor( val cardTypesResolver = currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) val fetcher = when { - cardTypesResolver.isMultiwalletAllowed() -> multiWalletBalanceFetcher + cardTypesResolver == null || cardTypesResolver.isMultiwalletAllowed() -> multiWalletBalanceFetcher cardTypesResolver.isSingleWalletWithToken() -> singleWalletWithTokenBalanceFetcher cardTypesResolver.isSingleWallet() -> singleWalletBalanceFetcher else -> error("Unknown type of wallet: $userWalletId") diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 475ea789ec..bdff6776cf 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -48,7 +48,10 @@ internal class MockCurrenciesRepository( override suspend fun saveNewCurrenciesList(userWalletId: UserWalletId, currencies: List) = Unit - override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) = Unit + override suspend fun addCurrencies( + userWalletId: UserWalletId, + currencies: List, + ): List = emptyList() override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) { removeCurrencyResult.onLeft { throw it } @@ -131,9 +134,9 @@ internal class MockCurrenciesRepository( return isSortedByBalance.map { it.getOrElse { e -> throw e } } } - override fun isSendBlockedByPendingTransactions( + override suspend fun isSendBlockedByPendingTransactions( + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, - coinStatus: CryptoCurrencyStatus?, ): Boolean { return false } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt deleted file mode 100644 index 303da59cda..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ /dev/null @@ -1,266 +0,0 @@ -package com.tangem.domain.tokens.repository - -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionStatus -import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.* -import com.tangem.domain.staking.model.stakekit.action.StakingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.domain.staking.model.stakekit.action.StakingActionType -import com.tangem.domain.staking.model.stakekit.transaction.* -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.flowOf -import org.joda.time.DateTime -import java.math.BigDecimal - -class MockStakingRepository : StakingRepository { - - override fun getSupportedIntegrationId(cryptoCurrencyId: CryptoCurrency.ID): String? = null - - override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = "" - - override suspend fun fetchEnabledYields() { - /* no-op */ - } - - override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo = - StakingEntryInfo( - apr = 1.toBigDecimal(), - tokenSymbol = "SOL", - rewardSchedule = Yield.Metadata.RewardSchedule.DAY, - ) - - override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = yield - - override suspend fun getYield(yieldId: String) = yield - - override fun getStakingAvailability( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): Flow = flowOf(StakingAvailability.Unavailable) - - override suspend fun getStakingAvailabilitySync( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): StakingAvailability = StakingAvailability.Unavailable - - override suspend fun getActions( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - networkType: NetworkType, - stakingActionStatus: StakingActionStatus, - ): List { - return emptyList() - } - - override suspend fun fetchSingleYieldBalance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - refresh: Boolean, - ) { - /* no-op */ - } - - override fun getSingleYieldBalanceFlow( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): Flow = channelFlow { - send(YieldBalance.Error(integrationId = null, address = null)) - } - - override suspend fun getSingleYieldBalanceSyncLegacy( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): YieldBalance = YieldBalance.Error(integrationId = null, address = null) - - override suspend fun getSingleYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): YieldBalance = YieldBalance.Error(integrationId = null, address = null) - - override suspend fun fetchMultiYieldBalance( - userWalletId: UserWalletId, - cryptoCurrencies: List, - refresh: Boolean, - ) { - /* no-op */ - } - - override fun getMultiYieldBalanceUpdates( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow { - return flowOf( - YieldBalanceList.Data( - balances = listOf(YieldBalance.Error(integrationId = null, address = null)), - ), - ) - } - - override suspend fun getMultiYieldBalanceSyncLegacy( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): YieldBalanceList = YieldBalanceList.Data( - balances = listOf(YieldBalance.Error(integrationId = null, address = null)), - ) - - override suspend fun getMultiYieldBalanceSync( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): YieldBalanceList = YieldBalanceList.Data( - balances = listOf(YieldBalance.Error(integrationId = null, address = null)), - ) - - override suspend fun createAction( - userWalletId: UserWalletId, - network: Network, - params: ActionParams, - ): StakingAction { - return StakingAction( - id = "quis", - integrationId = "persequeris", - status = StakingActionStatus.PROCESSING, - type = StakingActionType.CLAIM_REWARDS, - currentStepIndex = 8701, - amount = BigDecimal.ZERO, - validatorAddress = null, - validatorAddresses = listOf(), - transactions = listOf(), - createdAt = DateTime.now(), - ) - } - - override suspend fun estimateGas( - userWalletId: UserWalletId, - network: Network, - params: ActionParams, - ): StakingGasEstimate { - return StakingGasEstimate( - amount = BigDecimal(0.0001), - token = Token( - name = "Solana", - network = NetworkType.SOLANA, - symbol = "SOL", - decimals = 18, - address = null, - coinGeckoId = "solana", - logoURI = null, - isPoints = null, - ), - gasLimit = null, - ) - } - - override suspend fun constructTransaction( - networkId: String, - fee: Fee, - amount: Amount, - transactionId: String, - ): Pair = StakingTransaction( - id = "id", - network = NetworkType.SOLANA, - status = StakingTransactionStatus.SIGNED, - type = StakingTransactionType.FREEZE_ENERGY, - hash = null, - signedTransaction = null, - unsignedTransaction = null, - stepIndex = 9368, - error = null, - gasEstimate = null, - stakeId = null, - explorerUrl = null, - ledgerHwAppId = null, - isMessage = false, - ) to TransactionData.Compiled( - value = TransactionData.Compiled.Data.RawString(""), - status = TransactionStatus.Unconfirmed, - ) - - override fun getStakingApproval(cryptoCurrency: CryptoCurrency): StakingApproval = StakingApproval.Empty - - override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean = false - - private companion object { - val yield = Yield( - id = "1", - token = Token( - name = "Solana", - network = NetworkType.SOLANA, - symbol = "SOL", - decimals = 18, - address = null, - coinGeckoId = "solana", - logoURI = null, - isPoints = null, - ), - tokens = listOf(), - args = Yield.Args( - enter = Yield.Args.Enter( - addresses = Yield.Args.Enter.Addresses( - address = AddressArgument( - required = false, - network = null, - minimum = null, - maximum = null, - ), - additionalAddresses = mapOf(), - ), - args = mapOf(), - ), - exit = null, - ), - status = Yield.Status(enter = false, exit = null), - apy = 1.toBigDecimal(), - rewardRate = 2.3, - rewardType = Yield.RewardType.APR, - metadata = Yield.Metadata( - name = "Yield", - logoUri = "", - description = "", - documentation = null, - gasFeeToken = Token( - name = "Solana", - network = NetworkType.SOLANA, - symbol = "SOL", - decimals = 18, - address = null, - coinGeckoId = null, - logoURI = null, - isPoints = null, - ), - token = Token( - name = "Solana", - network = NetworkType.SOLANA, - symbol = "SOL", - decimals = 18, - address = null, - coinGeckoId = null, - logoURI = null, - isPoints = null, - ), - tokens = listOf(), - type = "auto", - rewardSchedule = Yield.Metadata.RewardSchedule.DAY, - cooldownPeriod = Yield.Metadata.Period(days = 1), - warmupPeriod = Yield.Metadata.Period(days = 1), - rewardClaiming = Yield.Metadata.RewardClaiming.AUTO, - defaultValidator = null, - minimumStake = null, - supportsMultipleValidators = false, - revshare = Yield.Metadata.Enabled(enabled = false), - fee = Yield.Metadata.Enabled(enabled = false), - ), - validators = listOf(), - isAvailable = false, - ) - } -} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 52a4d262b4..96263d3397 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -101,12 +101,12 @@ interface TransactionRepository { signer: TransactionSigner, userWalletId: UserWalletId, network: Network, - ): Result + ): com.tangem.blockchain.extensions.Result suspend fun prepareForSendMultiple( transactionData: List, signer: TransactionSigner, userWalletId: UserWalletId, network: Network, - ): Result> + ): com.tangem.blockchain.extensions.Result> } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt index dab4a7f379..3c5c19ec91 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/PrepareForSendUseCase.kt @@ -5,11 +5,13 @@ import arrow.core.left import arrow.core.right import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.extensions.Result import com.tangem.domain.card.models.TwinKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.models.network.Network import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.requireColdWallet @@ -21,30 +23,36 @@ class PrepareForSendUseCase( transactionData: TransactionData, userWallet: UserWallet, network: Network, - ): Either { + ): Either { val signer = createSigner(userWallet) - return transactionRepository.prepareForSend( + val result = transactionRepository.prepareForSend( transactionData = transactionData, userWalletId = userWallet.walletId, network = network, signer = signer, ) - .fold(onSuccess = { it.right() }, onFailure = { it.left() }) + return when (result) { + is Result.Failure -> SendTransactionUseCase.handleError(result).left() + is Result.Success -> result.data.right() + } } suspend operator fun invoke( transactionData: List, userWallet: UserWallet, network: Network, - ): Either> { + ): Either> { val signer = createSigner(userWallet) - return transactionRepository.prepareForSendMultiple( + val result = transactionRepository.prepareForSendMultiple( transactionData = transactionData, userWalletId = userWallet.walletId, network = network, signer = signer, ) - .fold(onSuccess = { it.right() }, onFailure = { it.left() }) + return when (result) { + is Result.Failure -> SendTransactionUseCase.handleError(result).left() + is Result.Success -> result.data.right() + } } private fun createSigner(userWallet: UserWallet): TransactionSigner { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 9b8a7e80c2..0c9265cfa8 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -25,7 +25,6 @@ import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.parseWrappedError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet class SendTransactionUseCase( private val demoConfig: DemoConfig, @@ -33,29 +32,38 @@ class SendTransactionUseCase( private val transactionRepository: TransactionRepository, private val walletManagersFacade: WalletManagersFacade, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, + private val getHotSigner: (UserWallet.Hot) -> TransactionSigner, ) { suspend operator fun invoke( txsData: List, userWallet: UserWallet, network: Network, sendMode: TransactionSender.MultipleTransactionSendMode, - ): Either> { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + ): Either> { + val signer = when (userWallet) { + is UserWallet.Cold -> { + val card = userWallet.scanResponse.card + val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins - val card = userWallet.scanResponse.card - val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins + val coldSigner = cardSdkConfigRepository.getCommonSigner( + cardId = card.cardId.takeIf { isCardNotBackedUp }, + twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), + ) - val signer = cardSdkConfigRepository.getCommonSigner( - cardId = card.cardId.takeIf { isCardNotBackedUp }, - twinKey = TwinKey.getOrNull(scanResponse = userWallet.scanResponse), - ) + coldSigner + } + is UserWallet.Hot -> { + getHotSigner(userWallet) + } + } val linkedTerminal = cardSdkConfigRepository.isLinkedTerminal() - if (userWallet.scanResponse.card.isStart2Coin) { + if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isStart2Coin) { cardSdkConfigRepository.setLinkedTerminal(false) } + val sendResult = try { - if (demoConfig.isDemoCardId(cardId = userWallet.cardId)) { + if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(cardId = userWallet.cardId)) { sendDemo( userWallet = userWallet, network = network, @@ -101,7 +109,7 @@ class SendTransactionUseCase( txData: TransactionData, userWallet: UserWallet, network: Network, - ): Either { + ): Either { return invoke(listOf(txData), userWallet, network, TransactionSender.MultipleTransactionSendMode.DEFAULT) .map { it.first() } } @@ -127,27 +135,29 @@ class SendTransactionUseCase( } } - private fun handleError(result: Result.Failure): SendTransactionError { - if (ResultChecker.isNetworkError(result)) { - return SendTransactionError.NetworkError( - code = result.error.message, - message = result.error.customMessage, - ) - } - val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() - return when (error) { - is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) - is BlockchainSdkError.CreateAccountUnderfunded -> { - val minAmount = error.minReserve - val minValue = minAmount.value?.format { simple(minAmount.decimals) }.orEmpty() - SendTransactionError.CreateAccountUnderfunded(minValue) - } - else -> { - SendTransactionError.BlockchainSdkError( - code = error.code, - message = error.customMessage, + companion object { + internal fun handleError(result: Result.Failure): SendTransactionError { + if (ResultChecker.isNetworkError(result)) { + return SendTransactionError.NetworkError( + code = result.error.message, + message = result.error.customMessage, ) } + val error = result.error as? BlockchainSdkError ?: return SendTransactionError.UnknownError() + return when (error) { + is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) + is BlockchainSdkError.CreateAccountUnderfunded -> { + val minAmount = error.minReserve + val minValue = minAmount.value?.format { simple(minAmount.decimals) }.orEmpty() + SendTransactionError.CreateAccountUnderfunded(minValue) + } + else -> { + SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) + } + } } } } \ No newline at end of file diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index f24225c89c..6150ebfe15 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -16,6 +16,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.walletConnect.models) + implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) /* Project - Core */ implementation(projects.core.analytics) @@ -25,4 +27,5 @@ dependencies { /* Tangem libraries */ implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts index ddd2d3024a..0e4052be5f 100644 --- a/domain/wallet-connect/models/build.gradle.kts +++ b/domain/wallet-connect/models/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) implementation(projects.domain.blockaid.models) + implementation(projects.domain.transaction.models) /* Other */ implementation(deps.moshi) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt new file mode 100644 index 0000000000..55a61db209 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequestError.kt @@ -0,0 +1,56 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.transaction.error.SendTransactionError + +sealed class WcRequestError { + + data class WrappedSendError( + val sendTransactionError: SendTransactionError, + ) : WcRequestError() + + data class WcRespondError( + val code: Int, + val message: String, + ) : WcRequestError() + + data class UnknownError(val ex: Throwable? = null) : WcRequestError() + + companion object { + + fun WcRequestError.message(): String? = when (this) { + is UnknownError -> ex?.message + is WcRespondError -> message + is WrappedSendError -> sendTransactionError.message() + } + + fun WcRequestError.code(): String? = when (this) { + is UnknownError -> null + is WcRespondError -> this.code.toString() + is WrappedSendError -> sendTransactionError.code() + } + + private fun SendTransactionError.code(): String? = when (this) { + is SendTransactionError.BlockchainSdkError -> code.toString() + is SendTransactionError.NetworkError -> code + is SendTransactionError.TangemSdkError -> code.toString() + is SendTransactionError.DataError, + SendTransactionError.DemoCardError, + is SendTransactionError.UnknownError, + SendTransactionError.UserCancelledError, + is SendTransactionError.CreateAccountUnderfunded, + -> null + } + + private fun SendTransactionError.message(): String? = when (this) { + is SendTransactionError.BlockchainSdkError -> message + is SendTransactionError.NetworkError -> message + is SendTransactionError.DataError -> message + is SendTransactionError.UnknownError -> ex?.message + is SendTransactionError.TangemSdkError, + SendTransactionError.DemoCardError, + SendTransactionError.UserCancelledError, + is SendTransactionError.CreateAccountUnderfunded, + -> null + } + } +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt index 163e84cd6d..2aa9af657a 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -10,4 +10,5 @@ data class WcSession( val networks: Set, val sdkModel: WcSdkSession, val securityStatus: CheckDAppResult, + val connectingTime: Long?, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt index a2dd9abd9e..06a471504f 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -9,4 +9,5 @@ data class WcSessionDTO( val topic: String, val walletId: UserWalletId, val securityStatus: CheckDAppResult = CheckDAppResult.FAILED_TO_VERIFY, + val connectingTime: Long? = null, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt index d599bea8d7..0042025dfb 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -10,6 +10,7 @@ sealed interface WcSolanaMethod : WcMethod { data class SignTransaction( val transaction: String, + val address: String?, ) : WcSolanaMethod data class SignAllTransaction( diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 37b4175437..cb8f983b1f 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -1,5 +1,7 @@ package com.tangem.domain.walletconnect +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.CheckDAppResult.* import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.models.network.Network @@ -7,6 +9,7 @@ import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest sealed class WcAnalyticEvents( @@ -14,6 +17,7 @@ sealed class WcAnalyticEvents( params: Map = mapOf(), ) : AnalyticsEvent(category = "Wallet Connect", event = event, params = params) { + object ScreenOpened : WcAnalyticEvents(event = "WC Screen Opened") class NewPairInitiated(source: WcPairRequest.Source) : WcAnalyticEvents( event = "Session Initiated", params = mapOf( @@ -32,12 +36,16 @@ sealed class WcAnalyticEvents( class PairRequested( network: Set, - domainVerification: String, + domainVerification: CheckDAppResult, ) : WcAnalyticEvents( event = "dApp Connection Requested", params = mapOf( NETWORKS to network.joinToString(",") { it.name }, - DOMAIN_VERIFICATION to domainVerification, + DOMAIN_VERIFICATION to when (domainVerification) { + SAFE -> "Verified" + UNSAFE -> "Risky" + FAILED_TO_VERIFY -> "Unknown" + }, ), ) @@ -66,11 +74,11 @@ sealed class WcAnalyticEvents( ), ) - class SessionDisconnected(sessionProposal: WcSessionProposal) : WcAnalyticEvents( + class SessionDisconnected(dAppMetaData: WcAppMetaData) : WcAnalyticEvents( event = "dApp Disconnected", params = mapOf( - AnalyticsParam.Key.DAPP_NAME to sessionProposal.dAppMetaData.name, - AnalyticsParam.Key.DAPP_URL to sessionProposal.dAppMetaData.url, + AnalyticsParam.Key.DAPP_NAME to dAppMetaData.name, + AnalyticsParam.Key.DAPP_URL to dAppMetaData.url, ), ) @@ -155,6 +163,30 @@ sealed class WcAnalyticEvents( ), ) + class NoticeSecurityAlert( + dAppMetaData: WcAppMetaData, + securityStatus: CheckDAppResult, + source: Source, + ) : WcAnalyticEvents( + event = "Notice - Security Alert", + params = mapOf( + AnalyticsParam.Key.DAPP_NAME to dAppMetaData.name, + AnalyticsParam.Key.DAPP_URL to dAppMetaData.url, + AnalyticsParam.Key.SOURCE to when (source) { + Source.Domain -> "Domain" + Source.SmartContract -> "Smart Contract" + }, + AnalyticsParam.Key.TYPE to when (securityStatus) { + SAFE -> "Verified" + UNSAFE -> "Risky" + FAILED_TO_VERIFY -> "Unknown" + }, + + ), + ) { + enum class Source { Domain, SmartContract } + } + companion object { const val NETWORKS = "Networks" const val DOMAIN_VERIFICATION = "Domain Verification" diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/error/ErrorsMapper.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/error/ErrorsMapper.kt new file mode 100644 index 0000000000..db38ce80aa --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/error/ErrorsMapper.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.walletconnect.error + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.common.core.TangemError +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.error.parseWrappedError +import com.tangem.domain.walletconnect.model.WcRequestError + +fun parseSendError(error: SendTransactionError): WcRequestError.WrappedSendError { + return WcRequestError.WrappedSendError(error) +} + +fun parseTangemSdkError(error: TangemError): WcRequestError.WrappedSendError { + val sendError = parseWrappedError(BlockchainSdkError.WrappedTangemError(error)) + return parseSendError(sendError) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt index b7794de457..787bea6e93 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcAddNetworkUseCase.kt @@ -1,11 +1,12 @@ package com.tangem.domain.walletconnect.usecase.method import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcRequestError interface WcAddNetworkUseCase : WcMethodUseCase, WcMethodContext { - suspend fun approve(): Either + suspend fun approve(): Either fun reject() } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMethodBase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMethodBase.kt index 5f95849a01..77f5081a18 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMethodBase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/method/WcMethodBase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.walletconnect.usecase.method import arrow.core.Either import com.tangem.domain.models.network.Network import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.wallets.models.UserWallet @@ -38,5 +39,5 @@ data class WcSignState( sealed interface WcSignStep { data object PreSign : WcSignStep data object Signing : WcSignStep - data class Result(val result: Either) : WcSignStep + data class Result(val result: Either) : WcSignStep } \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 331335a257..6a6f7db261 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { // region Tangem libraries implementation(tangemDeps.blockchain) // android-library implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) // endregion // region DI diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 58d87775b5..1af28af669 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -8,6 +8,7 @@ plugins { dependencies { // region Tangem libraries implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) // endregion // region Domain modules diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt index 459195dd77..c4f054841a 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt @@ -1,8 +1,9 @@ package com.tangem.domain.wallets.models +import com.tangem.domain.models.MobileWallet import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.models.UserWallet.Cold +import com.tangem.hot.sdk.model.HotWalletId import kotlinx.serialization.Serializable import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -56,17 +57,22 @@ sealed interface UserWallet { data class Hot( override val name: String, override val walletId: UserWalletId, - val isLocked: Boolean, - ) : UserWallet + val hotWalletId: HotWalletId, + val wallets: List?, + val backedUp: Boolean, + ) : UserWallet { + + val isLocked: Boolean get() = wallets == null + } } @OptIn(ExperimentalContracts::class) -fun UserWallet.requireColdWallet(): Cold { +fun UserWallet.requireColdWallet(): UserWallet.Cold { contract { - returns() implies (this@requireColdWallet is Cold) + returns() implies (this@requireColdWallet is UserWallet.Cold) } - return this as? Cold + return this as? UserWallet.Cold ?: error("This user wallet is not a cold wallet") } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt new file mode 100644 index 0000000000..79f665387d --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.wallets.builder + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.models.MobileWallet +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.DeriveWalletRequest +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.hot.sdk.model.UnlockHotWallet +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.withContext + +class HotUserWalletBuilder @AssistedInject constructor( + @Assisted private val hotWalletId: HotWalletId, + private val hotSdk: TangemHotSdk, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val dispatcherProvider: CoroutineDispatcherProvider, +) { + + suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { + val allNetworks = Blockchain.entries // TODO use HotDerivationsRepository to get supported networks + val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() + val requests = curves.sortedBy { it.ordinal }.map { curve -> + val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } + .mapNotNull { it.derivationPath(DerivationStyle.V3) } + + DeriveWalletRequest.Request( + curve = curve, + paths = derivationPaths, + ) + } + + val derivationResult = hotSdk.derivePublicKey( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = HotAuth.NoAuth, + ), + request = DeriveWalletRequest( + requests = requests, + ), + ) + + val wallets = derivationResult.responses.map { + MobileWallet( + publicKey = it.seedKey.publicKey, + chainCode = it.seedKey.chainCode, + curve = it.curve, + derivedKeys = it.publicKeys, + ) + } + + UserWallet.Hot( + name = generateWalletNameUseCase.invokeForHot(), + walletId = UserWalletIdBuilder.walletPublicKey(wallets.first().publicKey), + hotWalletId = hotWalletId, + wallets = wallets, + backedUp = false, + ) + } + + @AssistedFactory + interface Factory { + fun create(hotWalletId: HotWalletId): HotUserWalletBuilder + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt new file mode 100644 index 0000000000..5616695e03 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/HotDerivationsRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.repository + +import com.tangem.domain.models.network.Network + +interface HotDerivationsRepository { + + fun getAllSupportedNetworks(): Set +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt index f1d1852f0c..e779085950 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -21,6 +21,12 @@ class GenerateWalletNameUseCase( return suggestedWalletName(defaultName, existingNames) } + fun invokeForHot(): String { + val defaultName = "Wallet" + val existingNames = userWalletsListManager.userWalletsSync.map { it.name }.toSet() + return suggestedWalletName(defaultName, existingNames) + } + private fun suggestedWalletName(defaultName: String, existingNames: Set): String { val startIndex = 2 if (!existingNames.contains(defaultName)) { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index 6b3f2f5ab8..dbee9b5cfb 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -1,7 +1,6 @@ package com.tangem.domain.wallets.usecase import com.tangem.common.card.FirmwareVersion -import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper @@ -9,20 +8,16 @@ import com.tangem.domain.models.ArtworkModel import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.ArtworkSize import com.tangem.operations.attestation.CardArtworksProvider -import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles /** * Use case for getting card image url * - * @property verifier online card verifier + * @property cardArtworksProvider card artworks provider * [REDACTED_AUTHOR] */ class GetCardImageUseCase( - private val verifier: OnlineCardVerifier, private val cardArtworksProvider: CardArtworksProvider, - private val cardSdkFeatureToggles: CardSdkFeatureToggles, ) { /** @@ -38,39 +33,17 @@ class GetCardImageUseCase( firmwareVersion: FirmwareVersion, size: ArtworkSize = ArtworkSize.SMALL, ): ArtworkModel { - return if (cardSdkFeatureToggles.isNewArtworkLoadingEnabled) { - val result = cardArtworksProvider.getArtwork( - cardId = cardId, - cardPublicKey = cardPublicKey, - manufacturerName = manufacturerName, - firmwareVersion = firmwareVersion, - size = size, - ) - when (result) { - is Result.Failure -> ArtworkModel(null, getFallbackArtworkUrl(cardId)) - is Result.Success -> ArtworkModel(result.data, getFallbackArtworkUrl(cardId)) - } - } else { - ArtworkModel(null, getLegacyArtwork(cardId, cardPublicKey)) - } - } + val result = cardArtworksProvider.getArtwork( + cardId = cardId, + cardPublicKey = cardPublicKey, + manufacturerName = manufacturerName, + firmwareVersion = firmwareVersion, + size = size, + ) - private suspend fun getLegacyArtwork(cardId: String, cardPublicKey: ByteArray): String { - return when (val result = verifier.getCardInfo(cardId, cardPublicKey)) { - is Result.Success -> { - val artworkId = result.data.artwork?.id - if (artworkId.isNullOrEmpty()) { - getFallbackArtworkUrl(cardId) - } else { - CardArtworksProvider.getUrlForArtwork( - cardId = cardId, - cardPublicKey = cardPublicKey.toHexString(), - artworkId = artworkId, - ) - } - } - - is Result.Failure -> getFallbackArtworkUrl(cardId) + return when (result) { + is Result.Failure -> ArtworkModel(null, getFallbackArtworkUrl(cardId)) + is Result.Success -> ArtworkModel(result.data, getFallbackArtworkUrl(cardId)) } } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index 93eee5750e..d7aad6e31a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -5,10 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.details.entity.DetailsItemUM -import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -76,17 +74,6 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) { onClick = { router.push(AppRoute.AppSettings) }, ), ).let(::add) - - if (BuildConfig.TESTER_MENU_ENABLED) { - DetailsItemUM.Basic.Item( - id = "tester_menu", - block = BlockUM( - text = stringReference(value = "Tester menu"), - iconRes = R.drawable.ic_alert_24, - onClick = { router.push(AppRoute.TesterMenu) }, - ), - ).let(::add) - } }.toImmutableList(), ) diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 7b63af71b6..f80932c0e5 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /** Api */ implementation(projects.features.hotWallet.api) + implementation(projects.features.pushNotifications.api) /** Core modules */ implementation(projects.core.configToggles) @@ -26,6 +27,10 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.datasource) + /** Domain */ + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) @@ -36,6 +41,8 @@ dependencies { implementation(tangemDeps.card.android) { exclude(module = "joda-time") } + implementation(tangemDeps.hot.core) + implementation(tangemDeps.hot.android) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt index dd9a7da008..da289198f7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/AddExistingWalletModel.kt @@ -6,9 +6,11 @@ import com.arkivanov.decompose.router.stack.push import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute +import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import javax.inject.Inject @@ -20,6 +22,8 @@ internal class AddExistingWalletModel @Inject constructor( val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks() val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks() + val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks() + val accessCodeModelCallbacks = AccessCodeModelCallbacks() val stackNavigation = StackNavigation() @@ -38,4 +42,20 @@ internal class AddExistingWalletModel @Inject constructor( stackNavigation.pop() } } + + inner class PushNotificationsComponentModelCallbacks : PushNotificationsComponent.ModelCallbacks { + override fun onResult() { + // TODO [REDACTED_TASK_KEY] + } + } + + inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks { + override fun onBackClick() { + // TODO [REDACTED_TASK_KEY] + } + + override fun onAccessCodeSet() { + // TODO [REDACTED_TASK_KEY] + } + } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt index d2019f9f33..eebd8bf8cb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/di/AddExistingWalletModule.kt @@ -2,6 +2,7 @@ package com.tangem.features.hotwallet.addexistingwallet.root.di import com.tangem.core.decompose.model.Model import com.tangem.features.hotwallet.AddExistingWalletComponent +import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel @@ -42,4 +43,9 @@ internal interface AddExistingWalletModuleBinds { @IntoMap @ClassKey(AddExistingWalletImportModel::class) fun bindAddExistingWalletImportModel(model: AddExistingWalletImportModel): Model + + @Binds + @IntoMap + @ClassKey(SetAccessCodeModel::class) + fun bindAccessCodeModel(model: SetAccessCodeModel): Model } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt index 4c1e57c968..301d9caff4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletChildFactory.kt @@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent +import com.tangem.features.pushnotifications.api.PushNotificationsComponent import javax.inject.Inject -internal class AddExistingWalletChildFactory @Inject constructor() { +internal class AddExistingWalletChildFactory @Inject constructor( + private val pushNotificationsComponent: PushNotificationsComponent.Factory, +) { fun createChild( route: AddExistingWalletRoute, @@ -27,6 +31,18 @@ internal class AddExistingWalletChildFactory @Inject constructor() { callbacks = model.addExistingWalletImportModelCallbacks, ), ) + is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create( + context = childContext, + params = PushNotificationsComponent.Params.Callbacks( + callbacks = model.pushNotificationsComponentModelCallbacks, + ), + ) + is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent( + context = childContext, + params = SetAccessCodeComponent.Params( + callbacks = model.accessCodeModelCallbacks, + ), + ) } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt index 1566d1bc74..e38bdfdc60 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/root/routing/AddExistingWalletRoute.kt @@ -10,4 +10,10 @@ internal sealed class AddExistingWalletRoute : Route { @Serializable object Import : AddExistingWalletRoute() + + @Serializable + object PushNotifications : AddExistingWalletRoute() + + @Serializable + object AccessCode : AddExistingWalletRoute() } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index 6491acb916..a4a7a4f243 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -1,18 +1,30 @@ package com.tangem.features.hotwallet.createmobilewallet +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.wallets.builder.HotUserWalletBuilder +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.model.HotAuth +import com.tangem.hot.sdk.model.MnemonicType import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped internal class CreateMobileWalletModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, + private val saveUserWalletUseCase: SaveWalletUseCase, private val router: Router, + private val tangemHotSdk: TangemHotSdk, ) : Model() { internal val uiState: StateFlow @@ -20,10 +32,30 @@ internal class CreateMobileWalletModel @Inject constructor( CreateMobileWalletUM( onBackClick = { router.pop() }, onCreateClick = ::onCreateClick, + createButtonLoading = false, ), ) private fun onCreateClick() { - // TODO create a wallet + modelScope.launch { + uiState.update { + it.copy(createButtonLoading = true) + } + + runCatching { + val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12) + val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) + saveUserWalletUseCase( + hotUserWalletBuilder.build(), + ) + router.push(AppRoute.Wallet) + }.onFailure { + Timber.e(it) + + uiState.update { + it.copy(createButtonLoading = false) + } + } + } } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt index 7a8181fb40..bf5fd61424 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/DefaultCreateMobileWalletComponent.kt @@ -12,10 +12,12 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +@Suppress("UnusedPrivateMember") internal class DefaultCreateMobileWalletComponent @AssistedInject constructor( @Assisted private val context: AppComponentContext, @Assisted private val params: Unit, ) : CreateMobileWalletComponent, AppComponentContext by context { + private val model: CreateMobileWalletModel = getOrCreateModel(params) @Composable diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModuleBinds.kt similarity index 87% rename from features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt rename to features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModuleBinds.kt index d34f94f439..885165e2a1 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/di/CreateMobileWalletModuleBinds.kt @@ -10,18 +10,12 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object CreateMobileWalletModule @Module @InstallIn(SingletonComponent::class) internal interface CreateMobileWalletModuleBinds { @Binds - @Singleton fun bindCreateMobileWalletComponentFactory( impl: DefaultCreateMobileWalletComponent.Factory, ): CreateMobileWalletComponent.Factory diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt index 918e811d85..5d4569e604 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/entity/CreateMobileWalletUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.createmobilewallet.entity internal data class CreateMobileWalletUM( + val createButtonLoading: Boolean, val onBackClick: () -> Unit, val onCreateClick: () -> Unit, ) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt index d6861cb49e..08b9a65372 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/ui/CreateMobileWalletContent.kt @@ -85,7 +85,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo .fillMaxWidth() .padding(16.dp), text = stringResourceSafe(R.string.common_create), - showProgress = false, + showProgress = state.createButtonLoading, enabled = true, onClick = state.onCreateClick, ) @@ -133,6 +133,7 @@ private fun PreviewCreateWalletContent() { CreateMobileWalletContent( state = CreateMobileWalletUM( onBackClick = {}, + createButtonLoading = false, onCreateClick = {}, ), ) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt new file mode 100644 index 0000000000..b1f774aa93 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.hotwallet.setaccesscode + +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.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect +import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class SetAccessCodeComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: Params, +) : ComposableContentComponent, AppComponentContext by context { + + private val model: SetAccessCodeModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + SetAccessCodeContent( + state = state, + onBack = { model.onBack() }, + modifier = modifier, + ) + + DisableScreenshotsDisposableEffect() + } + + interface ModelCallbacks { + fun onBackClick() + fun onAccessCodeSet() + } + + data class Params( + val callbacks: ModelCallbacks, + ) +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt new file mode 100644 index 0000000000..945ff9ad20 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/SetAccessCodeModel.kt @@ -0,0 +1,84 @@ +package com.tangem.features.hotwallet.setaccesscode + +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.features.hotwallet.setaccesscode.entity.SetAccessCodeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@Stable +@ModelScoped +internal class SetAccessCodeModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + internal val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + + fun onBack() { + when (uiState.value.step) { + SetAccessCodeUM.Step.AccessCode -> { + params.callbacks.onBackClick() + } + SetAccessCodeUM.Step.ConfirmAccessCode -> { + uiState.update { + it.copy( + step = SetAccessCodeUM.Step.AccessCode, + accessCodeSecond = "", + ) + } + } + } + } + + private fun getInitialState() = SetAccessCodeUM( + step = SetAccessCodeUM.Step.AccessCode, + accessCodeFirst = "", + accessCodeSecond = "", + onAccessCodeFirstChange = ::onAccessCodeFirstChange, + onAccessCodeSecondChange = ::onAccessCodeSecondChange, + buttonEnabled = false, + onContinue = ::onContinue, + ) + + private fun onAccessCodeFirstChange(value: String) { + uiState.update { + it.copy( + accessCodeFirst = value, + buttonEnabled = value.length == uiState.value.accessCodeLength, + ) + } + } + + private fun onAccessCodeSecondChange(value: String) { + uiState.update { + it.copy( + accessCodeSecond = value, + buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond, + ) + } + } + + private fun onContinue() { + when (uiState.value.step) { + SetAccessCodeUM.Step.AccessCode -> { + uiState.update { + it.copy( + step = SetAccessCodeUM.Step.ConfirmAccessCode, + ) + } + } + SetAccessCodeUM.Step.ConfirmAccessCode -> { + params.callbacks.onAccessCodeSet() + } + } + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt new file mode 100644 index 0000000000..a5e98ca830 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/entity/SetAccessCodeUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.hotwallet.setaccesscode.entity + +internal data class SetAccessCodeUM( + val step: Step, + val accessCodeFirst: String, + val accessCodeSecond: String, + val onAccessCodeFirstChange: (String) -> Unit, + val onAccessCodeSecondChange: (String) -> Unit, + val buttonEnabled: Boolean, + val onContinue: () -> Unit, +) { + val accessCodeLength: Int = ACCESS_CODE_LENGTH + + enum class Step { + AccessCode, + ConfirmAccessCode, + } + + companion object { + private const val ACCESS_CODE_LENGTH = 6 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt new file mode 100644 index 0000000000..64449365ae --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeContent.kt @@ -0,0 +1,59 @@ +package com.tangem.features.hotwallet.setaccesscode.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM +import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.* +import com.tangem.core.res.R + +@Composable +internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) { + BackHandler(onBack = onBack) + + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + AnimatedContent( + modifier = Modifier.weight(1f), + targetState = state.step, + transitionSpec = TangemAnimations.AnimatedContent + .slide { initial, target -> target.ordinal > initial.ordinal }, + label = "AnimatedContent", + ) { step -> + when (step) { + AccessCode -> SetAccessCodeEnter( + modifier = Modifier.padding(top = 16.dp), + state = state, + reEnterAccessCodeState = false, + ) + ConfirmAccessCode -> SetAccessCodeEnter( + modifier = Modifier.padding(top = 16.dp), + state = state, + reEnterAccessCodeState = true, + ) + } + } + + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .imePadding(), + text = if (state.step == ConfirmAccessCode) { + stringResourceSafe(R.string.common_confirm) + } else { + stringResourceSafe(R.string.common_continue) + }, + onClick = state.onContinue, + ) + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt new file mode 100644 index 0000000000..bd787c1822 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/setaccesscode/ui/SetAccessCodeEnter.kt @@ -0,0 +1,125 @@ +package com.tangem.features.hotwallet.setaccesscode.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R +import com.tangem.core.ui.components.fields.PinTextField +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.hotwallet.setaccesscode.entity.SetAccessCodeUM + +@Composable +internal fun SetAccessCodeEnter( + state: SetAccessCodeUM, + reEnterAccessCodeState: Boolean, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier + .padding(top = 56.dp) + .align(Alignment.CenterHorizontally), + text = if (reEnterAccessCodeState) { + stringResourceSafe(R.string.access_code_confirm_title) + } else { + stringResourceSafe(R.string.access_code_create_title) + }, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterHorizontally), + text = if (reEnterAccessCodeState) { + stringResourceSafe(R.string.access_code_confirm_description) + } else { + stringResourceSafe( + R.string.access_code_create_description, + state.accessCodeLength, + ) + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + contentAlignment = Alignment.Center, + ) { + PinTextField( + length = state.accessCodeLength, + isPasswordVisual = true, + value = if (reEnterAccessCodeState) { + state.accessCodeSecond + } else { + state.accessCodeFirst + }, + onValueChange = if (reEnterAccessCodeState) { + state.onAccessCodeSecondChange + } else { + state.onAccessCodeFirstChange + }, + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + SetAccessCodeEnter( + reEnterAccessCodeState = false, + state = SetAccessCodeUM( + step = SetAccessCodeUM.Step.AccessCode, + accessCodeFirst = "", + accessCodeSecond = "", + onAccessCodeFirstChange = {}, + onAccessCodeSecondChange = {}, + buttonEnabled = false, + onContinue = {}, + ), + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview2() { + TangemThemePreview { + SetAccessCodeEnter( + reEnterAccessCodeState = true, + state = SetAccessCodeUM( + step = SetAccessCodeUM.Step.ConfirmAccessCode, + accessCodeFirst = "", + accessCodeSecond = "", + onAccessCodeFirstChange = {}, + onAccessCodeSecondChange = {}, + buttonEnabled = false, + onContinue = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 2ce867d718..ca27225294 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -74,7 +74,6 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) - implementation(projects.core.deepLinks) /* Common */ implementation(projects.common.ui) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt index f9d5ca176b..35b7a3b8d6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt @@ -3,7 +3,7 @@ package com.tangem.features.markets.deeplink import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index dd927115b7..e155d62edf 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -25,7 +25,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.impl.R @@ -194,8 +193,7 @@ internal class MarketsPortfolioModel @Inject constructor( private fun loadArtworks(wallets: List) { modelScope.launch { loadArtworksMutex.withLock { - wallets.forEach { wallet -> - wallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] + wallets.filterIsInstance().forEach { wallet -> if (!loadedArtworks.containsKey(wallet.walletId)) { val artwork = getCardImageUseCase( cardId = wallet.cardId, diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt index f982bc3abb..36689ae915 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt @@ -1,8 +1,6 @@ package com.tangem.features.nft interface NFTFeatureToggles { - val isNFTEnabled: Boolean - val isNFTEVMEnabled: Boolean - val isNFTSolanaEnabled: Boolean + val isNFTMediaContentEnabled: Boolean } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt index d59d0d8472..6ee99738ee 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt @@ -5,14 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class DefaultNFTFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : NFTFeatureToggles { - override val isNFTEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NFT_ENABLED") - - override val isNFTEVMEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NFT_EVM_ENABLED") - - override val isNFTSolanaEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NFT_SOLANA_ENABLED") override val isNFTMediaContentEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NFT_MEDIA_CONTENT_ENABLED") diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt index 2d42caaf85..3036cc073f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt @@ -1,8 +1,8 @@ package com.tangem.features.nft.collections.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.appbar.AppBarWithBackButton @@ -17,31 +17,28 @@ import com.tangem.features.nft.impl.R internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) - Scaffold( - modifier = modifier, - containerColor = TangemTheme.colors.background.secondary, - topBar = { - AppBarWithBackButton( - modifier = Modifier.statusBarsPadding(), - onBackClick = state.onBackClick, - text = stringResourceSafe(id = R.string.nft_collections_title), - iconRes = R.drawable.ic_back_24, - ) - }, - content = { innerPadding -> - TangemPullToRefreshContainer( - config = state.pullToRefreshConfig, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - ) { - when (val content = state.content) { - is NFTCollectionsUM.Content -> NFTCollectionsContent(content) - is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content) - is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content) - is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content) - } + Column( + modifier = modifier + .background(TangemTheme.colors.background.secondary), + ) { + AppBarWithBackButton( + modifier = Modifier, + onBackClick = state.onBackClick, + text = stringResourceSafe(id = R.string.nft_collections_title), + iconRes = R.drawable.ic_back_24, + ) + + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, + modifier = Modifier + .fillMaxSize(), + ) { + when (val content = state.content) { + is NFTCollectionsUM.Content -> NFTCollectionsContent(content) + is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content) + is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content) + is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content) } - }, - ) + } + } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt index 7e2c62d33b..167988824f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt @@ -52,14 +52,14 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, ) - PrimaryButton( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing48) - .widthIn(min = TangemTheme.dimens.size158), - text = stringResourceSafe(R.string.nft_collections_receive), - onClick = state.onReceiveClick, - ) } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = state.onReceiveClick, + ) } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt index ef907f1e0f..f906d793ee 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/ui/NFTContent.kt @@ -19,8 +19,8 @@ import com.tangem.features.nft.common.NFTRoute @Composable internal fun NFTContent(stackState: ChildStack) { Column( - modifier = Modifier.Companion - .background(color = TangemTheme.colors.background.tertiary) + modifier = Modifier + .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() .imePadding() .systemBarsPadding(), diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt index 8ab05b6d7b..ff99321696 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/entity/transformer/NFTPriceChangeTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.nft.details.entity.transformer +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat @@ -8,6 +9,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.features.nft.details.entity.NFTAssetUM import com.tangem.features.nft.details.entity.NFTDetailsUM +import com.tangem.features.nft.impl.R import com.tangem.utils.transformer.Transformer internal class NFTPriceChangeTransformer( @@ -18,35 +20,50 @@ internal class NFTPriceChangeTransformer( override fun transform(prevState: NFTDetailsUM): NFTDetailsUM { val topInfo = prevState.nftAsset.topInfo as? NFTAssetUM.TopInfo.Content ?: return prevState - return prevState.copy( - nftAsset = prevState.nftAsset.copy( - topInfo = topInfo.copy( - salePrice = when (nftSalePrice) { - is NFTSalePrice.Empty, - is NFTSalePrice.Error, - -> NFTAssetUM.SalePrice.Empty - is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading - is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content( - isFlickering = false, - cryptoPrice = stringReference( - nftSalePrice.value.format { - crypto( - symbol = nftSalePrice.symbol, - decimals = nftSalePrice.decimals, - ) - }, - ), - fiatPrice = stringReference( - nftSalePrice.fiatValue.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - ), + val salePrice = when (nftSalePrice) { + is NFTSalePrice.Empty, + is NFTSalePrice.Error, + -> NFTAssetUM.SalePrice.Empty + is NFTSalePrice.Loading -> NFTAssetUM.SalePrice.Loading + is NFTSalePrice.Value -> NFTAssetUM.SalePrice.Content( + isFlickering = false, + cryptoPrice = stringReference( + nftSalePrice.value.format { + crypto( + symbol = nftSalePrice.symbol, + decimals = nftSalePrice.decimals, ) }, ), + fiatPrice = stringReference( + nftSalePrice.fiatValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + ) + } + + val hasSalePrice = salePrice !is NFTAssetUM.SalePrice.Empty + + val newTopInfo = if ( + topInfo.rarity is NFTAssetUM.Rarity.Empty && + topInfo.description.isNullOrEmpty() && + !hasSalePrice + ) { + NFTAssetUM.TopInfo.Empty + } else { + topInfo.copy( + title = resourceReference(R.string.nft_details_last_sale_price).takeIf { hasSalePrice }, + salePrice = salePrice, + ) + } + + return prevState.copy( + nftAsset = prevState.nftAsset.copy( + topInfo = newTopInfo, ), ) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt index 203288ef78..2c3ad2ede5 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetails.kt @@ -1,13 +1,10 @@ package com.tangem.features.nft.details.ui import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.FabPosition -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TangemTopAppBar @@ -22,25 +19,25 @@ import com.tangem.features.nft.impl.R internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) - Scaffold( - modifier = modifier, - containerColor = TangemTheme.colors.background.secondary, - topBar = { + Box( + modifier = modifier + .background(TangemTheme.colors.background.secondary), + ) { + Column( + modifier = Modifier.fillMaxSize(), + ) { TangemTopAppBar( - modifier = Modifier.statusBarsPadding(), + modifier = Modifier, startButton = TopAppBarButtonUM( iconRes = R.drawable.ic_back_24, onIconClicked = state.onBackClick, ), title = state.nftAsset.name, ) - }, - content = { innerPadding -> + TangemPullToRefreshContainer( config = state.pullToRefreshConfig, - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), ) { NFTDetailsAsset( state = state.nftAsset, @@ -49,16 +46,19 @@ internal fun NFTDetails(state: NFTDetailsUM, modifier: Modifier = Modifier) { onExploreClick = state.onExploreClick, ) } - }, - floatingActionButtonPosition = FabPosition.Center, - floatingActionButton = { - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_send), - onClick = state.onSendClick, - ) - }, - ) + } + + PrimaryButton( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + text = stringResourceSafe(id = R.string.common_send), + onClick = state.onSendClick, + ) + } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt index df471fec36..049d91b0cd 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/ui/NFTDetailsBlocksGroup.kt @@ -173,8 +173,7 @@ internal fun NFTDetailsGroupBlock( } else { onBlockClick?.invoke() } - } - .padding(top = TangemTheme.dimens.spacing4), + }, text = value.resolveReference(), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt index 4dac3a8d5c..21c147c3a9 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -1,8 +1,8 @@ package com.tangem.features.nft.receive.ui import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet @@ -22,41 +22,33 @@ import com.tangem.features.nft.receive.entity.NFTReceiveUM internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) - Scaffold( - modifier = modifier, - containerColor = TangemTheme.colors.background.secondary, - topBar = { - TangemTopAppBar( - modifier = Modifier.statusBarsPadding(), - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_close_24, - onIconClicked = state.onBackClick, - ), - title = stringResourceSafe(id = R.string.nft_receive_title), - subtitle = state.appBarSubtitle.resolveReference(), - ) - }, - content = { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .fillMaxSize(), - ) { - SearchBar( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - state = state.search, - colors = TangemSearchBarDefaults.secondaryTextFieldColors, - ) + Column( + modifier = modifier + .background(TangemTheme.colors.background.secondary), + ) { + TangemTopAppBar( + modifier = Modifier, + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_close_24, + onIconClicked = state.onBackClick, + ), + title = stringResourceSafe(id = R.string.nft_receive_title), + subtitle = state.appBarSubtitle.resolveReference(), + ) - when (val networks = state.networks) { - is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks) - is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty() - } - } - }, - ) + SearchBar( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state.search, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + + when (val networks = state.networks) { + is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks) + is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty() + } + } ShowBottomSheet(state.bottomSheetConfig) } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt index f6e27276f2..9f18df108f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/traits/ui/NFTAssetTraits.kt @@ -1,9 +1,8 @@ package com.tangem.features.nft.traits.ui import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.Scaffold +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.appbar.TangemTopAppBar @@ -17,25 +16,21 @@ import com.tangem.features.nft.traits.entity.NFTAssetTraitsUM internal fun NFTAssetTraits(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) - Scaffold( - modifier = modifier, - containerColor = TangemTheme.colors.background.secondary, - topBar = { - TangemTopAppBar( - modifier = Modifier.statusBarsPadding(), - startButton = TopAppBarButtonUM( - iconRes = R.drawable.ic_back_24, - onIconClicked = state.onBackClick, - ), - title = stringResourceSafe(R.string.nft_traits_title), - ) - }, - content = { innerPadding -> - NFTAssetTraitsContent( - modifier = Modifier - .padding(innerPadding), - state = state, - ) - }, - ) + Column( + modifier = modifier + .background(TangemTheme.colors.background.secondary), + ) { + TangemTopAppBar( + modifier = Modifier, + startButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = state.onBackClick, + ), + title = stringResourceSafe(R.string.nft_traits_title), + ) + + NFTAssetTraitsContent( + state = state, + ) + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt index bc0d8551bd..6c8cb1fc1b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeEnter.kt @@ -71,13 +71,11 @@ internal fun MultiWalletAccessCodeEnter( state.onAccessCodeFirstChange }, label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third), - isError = state.codesNotMatchError || state.atLeast4CharError, + isError = state.codesNotMatchError, visualTransformation = PasswordVisualTransformation(), caption = when { state.codesNotMatchError && reEnterAccessCodeState -> stringResourceSafe(R.string.onboarding_access_codes_doesnt_match) - state.atLeast4CharError && !reEnterAccessCodeState -> - stringResourceSafe(R.string.onboarding_access_code_too_short) else -> null }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index cbaed64847..2970584dc3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -204,12 +204,9 @@ internal class OnboardingNoteTopUpModel @Inject constructor( } private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { - val userWalletId = userWallet?.walletId ?: return - modelScope.launch { val availableForBuy = rampStateManager.availableForBuy( - scanResponse = scanResponse, - userWalletId = userWalletId, + userWallet = userWallet ?: return@launch, cryptoCurrency = cryptoCurrencyStatus.currency, ) _uiState.update { diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts index 598b224b08..ac5210f2a6 100644 --- a/features/onramp/api/build.gradle.kts +++ b/features/onramp/api/build.gradle.kts @@ -13,7 +13,6 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.deepLinks) /* Project - Domain */ implementation(projects.domain.onramp.models) diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt deleted file mode 100644 index 8a45fc89ef..0000000000 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.deeplink - -import com.tangem.core.deeplink.DeepLink -import kotlinx.coroutines.CoroutineScope - -@Deprecated("Use OnrampDeepLinkHandler") -abstract class OnrampDeepLink : DeepLink() { - override val uri = "tangem://onramp" - - interface Factory { - fun create(coroutineScope: CoroutineScope): OnrampDeepLink - } -} - -interface OnrampDeepLinkHandler { - - interface Factory { - fun create(coroutineScope: CoroutineScope, queryParams: Map): OnrampDeepLinkHandler - } -} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLinkHandler.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLinkHandler.kt new file mode 100644 index 0000000000..35f5e06190 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface OnrampDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, queryParams: Map): OnrampDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 2307564258..b32858e319 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.configToggles) implementation(projects.core.decompose) - implementation(projects.core.deepLinks) implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt deleted file mode 100644 index a7edc4cbf5..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tangem.features.onramp.deeplink - -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -internal class DefaultOnrampDeepLink @AssistedInject constructor( - private val appRouter: AppRouter, - private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, - @Assisted private val scope: CoroutineScope, -) : OnrampDeepLink() { - - override fun onReceive(params: Map) { - val txId = params[TX_ID_KEY] - val result = OnrampRedirectResult.getResult(params[RESULT_KEY]) - - when { - !txId.isNullOrEmpty() -> { - // finish current onramp flow and show onramp success screen - val replaceOnrampScreens = appRouter.stack - .filterNot { it is AppRoute.Onramp } - .toMutableList() - replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId)) - appRouter.replaceAll(*replaceOnrampScreens.toTypedArray()) - } - result != OnrampRedirectResult.Unknown -> { - scope.launch { - onrampSuccessScreenTrigger.triggerOnrampSuccess(result == OnrampRedirectResult.Success) - } - } - } - } - - @AssistedFactory - interface Factory : OnrampDeepLink.Factory { - override fun create(coroutineScope: CoroutineScope): DefaultOnrampDeepLink - } - - private companion object { - const val TX_ID_KEY = "tx_id" - const val RESULT_KEY = "result" - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt index fafbf571a6..48b413740f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt @@ -11,10 +11,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface OnrampDeeplinkModule { - @Binds - @Singleton - fun bindFactory(impl: DefaultOnrampDeepLink.Factory): OnrampDeepLink.Factory - @Binds @Singleton fun bindOnrampDeepLinkHandlerFactory(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 4f6245b649..5d814942e4 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -83,8 +83,7 @@ internal class OnrampOperationModel @Inject constructor( fun onHotTokenClick(status: CryptoCurrencyStatus) { modelScope.launch { val unavailabilityReason = rampStateManager.availableForBuy( - scanResponse = selectedUserWallet.scanResponse, - userWalletId = params.userWalletId, + userWallet = selectedUserWallet, cryptoCurrency = status.currency, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index 2fb53e1132..2a40893a63 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -147,7 +147,7 @@ internal class OnrampTokenListModel @Inject constructor( private fun Lce.isInsufficientBalanceForSell(): Boolean { return if (params.filterOperation == OnrampOperation.SELL) { isContent { - (it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() ?: false + (it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true } } else { false @@ -229,13 +229,16 @@ internal class OnrampTokenListModel @Inject constructor( return when (params.filterOperation) { OnrampOperation.BUY -> { rampStateManager.availableForBuy( - scanResponse = userWallet.scanResponse, - userWalletId = params.userWalletId, + userWallet = userWallet, cryptoCurrency = status.currency, ).isAvailable() } OnrampOperation.SELL -> { - rampStateManager.availableForSell(userWallet = userWallet, status = status).isRight() + rampStateManager.availableForSell( + userWalletId = userWallet.walletId, + status = status, + sendUnavailabilityReason = null, + ).isRight() } OnrampOperation.SWAP -> { val isAvailable = rampStateManager.availableForSwap( diff --git a/features/push-notifications/api/build.gradle.kts b/features/push-notifications/api/build.gradle.kts index 3366d4511c..23997238e3 100644 --- a/features/push-notifications/api/build.gradle.kts +++ b/features/push-notifications/api/build.gradle.kts @@ -16,4 +16,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.core.analytics.models) + + /** Common */ + implementation(projects.common.routing) } \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsComponent.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsComponent.kt index 515492abe3..fd0b224d91 100644 --- a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsComponent.kt +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/PushNotificationsComponent.kt @@ -1,9 +1,19 @@ package com.tangem.features.pushnotifications.api +import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent interface PushNotificationsComponent : ComposableContentComponent { - interface Factory : ComponentFactory + interface Factory : ComponentFactory + + interface ModelCallbacks { + fun onResult() + } + + sealed class Params { + data class Callbacks(val callbacks: ModelCallbacks) : Params() + data class Route(val route: AppRoute) : Params() + } } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt index 07278cd29d..35ed4ef76a 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/DefaultPushNotificationsComponent.kt @@ -19,7 +19,7 @@ import dagger.assisted.AssistedInject internal class DefaultPushNotificationsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted params: PushNotificationsComponent.Params, ) : PushNotificationsComponent, AppComponentContext by appComponentContext { private val model: PushNotificationsModel = getOrCreateModel(params) @@ -41,6 +41,9 @@ internal class DefaultPushNotificationsComponent @AssistedInject constructor( @AssistedFactory interface Factory : PushNotificationsComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultPushNotificationsComponent + override fun create( + context: AppComponentContext, + params: PushNotificationsComponent.Params, + ): DefaultPushNotificationsComponent } } \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt index de2d442132..1d34193a8f 100644 --- a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/model/PushNotificationsModel.kt @@ -1,15 +1,16 @@ package com.tangem.features.pushnotifications.impl.model import androidx.compose.runtime.Stable -import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.NeverRequestPermissionUseCase import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase +import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.impl.presentation.state.PushNotificationsUM @@ -21,15 +22,19 @@ import javax.inject.Inject @Stable @ModelScoped +@Suppress("LongParameterList") internal class PushNotificationsModel @Inject constructor( + paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, private val appRouter: AppRouter, private val analyticHandler: AnalyticsEventHandler, - private val notificationsFeatureToggles: NotificationsFeatureToggles, + notificationsFeatureToggles: NotificationsFeatureToggles, ) : Model(), PushNotificationsClickIntents { + private val params: PushNotificationsComponent.Params = paramsContainer.require() + private val _state = MutableStateFlow( PushNotificationsUM( showInfoAboutNotifications = notificationsFeatureToggles.isNotificationsEnabled, @@ -51,7 +56,7 @@ internal class PushNotificationsModel @Inject constructor( modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) - appRouter.push(AppRoute.Home) + onResult() } } @@ -62,7 +67,7 @@ internal class PushNotificationsModel @Inject constructor( modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) - appRouter.push(AppRoute.Home) + onResult() } } @@ -73,7 +78,18 @@ internal class PushNotificationsModel @Inject constructor( modelScope.launch { neverRequestPermissionUseCase(PUSH_PERMISSION) neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) - appRouter.push(AppRoute.Home) + onResult() + } + } + + private fun onResult() { + when (params) { + is PushNotificationsComponent.Params.Callbacks -> { + params.callbacks.onResult() + } + is PushNotificationsComponent.Params.Route -> { + appRouter.push(params.route) + } } } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt index 2e00a6910d..8ee7c7ea48 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt @@ -18,6 +18,7 @@ import com.google.mlkit.vision.common.InputImage import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.SystemBarsIconsDisposable +import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.model.QrScanningModel import com.tangem.feature.qrscanning.presentation.QrScanningContent @@ -41,10 +42,10 @@ class DefaultQrScanningComponent @AssistedInject constructor( // Camera requires its own analyzer instance due to flow of frames needed to be analyzed. // Each new frame can cancel previous analysis e.i. image from the gallery can be skipped. private val cameraAnalyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) { - MLKitBarcodeAnalyzer(model::onQrScanned) + MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.CAMERA) } } private val analyzer: MLKitBarcodeAnalyzer by lazy(LazyThreadSafetyMode.NONE) { - MLKitBarcodeAnalyzer(model::onQrScanned) + MLKitBarcodeAnalyzer { qrCode -> model.onQrScanned(qrCode, QrResultSource.GALLERY) } } init { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt index b0702b0d62..c44a3c6fcd 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.qrscanning.model +import com.tangem.domain.qrscanning.models.QrResultSource import kotlinx.coroutines.flow.SharedFlow internal interface QrScanningClickIntents { @@ -8,7 +9,7 @@ internal interface QrScanningClickIntents { fun onBackClick() - fun onQrScanned(qrCode: String) + fun onQrScanned(qrCode: String, source: QrResultSource) fun onGalleryClicked() diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt index 505a42e7ec..54f9af36c5 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/model/QrScanningModel.kt @@ -10,6 +10,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.domain.qrscanning.models.QrResultSource +import com.tangem.domain.qrscanning.models.RawQrResult import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.qrscanning.presentation.QrScanningState @@ -71,10 +73,11 @@ internal class QrScanningModel @Inject constructor( override fun onBackClick() = appRouter.pop() - override fun onQrScanned(qrCode: String) { + override fun onQrScanned(qrCode: String, source: QrResultSource) { if (qrCode.isNotBlank()) { modelScope.launch(dispatchers.mainImmediate) { - emitQrScannedEventUseCase.invoke(params.source, qrCode) + val qrCode = RawQrResult(qrCode, source, params.source) + emitQrScannedEventUseCase.invoke(qrCode) } if (!isScanned) { appRouter.pop() diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index 1b51ac2222..bc9659cc28 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.impl.R import com.tangem.feature.qrscanning.model.QrScanningClickIntents @@ -28,7 +29,7 @@ internal class InitializeQrScanningStateTransformer( topBarConfig = constructTopBarConfig(), message = message, onBackClick = clickIntents::onBackClick, - onQrScanned = clickIntents::onQrScanned, + onQrScanned = { qrCode -> clickIntents.onQrScanned(qrCode, QrResultSource.CAMERA) }, onGalleryClick = clickIntents::onGalleryClicked, pasteAction = constructPasteAction(), ) @@ -50,7 +51,7 @@ internal class InitializeQrScanningStateTransformer( private fun constructPasteAction(): PasteAction { val uri = clipboardManager.getText() return if (uri != null) { - PasteAction.Perform { clickIntents.onQrScanned(uri) } + PasteAction.Perform { clickIntents.onQrScanned(uri, QrResultSource.CLIPBOARD) } } else { PasteAction.None } diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 8b079dc717..fdbe335284 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -11,7 +11,6 @@ import com.tangem.datasource.api.tangemTech.models.StartReferralBody import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.feature.referral.domain.models.ReferralData @@ -97,13 +96,13 @@ internal class ReferralRepositoryImpl @Inject constructor( sdkToken = sdkToken, blockchain = blockchain, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) } else { cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, ) } } diff --git a/features/referral/impl/build.gradle.kts b/features/referral/impl/build.gradle.kts index 34f1402a0b..67ca47e02f 100644 --- a/features/referral/impl/build.gradle.kts +++ b/features/referral/impl/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.ui) implementation(projects.core.decompose) - implementation(projects.core.deepLinks) implementation(projects.libs.crypto) implementation(projects.common.routing) diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts index 6a1ce98877..c1a6e00fc9 100644 --- a/features/send-v2/api/build.gradle.kts +++ b/features/send-v2/api/build.gradle.kts @@ -8,6 +8,10 @@ android { namespace = "com.tangem.features.send.v2.api" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Core */ implementation(projects.core.decompose) @@ -34,4 +38,14 @@ dependencies { /** Other */ implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) + + // region Tests + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) + testImplementation(projects.domain.staking.models) + // endregion } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt index 15f8c51974..bed3633556 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/entity/FeeSelectorUM.kt @@ -21,11 +21,9 @@ sealed class FeeSelectorUM { val fees: TransactionFee, val feeItems: ImmutableList, val selectedFeeItem: FeeItem, - val isFeeApproximate: Boolean, + val feeExtraInfo: FeeExtraInfo, val feeFiatRateUM: FeeFiatRateUM?, - val displayNonceInput: Boolean, - val nonce: BigInteger?, - val onNonceChange: (String) -> Unit, + val feeNonce: FeeNonce, ) : FeeSelectorUM() } @@ -35,6 +33,21 @@ data class FeeFiatRateUM( val appCurrency: AppCurrency, ) +@Immutable +data class FeeExtraInfo( + val isFeeApproximate: Boolean, + val isFeeConvertibleToFiat: Boolean, + val isTronToken: Boolean, +) + +sealed class FeeNonce { + data object None : FeeNonce() + data class Nonce( + val nonce: BigInteger?, + val onNonceChange: (String) -> Unit, + ) : FeeNonce() +} + @Immutable sealed class FeeItem { abstract val fee: Fee diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index db5fa2513b..4eaa21bf65 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -13,6 +13,7 @@ sealed class FeeSelectorParams { abstract val state: FeeSelectorUM abstract val onLoadFee: suspend () -> Either abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus abstract val suggestedFeeState: SuggestedFeeState abstract val feeDisplaySource: FeeDisplaySource @@ -20,6 +21,7 @@ sealed class FeeSelectorParams { override val state: FeeSelectorUM, override val onLoadFee: suspend () -> Either, override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, override val suggestedFeeState: SuggestedFeeState, override val feeDisplaySource: FeeDisplaySource, ) : FeeSelectorParams() @@ -28,6 +30,7 @@ sealed class FeeSelectorParams { override val state: FeeSelectorUM, override val onLoadFee: suspend () -> Either, override val cryptoCurrencyStatus: CryptoCurrencyStatus, + override val feeCryptoCurrencyStatus: CryptoCurrencyStatus, override val suggestedFeeState: SuggestedFeeState, override val feeDisplaySource: FeeDisplaySource, val callback: FeeSelectorModelCallback, diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt index b4468f86e4..6ca3b9f4f7 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationBlockComponent.kt @@ -6,6 +6,8 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat interface SendDestinationBlockComponent : ComposableContentComponent { + fun updateState(destinationUM: DestinationUM) + interface Factory { fun create( context: AppComponentContext, diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt new file mode 100644 index 0000000000..181267b68a --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtils.kt @@ -0,0 +1,99 @@ +package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils + +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.entity.FeeItem +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.utils.extensions.isZero +import java.math.BigDecimal +import java.math.RoundingMode + +object FeeCalculationUtils { + + private val FEE_MAX_DIFF = BigDecimal("5") + + /** + * Check and calculates subtracted amount + */ + fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, + ): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } + } + + /** + * Check if custom fee is too high + */ + fun checkIfCustomFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content): Pair { + val defaultResult = false to "" + + if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return defaultResult + + val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return defaultResult + val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return defaultResult + val highValue = multipleFees.priority.amount.value ?: return defaultResult + + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + val diff = if (highValue > BigDecimal.ZERO) { + customValue / highValue + } else { + BigDecimal.ZERO + } + val isFeeTooHigh = diff > FEE_MAX_DIFF + return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP) + } + + /** + * Check if custom fee is too low + */ + fun checkIfCustomFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean { + if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return false + + val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false + val minimumValue = multipleFees.minimum.amount.value ?: return false + val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return false + val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals) + + return minimumValue > customValue + } + + /** + * Checks if sending amount with fee is greater than balance + */ + fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, + ): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue + } + + /** + * Checks if fee exceeds fee paid currency balance + */ + fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { + return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance + } +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/notifications/SendNotificationsUpdateListener.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/notifications/SendNotificationsUpdateListener.kt new file mode 100644 index 0000000000..c4640cd523 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/notifications/SendNotificationsUpdateListener.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.api.subcomponents.notifications + +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import kotlinx.coroutines.flow.Flow + +interface SendNotificationsUpdateListener { + /** Flow triggers notifications update */ + val updateTriggerFlow: Flow + + /** Flow returns whether there is error notifications */ + val hasErrorFlow: Flow +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/notifications/SendNotificationsUpdateTrigger.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/notifications/SendNotificationsUpdateTrigger.kt new file mode 100644 index 0000000000..54c33ec881 --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/notifications/SendNotificationsUpdateTrigger.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2.api.subcomponents.notifications + +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData + +interface SendNotificationsUpdateTrigger { + /** Trigger return callback with check result */ + suspend fun callbackHasError(hasError: Boolean) + + /** Trigger fee check reload */ + suspend fun triggerUpdate(data: NotificationData) +} \ No newline at end of file diff --git a/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt new file mode 100644 index 0000000000..59ea19d75c --- /dev/null +++ b/features/send-v2/api/src/test/java/com/tangem/features/send/v2/api/subcomponents/feeSelector/utils/FeeCalculationUtilsTest.kt @@ -0,0 +1,188 @@ +package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import io.mockk.mockk +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +class FeeCalculationUtilsTest { + + @Test + fun `GIVEN amount subtract available and fee coverage needed WHEN checkAndCalculateSubtractedAmount THEN returns subtracted amount`() { + // GIVEN + val isAmountSubtractAvailable = true + val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("6")) + val amountValue = BigDecimal("5") + val feeValue = BigDecimal("2") + val reduceAmountBy = BigDecimal("1") + + // WHEN + val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + // THEN + assertThat(result).isEqualTo(BigDecimal("3")) + } + + @Test + fun `GIVEN amount subtract not available WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() { + // GIVEN + val isAmountSubtractAvailable = false + val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10")) + val amountValue = BigDecimal("5") + val feeValue = BigDecimal("2") + val reduceAmountBy = BigDecimal("1") + + // WHEN + val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + // THEN + assertThat(result).isEqualTo(amountValue) + } + + @Test + fun `GIVEN sufficient balance for amount and fee WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() { + // GIVEN + val isAmountSubtractAvailable = true + val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10")) + val amountValue = BigDecimal("5") + val feeValue = BigDecimal("2") + val reduceAmountBy = BigDecimal("1") + + // WHEN + val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + // THEN + assertThat(result).isEqualTo(amountValue) + } + + @Test + fun `GIVEN no balance WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() { + // GIVEN + val isAmountSubtractAvailable = true + val cryptoCurrencyStatus = createCryptoCurrencyStatus(null) + val amountValue = BigDecimal("5") + val feeValue = BigDecimal("2") + val reduceAmountBy = BigDecimal("1") + + // WHEN + val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + // THEN + assertThat(result).isEqualTo(amountValue) + } + + @Test + fun `GIVEN fee exceeds balance WHEN checkExceedBalance THEN returns true`() { + // GIVEN + val feeBalance = BigDecimal("5") + val feeAmount = BigDecimal("10") + + // WHEN + val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount) + + // THEN + assertThat(result).isTrue() + } + + @Test + fun `GIVEN fee within balance WHEN checkExceedBalance THEN returns false`() { + // GIVEN + val feeBalance = BigDecimal("10") + val feeAmount = BigDecimal("5") + + // WHEN + val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount) + + // THEN + assertThat(result).isFalse() + } + + @Test + fun `GIVEN null fee amount WHEN checkExceedBalance THEN returns true`() { + // GIVEN + val feeBalance = BigDecimal("10") + val feeAmount: BigDecimal? = null + + // WHEN + val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount) + + // THEN + assertThat(result).isTrue() + } + + @Test + fun `GIVEN null fee balance WHEN checkExceedBalance THEN returns true`() { + // GIVEN + val feeBalance: BigDecimal? = null + val feeAmount = BigDecimal("5") + + // WHEN + val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount) + + // THEN + assertThat(result).isTrue() + } + + @Test + fun `GIVEN zero fee amount WHEN checkExceedBalance THEN returns true`() { + // GIVEN + val feeBalance = BigDecimal("10") + val feeAmount = BigDecimal.ZERO + + // WHEN + val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount) + + // THEN + assertThat(result).isTrue() + } + + private fun createCryptoCurrencyStatus(amount: BigDecimal?): CryptoCurrencyStatus { + val value = if (amount != null) { + CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = BigDecimal.ZERO, + fiatRate = BigDecimal.ZERO, + priceChange = BigDecimal.ZERO, + yieldBalance = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = mockk(relaxed = true), + sources = CryptoCurrencyStatus.Sources(), + ) + } else { + CryptoCurrencyStatus.NoAmount( + priceChange = BigDecimal.ZERO, + fiatRate = BigDecimal.ZERO, + ) + } + return CryptoCurrencyStatus( + currency = mockk(relaxed = true), + value = value, + ) + } +} \ 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 dc97603af5..519db0736a 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -11,6 +11,10 @@ android { namespace = "com.tangem.features.send.v2.impl" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Api */ implementation(projects.features.sendV2.api) @@ -79,4 +83,13 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + // region Tests + testImplementation(deps.test.coroutine) + 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/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt index c40b00375d..acaf68a42f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt @@ -48,10 +48,14 @@ internal sealed class CommonSendAnalyticEvents( data class TransactionError( val categoryName: String, val token: String, + val blockchain: String, ) : CommonSendAnalyticEvents( category = categoryName, event = "Error - Transaction Rejected", - params = mapOf(TOKEN_PARAM to token), + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), ) /** Close button clicked */ 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 index 0a05f94348..b266aa0a6a 100644 --- 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 @@ -45,6 +45,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( params = FeeSelectorParams.FeeSelectorDetailsParams( state = model.uiState.value, onLoadFee = params.onLoadFee, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, cryptoCurrencyStatus = params.cryptoCurrencyStatus, callback = model, suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt index bab25e894b..d35aa6784b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorIntents.kt @@ -5,11 +5,13 @@ import com.tangem.features.send.v2.api.entity.FeeItem internal interface FeeSelectorIntents { fun onFeeItemSelected(feeItem: FeeItem) fun onCustomFeeValueChange(index: Int, value: String) + fun onNonceChange(value: String) fun onDoneClick() } internal class StubFeeSelectorIntents : FeeSelectorIntents { override fun onFeeItemSelected(feeItem: FeeItem) {} override fun onCustomFeeValueChange(index: Int, value: String) {} + override fun onNonceChange(value: String) {} override fun onDoneClick() {} } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 183224d789..acbc4e5158 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -15,10 +15,7 @@ import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams -import com.tangem.features.send.v2.feeselector.model.transformers.FeeItemSelectedTransformer -import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorCustomValueChangedTransformer -import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorErrorTransformer -import com.tangem.features.send.v2.feeselector.model.transformers.FeeSelectorLoadedTransformer +import com.tangem.features.send.v2.feeselector.model.transformers.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.MutableStateFlow @@ -67,6 +64,7 @@ internal class FeeSelectorModel @Inject constructor( uiState.update( FeeSelectorLoadedTransformer( cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, appCurrency = appCurrency, fees = fee, suggestedFeeState = params.suggestedFeeState, @@ -80,7 +78,7 @@ internal class FeeSelectorModel @Inject constructor( } private fun isFeeApproximate(amountType: AmountType): Boolean { - val networkId = params.cryptoCurrencyStatus.currency.network.id + val networkId = params.feeCryptoCurrencyStatus.currency.network.id return isFeeApproximateUseCase(networkId = networkId, amountType = amountType) } @@ -95,11 +93,15 @@ internal class FeeSelectorModel @Inject constructor( value = value, intents = this, appCurrency = appCurrency, - feeCryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, ), ) } + override fun onNonceChange(value: String) { + uiState.update(FeeSelectorNonceChangeTransformer(value = value)) + } + override fun onDoneClick() { (params as? FeeSelectorParams.FeeSelectorDetailsParams)?.callback?.onFeeResult(uiState.value) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt index 7d6b828036..da024d61e6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt @@ -1,19 +1,21 @@ package com.tangem.features.send.v2.feeselector.model.transformers +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.models.currency.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.v2.api.entity.FeeFiatRateUM -import com.tangem.features.send.v2.api.entity.FeeItem -import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents +import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList @Suppress("LongParameterList") internal class FeeSelectorLoadedTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrency: AppCurrency, private val fees: TransactionFee, private val suggestedFeeState: FeeSelectorParams.SuggestedFeeState, @@ -26,7 +28,7 @@ internal class FeeSelectorLoadedTransformer( normalFee = fees.normal, feeSelectorIntents = feeSelectorIntents, appCurrency = appCurrency, - cryptoCurrencyStatus = cryptoCurrencyStatus, + cryptoCurrencyStatus = feeCryptoCurrencyStatus, ) override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { @@ -44,20 +46,32 @@ internal class FeeSelectorLoadedTransformer( -> feeItems.find { it is FeeItem.Suggested } ?: feeItems.first { it is FeeItem.Market } } + val nonce = ((prevState as? FeeSelectorUM.Content)?.feeNonce as? FeeNonce.Nonce)?.nonce + return FeeSelectorUM.Content( fees = fees, feeItems = feeItems, selectedFeeItem = selectedFee, - isFeeApproximate = isFeeApproximate, - feeFiatRateUM = cryptoCurrencyStatus.value.fiatRate?.let { rate -> + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = isFeeApproximate, + isFeeConvertibleToFiat = feeCryptoCurrencyStatus.currency.network.hasFiatFeeRate, + isTronToken = cryptoCurrencyStatus.currency is CryptoCurrency.Token && + isTron(cryptoCurrencyStatus.currency.network.rawId), + ), + feeFiatRateUM = feeCryptoCurrencyStatus.value.fiatRate?.let { rate -> FeeFiatRateUM( rate = rate, appCurrency = appCurrency, ) }, - displayNonceInput = false, - nonce = null, - onNonceChange = {}, + feeNonce = if (fees.normal is Fee.Ethereum) { + FeeNonce.Nonce( + nonce = nonce, + onNonceChange = feeSelectorIntents::onNonceChange, + ) + } else { + FeeNonce.None + }, ) } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorNonceChangeTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorNonceChangeTransformer.kt new file mode 100644 index 0000000000..0450dc046f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorNonceChangeTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.send.v2.feeselector.model.transformers + +import com.tangem.features.send.v2.api.entity.FeeNonce +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.utils.transformer.Transformer + +internal class FeeSelectorNonceChangeTransformer( + private val value: String, +) : Transformer { + + override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { + val state = prevState as? FeeSelectorUM.Content ?: return prevState + val feeNonce = state.feeNonce as? FeeNonce.Nonce ?: return prevState + if (value.isEmpty()) { + return state.copy(feeNonce = feeNonce.copy(null)) + } + + val nonce = value.toBigIntegerOrNull() ?: return prevState + + return state.copy(feeNonce = feeNonce.copy(nonce = nonce)) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index b858c042ee..972b35167f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -10,15 +10,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.utils.getFiatString -import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.extensions.stringResourceSafe @@ -29,9 +31,8 @@ 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.send.v2.api.entity.FeeFiatRateUM -import com.tangem.features.send.v2.api.entity.FeeItem -import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.impl.R import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal @@ -50,21 +51,14 @@ internal fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = 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, - ) - SpacerWMax() + FeeSelectorDescription(state = state) + } +} + +@Composable +private fun FeeSelectorDescription(state: FeeSelectorUM, modifier: Modifier = Modifier) { + Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) { + FeeSelectorStaticPart(modifier = Modifier.weight(1f)) when (state) { is FeeSelectorUM.Content -> FeeContent(state) is FeeSelectorUM.Loading -> FeeLoading() @@ -74,7 +68,31 @@ internal fun FeeSelectorBlockContent(state: FeeSelectorUM, modifier: Modifier = } @Composable -private fun RowScope.FeeError() { +private fun FeeSelectorStaticPart(modifier: Modifier = Modifier) { + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f, fill = false), + text = stringResourceSafe(R.string.common_network_fee_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + 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, + ) + } +} + +@Composable +private fun FeeError() { Text( text = EMPTY_BALANCE_SIGN, color = TangemTheme.colors.text.primary1, @@ -83,7 +101,7 @@ private fun RowScope.FeeError() { } @Composable -private fun RowScope.FeeLoading() { +private fun FeeLoading() { TextShimmer( radius = TangemTheme.dimens.radius3, style = TangemTheme.typography.body1, @@ -92,62 +110,87 @@ private fun RowScope.FeeLoading() { } @Composable -private fun RowScope.FeeContent(state: FeeSelectorUM.Content) { +private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) { val fiatRate = state.feeFiatRateUM - EllipsisText( - text = if (fiatRate != null) { - getFiatString( - value = state.selectedFeeItem.fee.amount.value, - rate = fiatRate.rate, - appCurrency = fiatRate.appCurrency, - approximate = state.isFeeApproximate, - ) - } else { - state.selectedFeeItem.fee.amount.value.format { - crypto( - symbol = state.selectedFeeItem.fee.amount.currencySymbol, - decimals = state.selectedFeeItem.fee.amount.decimals, - ).fee(canBeLower = state.isFeeApproximate) - } - }, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.End, - modifier = Modifier - .weight(1f) - .padding(start = TangemTheme.dimens.spacing4), - ) - 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, - ) + Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + EllipsisText( + text = if (fiatRate != null) { + getFiatString( + value = state.selectedFeeItem.fee.amount.value, + rate = fiatRate.rate, + appCurrency = fiatRate.appCurrency, + approximate = state.feeExtraInfo.isFeeApproximate, + ) + } else { + state.selectedFeeItem.fee.amount.value.format { + crypto( + symbol = state.selectedFeeItem.fee.amount.currencySymbol, + decimals = state.selectedFeeItem.fee.amount.decimals, + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) + } + }, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + 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() { +private fun FeeSelectorBlockContent_Preview(@PreviewParameter(FeeSelectorUMProvider::class) state: FeeSelectorUM) { TangemThemePreview { - val feeItem = FeeItem.Market( - Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum)), - ) - FeeSelectorBlockContent( - modifier = Modifier.fillMaxWidth(), - state = FeeSelectorUM.Content( - feeItems = persistentListOf(feeItem), - selectedFeeItem = feeItem, - isFeeApproximate = false, - feeFiatRateUM = FeeFiatRateUM( - rate = BigDecimal("2500"), - appCurrency = AppCurrency.Default, - ), - displayNonceInput = false, - nonce = null, - onNonceChange = {}, - fees = TransactionFee.Single(feeItem.fee), - ), - ) + FeeSelectorBlockContent(modifier = Modifier.fillMaxWidth(), state = state) } +} + +private class FeeSelectorUMProvider : PreviewParameterProvider { + private val maxFeeItem = FeeItem.Market( + fee = Fee.Common(amount = Amount(value = BigDecimal("100000000"), blockchain = Blockchain.Ethereum)), + ) + private val lowFeeItem = + FeeItem.Market(Fee.Common(amount = Amount(value = BigDecimal("0.0002876"), blockchain = Blockchain.Ethereum))) + + override val values: Sequence = sequenceOf( + FeeSelectorUM.Content( + feeItems = persistentListOf(lowFeeItem), + selectedFeeItem = lowFeeItem, + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = true, + isTronToken = false, + ), + feeNonce = FeeNonce.None, + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("2500"), + appCurrency = AppCurrency.Default, + ), + fees = TransactionFee.Single(lowFeeItem.fee), + ), + FeeSelectorUM.Content( + feeItems = persistentListOf(maxFeeItem), + selectedFeeItem = maxFeeItem, + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = true, + isTronToken = false, + ), + feeNonce = FeeNonce.None, + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("2500000000000"), + appCurrency = AppCurrency.Default, + ), + fees = TransactionFee.Single(maxFeeItem.fee), + ), + FeeSelectorUM.Error(GetFeeError.UnknownError), + FeeSelectorUM.Loading, + ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt index ccde03a163..24445e46cf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorModalBottomSheet.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter +import com.tangem.core.ui.components.inputrow.InputRowEnter import com.tangem.core.ui.components.inputrow.InputRowEnterInfoAmountV2 import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.crypto @@ -45,10 +46,7 @@ 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.send.v2.api.entity.CustomFeeFieldUM -import com.tangem.features.send.v2.api.entity.FeeFiatRateUM -import com.tangem.features.send.v2.api.entity.FeeItem -import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.entity.* import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorIntents import com.tangem.features.send.v2.feeselector.model.StubFeeSelectorIntents @@ -57,7 +55,6 @@ import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -import java.math.BigInteger @Composable internal fun FeeSelectorModalBottomSheet( @@ -158,7 +155,7 @@ private fun FeeSelectorItems( crypto( symbol = item.fee.amount.currencySymbol, decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.isFeeApproximate) + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) }, ), postDot = if (feeFiatRateUM != null) { @@ -184,7 +181,7 @@ private fun FeeSelectorItems( crypto( symbol = item.fee.amount.currencySymbol, decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.isFeeApproximate) + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) }, ), postDot = if (feeFiatRateUM != null) { @@ -210,7 +207,7 @@ private fun FeeSelectorItems( crypto( symbol = item.fee.amount.currencySymbol, decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.isFeeApproximate) + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) }, ), postDot = if (feeFiatRateUM != null) { @@ -236,7 +233,7 @@ private fun FeeSelectorItems( crypto( symbol = item.fee.amount.currencySymbol, decimals = item.fee.amount.decimals, - ).fee(canBeLower = state.isFeeApproximate) + ).fee(canBeLower = state.feeExtraInfo.isFeeApproximate) }, ), postDot = if (feeFiatRateUM != null) { @@ -258,9 +255,7 @@ private fun FeeSelectorItems( iconBackgroundColor = iconBackgroundColor, iconTint = iconTint, onValueChange = feeSelectorIntents::onCustomFeeValueChange, - displayNonceInput = state.displayNonceInput, - nonce = state.nonce, - onNonceChange = state.onNonceChange, + nonce = state.feeNonce, ) } } @@ -275,9 +270,7 @@ private fun CustomFeeBlock( iconBackgroundColor: Color, iconTint: Color, onValueChange: (Int, String) -> Unit, - displayNonceInput: Boolean, - nonce: BigInteger?, - onNonceChange: (String) -> Unit, + nonce: FeeNonce, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -310,9 +303,7 @@ private fun CustomFeeBlock( ExpandedCustomFeeItems( customFeeFields = customFee.customValues, onValueChange = onValueChange, - displayNonceInput = displayNonceInput, nonce = nonce, - onNonceChange = onNonceChange, ) } } @@ -322,14 +313,12 @@ private fun CustomFeeBlock( private fun ExpandedCustomFeeItems( customFeeFields: ImmutableList, onValueChange: (Int, String) -> Unit, - displayNonceInput: Boolean, - nonce: BigInteger?, - onNonceChange: (String) -> Unit, + nonce: FeeNonce, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { customFeeFields.fastForEachIndexed { index, field -> - val showDivider = index != customFeeFields.size - 1 || displayNonceInput + val showDivider = index != customFeeFields.size - 1 || nonce is FeeNonce.Nonce if (field.label != null) { InputRowEnterInfoAmountV2( text = field.value, @@ -338,6 +327,7 @@ private fun ExpandedCustomFeeItems( title = field.title, titleColor = TangemTheme.colors.text.tertiary, info = field.label, + description = field.footer, keyboardOptions = field.keyboardOptions, keyboardActions = field.keyboardActions, onValueChange = { onValueChange(index, it) }, @@ -351,6 +341,7 @@ private fun ExpandedCustomFeeItems( title = field.title, titleColor = TangemTheme.colors.text.tertiary, symbol = field.symbol, + description = field.footer, onValueChange = { onValueChange(index, it) }, keyboardOptions = field.keyboardOptions, keyboardActions = field.keyboardActions, @@ -359,18 +350,21 @@ private fun ExpandedCustomFeeItems( } } - if (displayNonceInput) { - // TODO implement v2 input without binding to amount - InputRowEnterInfoAmountV2( - text = nonce?.toString() ?: "", - decimals = 0, + if (nonce is FeeNonce.Nonce) { + InputRowEnter( + text = nonce.nonce?.toString().orEmpty(), title = resourceReference(R.string.send_nonce), - titleColor = TangemTheme.colors.text.tertiary, - symbol = null, - onValueChange = onNonceChange, + description = resourceReference(R.string.send_nonce_footer), + onValueChange = nonce.onNonceChange, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - keyboardActions = KeyboardActions(), + placeholder = resourceReference(R.string.send_nonce_hint), + titleColor = TangemTheme.colors.text.secondary, showDivider = false, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), ) } } @@ -504,14 +498,16 @@ private class FeeSelectorUMContentProvider : CollectionPreviewParameterProvider< // amount = Amount(value = BigDecimal("0.02"), blockchain = Blockchain.Ethereum), // ), selectedFeeItem = customFeeItem, - isFeeApproximate = true, + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = true, + isFeeConvertibleToFiat = true, + isTronToken = false, + ), feeFiatRateUM = FeeFiatRateUM( rate = BigDecimal.TEN, appCurrency = AppCurrency.Default, ), - displayNonceInput = true, - onNonceChange = {}, - nonce = null, + feeNonce = FeeNonce.None, fees = TransactionFee.Single(customFeeItem.fee), ), ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index dd7a643b22..532911f4f6 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -21,6 +21,7 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendComponent import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents @@ -53,6 +54,7 @@ internal class DefaultSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SendComponent.Params, private val analyticsEventHandler: AnalyticsEventHandler, + private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory, ) : SendComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -298,6 +300,7 @@ internal class DefaultSendComponent @AssistedInject constructor( innerRouter.replaceAll(CommonSendRoute.ConfirmSuccess) }, ), + feeSelectorComponentFactory = feeSelectorComponentFactory, ) } else { model.showAlertError() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index df576bb18f..022f381f53 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -14,9 +14,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -35,6 +37,7 @@ import kotlinx.coroutines.flow.* internal class SendConfirmComponent( appComponentContext: AppComponentContext, params: Params, + private val feeSelectorComponentFactory: FeeSelectorBlockComponent.Factory, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: SendConfirmModel = getOrCreateModel(params = params) @@ -92,6 +95,19 @@ internal class SendConfirmComponent( onClick = model::showEditFee, ) + private val feeSelectorBlockComponent = feeSelectorComponentFactory.create( + context = appComponentContext, + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = params.onLoadFee, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + suggestedFeeState = model.suggestedFeeState, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + ), + onResult = model::onFeeResult, + ) + private val notificationsComponent = DefaultSendNotificationsComponent( appComponentContext = child("sendConfirmNotifications"), params = SendNotificationsComponent.Params( @@ -136,6 +152,7 @@ internal class SendConfirmComponent( destinationBlockComponent = destinationBlockComponent, amountBlockComponent = amountBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, notificationsComponent = notificationsComponent, notificationsUM = notificationState, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index e4bfe5b26f..594d12a547 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -35,7 +35,13 @@ import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeNonce +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory @@ -49,13 +55,13 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmIn import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer +import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformerV2 import com.tangem.features.send.v2.send.ui.state.SendUM import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString @@ -86,13 +92,14 @@ internal class SendConfirmModel @Inject constructor( private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, private val sendFeeCheckReloadListener: SendFeeCheckReloadListener, - private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger, + private val notificationsUpdateListener: SendNotificationsUpdateListener, private val alertFactory: SendConfirmAlertFactory, private val sendAnalyticHelper: SendAnalyticHelper, private val urlOpener: UrlOpener, private val shareManager: ShareManager, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, -) : Model(), SendConfirmClickIntents { +) : Model(), SendConfirmClickIntents, FeeSelectorModelCallback { private val params: SendConfirmComponent.Params = paramsContainer.require() @@ -115,6 +122,8 @@ internal class SendConfirmModel @Inject constructor( get() = uiState.value.feeUM as? FeeUM.Content private val feeSelectorUM get() = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + private val feeUMV2 + get() = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content val confirmData: ConfirmData get() = ConfirmData( @@ -129,6 +138,7 @@ internal class SendConfirmModel @Inject constructor( private var sendIdleTimer: Long = 0L private var isAmountSubtractAvailable = false + internal var suggestedFeeState: FeeSelectorParams.SuggestedFeeState = FeeSelectorParams.SuggestedFeeState.None init { modelScope.launch { @@ -300,16 +310,25 @@ internal class SendConfirmModel @Inject constructor( } private fun subscribeOnNotificationsUpdateTrigger() { - notificationsUpdateTrigger.hasErrorFlow + notificationsUpdateListener.hasErrorFlow .onEach { hasError -> _uiState.update { - val feeUM = it.feeUM as? FeeUM.Content - val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content - it.copy( - confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( - isPrimaryButtonEnabled = !hasError && feeSelectorUM != null, - ) ?: it.confirmUM, - ) + if (_uiState.value.isRedesignEnabled) { + val feeUM = it.feeSelectorUM as? FeeSelectorUMRedesigned.Content + it.copy( + confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + isPrimaryButtonEnabled = !hasError && feeUM != null, + ) ?: it.confirmUM, + ) + } else { + val feeUM = it.feeUM as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content + it.copy( + confirmUM = (it.confirmUM as? ConfirmUM.Content)?.copy( + isPrimaryButtonEnabled = !hasError && feeSelectorUM != null, + ) ?: it.confirmUM, + ) + } } } .launchIn(modelScope) @@ -319,9 +338,18 @@ internal class SendConfirmModel @Inject constructor( val amountValue = amountState?.amountTextField?.cryptoAmount?.value ?: return val destination = destinationUM?.addressTextField?.actualAddress ?: return val memo = destinationUM?.memoTextField?.value - val fee = feeSelectorUM?.selectedFee + val isRedesignEnabled = uiState.value.isRedesignEnabled + val fee = if (isRedesignEnabled) { + feeUMV2?.selectedFeeItem?.fee + } else { + feeSelectorUM?.selectedFee + } + val nonce = if (isRedesignEnabled) { + (feeUMV2?.feeNonce as? FeeNonce.Nonce)?.nonce + } else { + feeSelectorUM?.nonce + } val feeValue = fee?.amount?.value ?: return - val nonce = feeSelectorUM?.nonce val receivingAmount = checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isAmountSubtractAvailable, @@ -377,6 +405,7 @@ internal class SendConfirmModel @Inject constructor( CommonSendAnalyticEvents.TransactionError( categoryName = analyticsCategoryName, token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, ), ) }, @@ -448,14 +477,25 @@ internal class SendConfirmModel @Inject constructor( ) _uiState.update { it.copy( - confirmUM = SendConfirmationNotificationsTransformer( - feeUM = uiState.value.feeUM, - amountUM = uiState.value.amountUM, - analyticsEventHandler = analyticsEventHandler, - cryptoCurrency = cryptoCurrencyStatus.currency, - appCurrency = appCurrency, - analyticsCategoryName = params.analyticsCategoryName, - ).transform(uiState.value.confirmUM), + confirmUM = if (uiState.value.isRedesignEnabled) { + SendConfirmationNotificationsTransformerV2( + feeSelectorUM = uiState.value.feeSelectorUM, + amountUM = uiState.value.amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + appCurrency = appCurrency, + analyticsCategoryName = params.analyticsCategoryName, + ).transform(uiState.value.confirmUM) + } else { + SendConfirmationNotificationsTransformer( + feeUM = uiState.value.feeUM, + amountUM = uiState.value.amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrencyStatus.currency, + appCurrency = appCurrency, + analyticsCategoryName = params.analyticsCategoryName, + ).transform(uiState.value.confirmUM) + }, ) } } @@ -556,6 +596,12 @@ internal class SendConfirmModel @Inject constructor( ) } + override fun onFeeResult(feeSelectorUM: FeeSelectorUMRedesigned) { + sendIdleTimer = SystemClock.elapsedRealtime() + _uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + private companion object { const val CHECK_FEE_UPDATE_DELAY = 10_000L } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt new file mode 100644 index 0000000000..7252942833 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2.kt @@ -0,0 +1,110 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.common.utils.formatFooterFiatFee +import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText +import com.tangem.features.send.v2.impl.R +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class SendConfirmationNotificationsTransformerV2( + private val feeSelectorUM: FeeSelectorUM, + private val amountUM: AmountState, + private val analyticsEventHandler: AnalyticsEventHandler, + private val cryptoCurrency: CryptoCurrency, + private val appCurrency: AppCurrency, + private val analyticsCategoryName: String, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + val state = prevState as? ConfirmUM.Content ?: return prevState + val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content ?: return prevState + return state.copy( + sendingFooter = getSendingFooterText(), + notifications = buildList { + addTooHighNotification(feeSelectorUM) + addTooLowNotification(feeSelectorUM) + }.toPersistentList(), + ) + } + + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { + if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) { + add(NotificationUM.Warning.FeeTooLow) + analyticsEventHandler.send( + CommonSendAnalyticEvents.NoticeTransactionDelays( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), + ) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) { + val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } + + private fun getSendingFooterText(): TextReference { + val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content + val amountUM = amountUM as? AmountState.Data + val fee = feeSelectorUM?.selectedFeeItem?.fee + + if (fee == null || amountUM == null) return TextReference.EMPTY + + val fiatAmountValue = amountUM.amountTextField.fiatAmount.value + val fiatFeeValue = feeSelectorUM.feeFiatRateUM?.rate?.let { fee.amount.value?.multiply(it) } + + val fiatSendingValue = if (feeSelectorUM.feeFiatRateUM != null) { + fiatFeeValue?.let { fiatAmountValue?.plus(it) } + } else { + fiatAmountValue + } + + val fiatSending = fiatSendingValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + val fiatFee = formatFooterFiatFee( + amount = fee.amount.copy(value = fiatFeeValue), + isFeeConvertibleToFiat = feeSelectorUM.feeFiatRateUM != null, + isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate, + appCurrency = appCurrency, + ) + + return if (fee is Fee.Tron) { + getTronTokenFeeSendingText( + fee = fee, + fiatFee = fiatFee, + fiatSending = stringReference(fiatSending), + ) + } else { + resourceReference( + id = if (feeSelectorUM.feeFiatRateUM != null) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList(fiatSending, fiatFee), + ) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt index 738c7e22ab..21e776b4ad 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/ui/SendConfirmContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.v2.send.confirm.ui import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -9,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerHMax @@ -19,6 +21,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.common.ui.SendingText import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.common.ui.tapHelp @@ -40,6 +43,7 @@ internal fun SendConfirmContent( destinationBlockComponent: DefaultSendDestinationBlockComponent, amountBlockComponent: SendAmountBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, notificationsComponent: DefaultSendNotificationsComponent, notificationsUM: ImmutableList, ) { @@ -54,6 +58,7 @@ internal fun SendConfirmContent( destinationBlockComponent = destinationBlockComponent, amountBlockComponent = amountBlockComponent, feeBlockComponent = feeBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, ) if (confirmUM != null) { tapHelp(isDisplay = confirmUM.showTapHelp) @@ -79,18 +84,24 @@ private fun LazyListScope.blocks( destinationBlockComponent: DefaultSendDestinationBlockComponent, amountBlockComponent: SendAmountBlockComponent, feeBlockComponent: SendFeeBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, ) { item(key = BLOCKS_KEY) { Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { if (uiState.isRedesignEnabled) { amountBlockComponent.Content(modifier = Modifier) destinationBlockComponent.Content(modifier = Modifier) + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) } else { TransactionDoneTitleAnimated(uiState) destinationBlockComponent.Content(modifier = Modifier) amountBlockComponent.Content(modifier = Modifier) + feeBlockComponent.Content(modifier = Modifier) } - feeBlockComponent.Content(modifier = Modifier) } } } 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 96a985f08d..c9ecb3d667 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 @@ -39,6 +39,7 @@ 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.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM @@ -395,5 +396,6 @@ internal class SendModel @Inject constructor( navigationUM = NavigationUM.Empty, isRedesignEnabled = sendFeatureToggles.isSendRedesignEnabled, confirmData = null, + feeSelectorUM = FeeSelectorUM.Loading, ) } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt index df436f0170..3599e91e56 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.send.ui.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM import com.tangem.features.send.v2.common.ui.state.ConfirmUM import com.tangem.features.send.v2.send.confirm.model.ConfirmData @@ -11,6 +12,7 @@ internal data class SendUM( val amountUM: AmountState, val destinationUM: DestinationUM, val feeUM: FeeUM, + val feeSelectorUM: FeeSelectorUM, val confirmUM: ConfirmUM, val navigationUM: NavigationUM, val isRedesignEnabled: Boolean, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index fb152c1b84..6b5874101e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -30,6 +30,8 @@ import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.features.nft.entity.NFTSendSuccessTrigger import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater import com.tangem.features.send.v2.common.SendConfirmAlertFactory @@ -47,7 +49,6 @@ import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update @@ -72,7 +73,8 @@ internal class NFTSendConfirmModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger, + private val notificationsUpdateListener: SendNotificationsUpdateListener, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, private val sendFeeCheckReloadListener: SendFeeCheckReloadListener, private val alertFactory: SendConfirmAlertFactory, @@ -242,7 +244,7 @@ internal class NFTSendConfirmModel @Inject constructor( } private fun subscribeOnNotificationsUpdateTrigger() { - notificationsUpdateTrigger.hasErrorFlow + notificationsUpdateListener.hasErrorFlow .onEach { hasError -> _uiState.update { val feeUM = it.feeUM as? FeeUM.Content @@ -310,6 +312,7 @@ internal class NFTSendConfirmModel @Inject constructor( CommonSendAnalyticEvents.TransactionError( categoryName = analyticsCategoryName, token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, ), ) }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index 16ba4a5cef..97a7e61118 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -94,7 +94,6 @@ internal class SendAmountModel @Inject constructor( }, ifRight = { wallet -> userWallet = wallet - appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } }, ) }.launchIn(modelScope) @@ -128,6 +127,8 @@ internal class SendAmountModel @Inject constructor( ) } + appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } + if (uiState.value is AmountState.Data) { _uiState.update( AmountBoundaryUpdateTransformer( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt index 3b51b887d7..829a28873d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/DefaultSendDestinationBlockComponent.kt @@ -33,7 +33,7 @@ internal class DefaultSendDestinationBlockComponent @AssistedInject constructor( }.launchIn(componentScope) } - fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM) + override fun updateState(destinationUM: DestinationUM) = model.updateState(destinationUM) @Composable override fun Content(modifier: Modifier) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultNotificationsUpdateTrigger.kt similarity index 63% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultNotificationsUpdateTrigger.kt index ed7123bbcd..8f4494843f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/NotificationsUpdateTrigger.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/DefaultNotificationsUpdateTrigger.kt @@ -1,28 +1,17 @@ package com.tangem.features.send.v2.subcomponents.notifications import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData -import kotlinx.coroutines.flow.Flow +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import javax.inject.Inject import javax.inject.Singleton -interface NotificationsUpdateTrigger { - /** Flow triggers notifications update */ - val updateTriggerFlow: Flow - - /** Flow returns whether there is error notifications */ - val hasErrorFlow: Flow - - /** Trigger return callback with check result */ - suspend fun callbackHasError(hasError: Boolean) - - /** Trigger fee check reload */ - suspend fun triggerUpdate(data: NotificationData) -} - @Singleton -internal class DefaultNotificationsUpdateTrigger @Inject constructor() : NotificationsUpdateTrigger { +internal class DefaultNotificationsUpdateTrigger @Inject constructor() : + SendNotificationsUpdateListener, + SendNotificationsUpdateTrigger { private val _updateTriggerFlow = MutableSharedFlow() override val updateTriggerFlow = _updateTriggerFlow.asSharedFlow() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt index 2c52cdeab5..99e88f56e9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/di/NotificationsModule.kt @@ -1,20 +1,23 @@ package com.tangem.features.send.v2.subcomponents.notifications.di +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import com.tangem.features.send.v2.subcomponents.notifications.DefaultNotificationsUpdateTrigger -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger +import dagger.Binds 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 NotificationsModule { +internal interface NotificationsModule { - @Provides @Singleton - fun providesNotificationsUpdateTrigger(): NotificationsUpdateTrigger { - return DefaultNotificationsUpdateTrigger() - } + @Binds + fun bindsNotificationsUpdateTrigger(impl: DefaultNotificationsUpdateTrigger): SendNotificationsUpdateTrigger + + @Singleton + @Binds + fun bindsNotificationsUpdateListener(impl: DefaultNotificationsUpdateTrigger): SendNotificationsUpdateListener } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 726be9421e..510533e56d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -37,12 +37,13 @@ import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger import com.tangem.features.send.v2.subcomponents.fee.SendFeeData import com.tangem.features.send.v2.subcomponents.fee.SendFeeReloadTrigger import com.tangem.features.send.v2.subcomponents.fee.model.checkAndCalculateSubtractedAmount import com.tangem.features.send.v2.subcomponents.fee.model.checkFeeCoverage -import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger import com.tangem.features.send.v2.subcomponents.notifications.analytics.NotificationsAnalyticEvents import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTron @@ -74,7 +75,8 @@ internal class NotificationsModel @Inject constructor( private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase, private val sendFeeReloadTrigger: SendFeeReloadTrigger, private val sendAmountReduceTrigger: SendAmountReduceTrigger, - private val notificationsUpdateTrigger: NotificationsUpdateTrigger, + private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger, + private val notificationsUpdateListener: SendNotificationsUpdateListener, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -101,7 +103,7 @@ internal class NotificationsModel @Inject constructor( } private fun subscribeToNotificationUpdateTrigger() { - notificationsUpdateTrigger.updateTriggerFlow + notificationsUpdateListener.updateTriggerFlow .onEach { updateState(it) } .launchIn(modelScope) } diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt new file mode 100644 index 0000000000..6eece69628 --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerTest.kt @@ -0,0 +1,439 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale +import org.junit.jupiter.api.BeforeAll + +class SendConfirmationNotificationsTransformerTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeUM: FeeUM = mockk(relaxed = true) + val amountUM: AmountState = mockk(relaxed = true) + val transformer = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM = ConfirmUM.Empty + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN fee UM not content WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeUM: FeeUM = FeeUM.Empty() + val amountUM: AmountState = mockk(relaxed = true) + val transformer = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM.Content = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest { + // GIVEN + val feeUM = createNormalFeeUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).isEmpty() + assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter) + } + + @Test + fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest { + // GIVEN + val feeUM = createFeeTooHighUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + } + + @Test + fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { + // GIVEN + val feeUM = createFeeTooLowUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + verify { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest { + // GIVEN + val feeUM = createFeeTooHighAndTooLowUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(2) + assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createTestAmountUM(): AmountState.Data { + val cryptoAmount = com.tangem.domain.tokens.model.Amount( + currencySymbol = "SOL", + value = BigDecimal("1.5"), + decimals = 8, + ) + val fiatAmount = com.tangem.domain.tokens.model.Amount( + currencySymbol = "USD", + value = BigDecimal("50.00"), + decimals = 2, + ) + + return AmountState.Data( + isPrimaryButtonEnabled = true, + isRedesignEnabled = false, + title = mockk(relaxed = true), + availableBalance = mockk(relaxed = true), + tokenName = mockk(relaxed = true), + tokenIconState = mockk(relaxed = true), + segmentedButtonConfig = persistentListOf(), + selectedButton = 0, + isSegmentedButtonsEnabled = false, + amountTextField = AmountFieldModel( + value = "1.5", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + cryptoAmount = cryptoAmount, + fiatAmount = fiatAmount, + isFiatValue = false, + fiatValue = "50.00", + isFiatUnavailable = false, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + isError = false, + isWarning = false, + error = mockk(relaxed = true), + ), + appCurrency = appCurrency, + isEditingDisabled = false, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + ) + } + + private fun createNormalFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf(), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooLowUM(): FeeUM.Content { + val fee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = com.tangem.blockchain.common.Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt new file mode 100644 index 0000000000..859eb4e9eb --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -0,0 +1,505 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale +import com.tangem.domain.tokens.model.Amount as DomainAmount + +class SendConfirmationNotificationsTransformerV2Test { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeSelectorUM: FeeSelectorUM = mockk(relaxed = true) + val amountUM: AmountState = mockk(relaxed = true) + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState: ConfirmUM = ConfirmUM.Empty + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN fee selector not content WHEN transform THEN returns original state`() = runTest { + // GIVEN + val feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + val amountUM: AmountState = mockk(relaxed = true) + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isEqualTo(initialState) + } + + @Test + fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest { + // GIVEN + val feeSelectorUM = createNormalFeeSelectorUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).isEmpty() + assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter) + } + + @Test + fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooHighUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + } + + @Test + fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooLowUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(1) + assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + verify { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest { + // GIVEN + val feeSelectorUM = createFeeTooHighAndTooLowUM() + val amountUM = createTestAmountUM() + val transformer = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + val initialState = createTestConfirmUM() + + // WHEN + val result = transformer.transform(initialState) + + // THEN + assertThat(result).isInstanceOf(ConfirmUM.Content::class.java) + val content = result as ConfirmUM.Content + assertThat(content.notifications).hasSize(2) + assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createTestAmountUM(): AmountState.Data { + val cryptoAmount = DomainAmount( + currencySymbol = "SOL", + value = BigDecimal("1.5"), + decimals = 8, + ) + val fiatAmount = DomainAmount( + currencySymbol = "USD", + value = BigDecimal("50.00"), + decimals = 2, + ) + + return AmountState.Data( + isPrimaryButtonEnabled = true, + isRedesignEnabled = false, + title = mockk(relaxed = true), + availableBalance = mockk(relaxed = true), + tokenName = mockk(relaxed = true), + tokenIconState = mockk(relaxed = true), + segmentedButtonConfig = persistentListOf(), + selectedButton = 0, + isSegmentedButtonsEnabled = false, + amountTextField = AmountFieldModel( + value = "1.5", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + cryptoAmount = cryptoAmount, + fiatAmount = fiatAmount, + isFiatValue = false, + fiatValue = "50.00", + isFiatUnavailable = false, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + isError = false, + isWarning = false, + error = mockk(relaxed = true), + ), + appCurrency = appCurrency, + isEditingDisabled = false, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + ) + } + + private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUM.Content( + fees = transactionFee, + feeItems = persistentListOf(FeeItem.Market(fee)), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighUM(): FeeSelectorUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUM.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooLowUM(): FeeSelectorUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeSelectorUM.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeSelectorUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUM.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = Fee.Common( + amount = Amount( + currencySymbol = "SOL", + value = BigDecimal("0.008"), + decimals = 8, + ), + ), + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "SOL", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt new file mode 100644 index 0000000000..f197f57b08 --- /dev/null +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/TransformersComparisonTest.kt @@ -0,0 +1,870 @@ +package com.tangem.features.send.v2.send.confirm.model.transformers + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.send.v2.api.entity.* +import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import java.math.BigDecimal +import java.math.BigInteger +import java.util.Locale +import com.tangem.domain.tokens.model.Amount as DomainAmount +import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2 + +class TransformersComparisonTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) + private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") + private val analyticsCategoryName = "test_category" + + @Test + fun `GIVEN equivalent input data WHEN both transformers transform THEN they produce equal ConfirmUM`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + val amountUM = createTestAmountUM() + + val feeUM = createTestFeeUM() + val feeSelectorUMV2 = createTestFeeSelectorUMV2() + + // WHEN + val transformerV1 = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications.size).isEqualTo(contentV2.notifications.size) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled) + assertThat(contentV1.isSending).isEqualTo(contentV2.isSending) + assertThat(contentV1.showTapHelp).isEqualTo(contentV2.showTapHelp) + } + + @Test + fun `GIVEN fee too low WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + val amountUM = createTestAmountUM() + + val feeUM = createFeeTooLowUM() + val feeSelectorUMV2 = createFeeTooLowUMV2() + + // WHEN + val transformerV1 = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications).hasSize(1) + assertThat(contentV2.notifications).hasSize(1) + assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + + // Verify analytics event was sent for both transformers + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN fee too high WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + val amountUM = createTestAmountUM() + + val feeUM = createFeeTooHighUM() + val feeSelectorUMV2 = createFeeTooHighUMV2() + + // WHEN + val transformerV1 = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications).hasSize(1) + assertThat(contentV2.notifications).hasSize(1) + assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java) + + val tooHighV1 = contentV1.notifications.first() as NotificationUM.Warning.TooHigh + val tooHighV2 = contentV2.notifications.first() as NotificationUM.Warning.TooHigh + assertThat(tooHighV1.value).isEqualTo(tooHighV2.value) + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + } + + @Test + fun `GIVEN fee both too high and too low WHEN both transformers transform THEN they produce equal notifications`() = + runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + val amountUM = createTestAmountUM() + + val feeUM = createFeeTooHighAndTooLowUM() + val feeSelectorUMV2 = createFeeTooHighAndTooLowUMV2() + + // WHEN + val transformerV1 = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications).hasSize(2) + assertThat(contentV2.notifications).hasSize(2) + + assertThat(contentV1.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + assertThat(contentV1.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + assertThat(contentV2.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue() + assertThat(contentV2.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue() + + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + + verify(exactly = 2) { analyticsEventHandler.send(any()) } + } + + @Test + fun `GIVEN normal fee WHEN both transformers transform THEN they produce equal notifications`() = runTest { + // GIVEN + val initialConfirmUM = createTestConfirmUM() + val amountUM = createTestAmountUM() + + val feeUM = createNormalFeeUM() + val feeSelectorUMV2 = createNormalFeeSelectorUMV2() + + // WHEN + val transformerV1 = SendConfirmationNotificationsTransformer( + feeUM = feeUM, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val transformerV2 = SendConfirmationNotificationsTransformerV2( + feeSelectorUM = feeSelectorUMV2, + amountUM = amountUM, + analyticsEventHandler = analyticsEventHandler, + cryptoCurrency = cryptoCurrency, + appCurrency = appCurrency, + analyticsCategoryName = analyticsCategoryName, + ) + + val resultV1 = transformerV1.transform(initialConfirmUM) + val resultV2 = transformerV2.transform(initialConfirmUM) + + // THEN + assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java) + assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java) + + val contentV1 = resultV1 as ConfirmUM.Content + val contentV2 = resultV2 as ConfirmUM.Content + + assertThat(contentV1.notifications).isEmpty() + assertThat(contentV2.notifications).isEmpty() + assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter) + } + + private fun createTestConfirmUM(): ConfirmUM.Content { + return ConfirmUM.Content( + isPrimaryButtonEnabled = true, + walletName = mockk(relaxed = true), + isSending = false, + showTapHelp = false, + sendingFooter = mockk(relaxed = true), + notifications = persistentListOf(), + ) + } + + private fun createTestAmountUM(): AmountState.Data { + val cryptoAmount = DomainAmount( + currencySymbol = "TST", + value = BigDecimal("1.5"), + decimals = 8, + ) + val fiatAmount = DomainAmount( + currencySymbol = "USD", + value = BigDecimal("50.00"), + decimals = 2, + ) + + return AmountState.Data( + isPrimaryButtonEnabled = true, + isRedesignEnabled = false, + title = mockk(relaxed = true), + availableBalance = mockk(relaxed = true), + tokenName = mockk(relaxed = true), + tokenIconState = mockk(relaxed = true), + segmentedButtonConfig = persistentListOf(), + selectedButton = 0, + isSegmentedButtonsEnabled = false, + amountTextField = AmountFieldModel( + value = "1.5", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + cryptoAmount = cryptoAmount, + fiatAmount = fiatAmount, + isFiatValue = false, + fiatValue = "50.00", + isFiatUnavailable = false, + isValuePasted = false, + onValuePastedTriggerDismiss = {}, + isError = false, + isWarning = false, + error = mockk(relaxed = true), + ), + appCurrency = appCurrency, + isEditingDisabled = false, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + ) + } + + private fun createTestFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createTestFeeSelectorUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUMV2.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Market(fee), + ), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooLowUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooLowUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.0001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = fee, + priority = fee, + ) + return FeeSelectorUMV2.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.0001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighUMV2(): FeeSelectorUMV2.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUMV2.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.01", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createFeeTooHighAndTooLowUM(): FeeUM.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Custom, + selectedFee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = true, + notifications = persistentListOf(), + ) + } + + private fun createFeeTooHighAndTooLowUMV2(): FeeSelectorUMV2.Content { + val priorityFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val minimumFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.01"), + decimals = 8, + ), + ) + val customFee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.008"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Choosable( + minimum = minimumFee, + normal = minimumFee, + priority = priorityFee, + ) + return FeeSelectorUMV2.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + ), + selectedFeeItem = FeeItem.Custom( + fee = customFee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.008", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + private fun createNormalFeeUM(): FeeUM.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeUM.Content( + feeSelectorUM = FeeSelectorUM.Content( + fees = transactionFee, + selectedType = FeeType.Market, + selectedFee = fee, + customValues = persistentListOf( + CustomFeeFieldUM( + value = "0.001", + onValueChange = {}, + keyboardOptions = mockk(relaxed = true), + keyboardActions = mockk(relaxed = true), + symbol = "TST", + decimals = 8, + title = mockk(relaxed = true), + footer = mockk(relaxed = true), + ), + ), + nonce = BigInteger.ZERO, + ), + rate = BigDecimal("50000"), + isFeeConvertibleToFiat = true, + isFeeApproximate = false, + isTronToken = false, + isEditingDisabled = false, + isPrimaryButtonEnabled = true, + appCurrency = AppCurrency.Default, + isCustomSelected = false, + notifications = persistentListOf(), + ) + } + + private fun createNormalFeeSelectorUMV2(): FeeSelectorUMV2.Content { + val fee = Fee.Common( + amount = Amount( + currencySymbol = "TST", + value = BigDecimal("0.001"), + decimals = 8, + ), + ) + val transactionFee = TransactionFee.Single(fee) + return FeeSelectorUMV2.Content( + fees = transactionFee, + feeItems = persistentListOf( + FeeItem.Market(fee), + ), + selectedFeeItem = FeeItem.Market(fee), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + ), + feeFiatRateUM = FeeFiatRateUM( + rate = BigDecimal("50000"), + appCurrency = appCurrency, + ), + feeNonce = FeeNonce.Nonce( + nonce = BigInteger.ZERO, + onNonceChange = {}, + ), + ) + } + + companion object { + @JvmStatic + @BeforeAll + fun setUpLocale() { + Locale.setDefault(Locale.US) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index ebfa0a6ffd..762201aacb 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -47,8 +47,6 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.decompose) - implementation(projects.core.deepLinks) - /** Domain */ implementation(projects.domain.tokens) @@ -65,6 +63,7 @@ dependencies { implementation(projects.domain.transaction) implementation(projects.domain.transaction.models) implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.notifications.models) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index 19c9e9e0b2..d1d3a3065f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -3,9 +3,9 @@ package com.tangem.features.staking.impl.deeplink import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 2454768e9c..1f75fd46e4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -28,7 +28,6 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.staking.* import com.tangem.domain.staking.analytics.StakeScreenSource @@ -37,6 +36,7 @@ import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.tokens.* @@ -120,6 +120,7 @@ internal class StakingModel @Inject constructor( private val getActionsUseCase: GetActionsUseCase, private val getYieldUseCase: GetYieldUseCase, private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase, + private val getActionRequirementAmountUseCase: GetActionRequirementAmountUseCase, private val paramsInterceptorHolder: ParamsInterceptorHolder, private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, @@ -527,9 +528,20 @@ internal class StakingModel @Inject constructor( val rewardPendingActionConstraints = yieldBalance?.reward?.rewardConstraints if (rewardBlockType == RewardBlockType.RewardsRequirementsError) { + val minimumAmount = rewardPendingActionConstraints?.amountArg?.minimum + // Temporary fix, until StakeKit adds minimum requirement amount to balance response + val minimumAmountValue = if (minimumAmount == null && yieldBalance.integrationId != null) { + getActionRequirementAmountUseCase.invoke( + integrationId = yieldBalance.integrationId, + actionType = StakingActionType.CLAIM_REWARDS, + ).getOrNull() + } else { + minimumAmount + } + stakingEventFactory.createStakingRewardsMinimumRequirementsErrorAlert( cryptoCurrencyName = cryptoCurrencyStatus.currency.name, - cryptoAmountValue = rewardPendingActionConstraints?.amountArg?.minimum?.format { + cryptoAmountValue = minimumAmountValue?.format { crypto(cryptoCurrencyStatus.currency) }.orEmpty(), ) @@ -931,11 +943,6 @@ internal class StakingModel @Inject constructor( isSingleWalletWithTokens = false, ) .conflate() - .filter { - val sources = it.getOrNull()?.value?.sources ?: return@filter true - - sources.networkSource == StatusSource.ACTUAL && sources.yieldBalanceSource == StatusSource.ACTUAL - } .distinctUntilChanged() .filter { value.currentStep == StakingStep.InitialInfo } .onEach { maybeStatus -> diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index db102751d3..a4b3ea96fd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -9,6 +9,7 @@ import java.math.BigDecimal @Immutable internal sealed class InnerYieldBalanceState { data class Data( + val integrationId: String?, val reward: YieldReward, val isActionable: Boolean, val balances: ImmutableList, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index 9d5d52d214..ee906adb33 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -50,6 +50,7 @@ internal class YieldBalancesConverter( ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } InnerYieldBalanceState.Data( + integrationId = yieldBalance?.integrationId, reward = YieldReward( rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, rewardsFiat = fiatRewardsValue.format { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index e6c32aa82c..61431deb79 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -43,7 +43,6 @@ internal class StakingBalanceUpdater @AssistedInject constructor( fetchStakingYieldBalanceUseCase( userWalletId = userWallet.walletId, cryptoCurrency = cryptoCurrencyStatus.currency, - isRefactoringEnabled = true, ) }, // we should update tx history and network for new balances @@ -64,7 +63,11 @@ internal class StakingBalanceUpdater @AssistedInject constructor( coroutineScope { listOf( async { - fetchCurrencyStatus() + /* + * It is important to use NonCancellable here to ensure the update is not interrupted midway. + * For example, this can happen if the user enters and immediately leaves the screen. + */ + withContext(NonCancellable) { fetchCurrencyStatus() } }, async { updateStakingActions() @@ -78,7 +81,6 @@ internal class StakingBalanceUpdater @AssistedInject constructor( fetchCurrencyStatusUseCase( userWalletId = userWallet.walletId, id = cryptoCurrencyStatus.currency.id, - refresh = true, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 1c9a799920..d7af6496e2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -64,6 +64,7 @@ internal object InitialStakingStatePreview { val stateWithYield = defaultState.copy( yieldBalance = InnerYieldBalanceState.Data( + integrationId = null, reward = YieldReward( rewardsFiat = "100 $", rewardsCrypto = "100 SOL", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index c6249834ec..da281e339d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.utils import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.BalanceState @@ -44,16 +45,18 @@ internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boole return isSingleAction && !isRestake || isCompositePendingActions } -internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isStubUnstakeAction(networkId)) { - activeStake.pendingActions.plus( - PendingAction( - type = StakingActionType.UNSTAKE, - passthrough = "", - args = null, - ), - ).toPersistentList() -} else { - activeStake.pendingActions +internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState): ImmutableList { + return if (isStubUnstakeAction(networkId) && activeStake.type != BalanceType.REWARDS) { + activeStake.pendingActions.plus( + PendingAction( + type = StakingActionType.UNSTAKE, + passthrough = "", + args = null, + ), + ).toPersistentList() + } else { + activeStake.pendingActions + } } internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean { diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index 817039a569..8c880b498a 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { implementation(projects.common.ui) implementation(projects.common.routing) + /** Libs */ + implementation(projects.libs.crypto) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } @@ -54,6 +57,9 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.transaction) implementation(projects.domain.legacy) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.settings) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index 2ba34fa932..26267a4710 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -19,8 +19,11 @@ internal sealed class SwapAmountUM { abstract val primaryAmount: SwapAmountFieldUM abstract val secondaryAmount: SwapAmountFieldUM abstract val selectedAmountType: SwapAmountType + abstract val swapDirection: SwapDirection - data object Empty : SwapAmountUM() { + data class Empty( + override val swapDirection: SwapDirection, + ) : SwapAmountUM() { override val isPrimaryButtonEnabled = false override val selectedAmountType = SwapAmountType.From override val primaryAmount = SwapAmountFieldUM.Empty(SwapAmountType.From) @@ -29,16 +32,16 @@ internal sealed class SwapAmountUM { data class Content( override val isPrimaryButtonEnabled: Boolean, + override val swapDirection: SwapDirection, + override val selectedAmountType: SwapAmountType, // two amount fields override val primaryAmount: SwapAmountFieldUM, override val secondaryAmount: SwapAmountFieldUM, - override val selectedAmountType: SwapAmountType, val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, // selected swap route - val swapDirection: SwapDirection, val swapRateType: ExpressRateType, // swap models diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 4debca50a0..f722d3e1ea 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -13,13 +13,9 @@ 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.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError -import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapQuoteModel @@ -38,17 +34,17 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountUpdateListener import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountReadyStateConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapQuoteUMConverter import com.tangem.features.swap.v2.impl.amount.model.transformers.* import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM -import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate @@ -77,18 +73,11 @@ internal class SwapAmountModel @Inject constructor( private var userWallet = params.userWallet private var primaryCryptoCurrency: CryptoCurrency = params.primaryCryptoCurrencyStatusFlow.value.currency - private var secondaryCryptoCurrency: CryptoCurrency? = params.secondaryCryptoCurrency - - private var primaryCryptoCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value - private var secondaryCryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( - currency = primaryCryptoCurrencyStatus.currency, - value = CryptoCurrencyStatus.Loading, - ) private var primaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() - private var secondaryMaximumAmountBoundary: EnterAmountBoundary by Delegates.notNull() private var primaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() - private var secondaryMinimumAmountBoundary: EnterAmountBoundary by Delegates.notNull() + private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null + private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null val bottomSheetNavigation: SlotNavigation = SlotNavigation() @@ -103,7 +92,6 @@ internal class SwapAmountModel @Inject constructor( subscribeOnCryptoCurrencyStatusFlow() subscribeOnAmountUpdateTriggerUpdates() observeChooseSelectToken() - // todo observe balance hiding flow } fun updateState(amountUM: SwapAmountUM) { @@ -132,11 +120,10 @@ internal class SwapAmountModel @Inject constructor( override fun onInfoClick() { val amountUM = uiState.value as? SwapAmountUM.Content ?: return val selectedProvider = amountUM.selectedQuote.provider ?: return - val cryptoCurrency = secondaryCryptoCurrency ?: return swapAlertFactory.priceImpactAlert( hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null, - currencySymbol = cryptoCurrency.symbol, + currencySymbol = amountUM.primaryCryptoCurrencyStatus.currency.symbol, provider = selectedProvider, ) } @@ -193,32 +180,58 @@ internal class SwapAmountModel @Inject constructor( } override fun onSeparatorClick() { - // todo send with swap make swap types + // todo handle reverse pair click with swap redesign val amountFieldData = uiState.value.primaryAmount.amountField as? AmountState.Data val callback = (params as? SwapAmountComponentParams.AmountParams)?.callback ?: return + val primaryCryptoCurrencyStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus + if (primaryCryptoCurrencyStatus != null) { + uiState.transformerUpdate( + SwapAmountPrimaryReadyStateTransformer( + userWallet = userWallet, + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + appCurrency = appCurrency, + swapDirection = swapDirection, + clickIntents = this, + isBalanceHidden = params.isBalanceHidingFlow.value, + ), + ) + } + callback.onSeparatorClick(lastAmount = amountFieldData?.amountTextField?.value.orEmpty()) + callback.onAmountResult(uiState.value) } private fun subscribeOnCryptoCurrencyStatusFlow() { - params.primaryCryptoCurrencyStatusFlow.onEach { primaryCurrencyStatus -> - primaryCryptoCurrencyStatus = primaryCurrencyStatus - - val state = uiState.value - if (state is SwapAmountUM.Content) { - secondaryCryptoCurrency = state.primaryCryptoCurrencyStatus.currency - secondaryCryptoCurrencyStatus = state.primaryCryptoCurrencyStatus + params.primaryCryptoCurrencyStatusFlow + .distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes + .onEach { primaryCurrencyStatus -> + val secondaryStatus = (uiState.value as? SwapAmountUM.Content)?.secondaryCryptoCurrencyStatus initCurrencies( - primaryStatus = state.primaryCryptoCurrencyStatus, - secondaryStatus = state.secondaryCryptoCurrencyStatus, + primaryStatus = primaryCurrencyStatus, + secondaryStatus = secondaryStatus, ) - } else { - initPairs( - primaryCryptoCurrency = primaryCryptoCurrency, - secondaryCryptoCurrency = secondaryCryptoCurrency, - ) - } - }.launchIn(modelScope) + if (secondaryStatus != null) { + uiState.transformerUpdate( + SwapAmountUpdateBalanceTransformer( + cryptoCurrencyStatus = primaryCurrencyStatus, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + ), + ) + } else { + uiState.transformerUpdate( + SwapAmountPrimaryReadyStateTransformer( + userWallet = userWallet, + primaryCryptoCurrencyStatus = primaryCurrencyStatus, + appCurrency = appCurrency, + swapDirection = swapDirection, + clickIntents = this, + isBalanceHidden = params.isBalanceHidingFlow.value, + ), + ) + } + }.launchIn(modelScope) } private fun subscribeOnAmountUpdateTriggerUpdates() { @@ -226,6 +239,7 @@ internal class SwapAmountModel @Inject constructor( .onEach { if (uiState.value is SwapAmountUM.Content) { onAmountValueChange(it) + saveResult() } } .launchIn(modelScope) @@ -246,12 +260,12 @@ internal class SwapAmountModel @Inject constructor( amountUM } } - initPairs(primaryCryptoCurrency, currency) + initPairs(currency) } .launchIn(modelScope) } - private fun initPairs(primaryCryptoCurrency: CryptoCurrency, secondaryCryptoCurrency: CryptoCurrency?) { + private fun initPairs(secondaryCryptoCurrency: CryptoCurrency?) { modelScope.launch { val cryptoCurrencyStatusList = getMultiCryptoCurrencyStatusUseCase .invokeMultiWalletSync(userWallet.walletId) @@ -277,22 +291,22 @@ internal class SwapAmountModel @Inject constructor( swapDirection = params.swapDirection, ) - if (secondaryStatus != null) { - initCurrencies(primaryCryptoCurrencyStatus, secondaryStatus) - this@SwapAmountModel.secondaryCryptoCurrency = secondaryCryptoCurrency - secondaryCryptoCurrencyStatus = secondaryStatus - uiState.update { - SwapAmountReadyStateConverter( - swapCurrencies = swapCurrencies, + val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus + if (secondaryStatus != null && primaryStatus != null) { + initCurrencies(primaryStatus, secondaryStatus) + uiState.transformerUpdate( + SwapAmountSecondaryReadyStateTransformer( userWallet = userWallet, - primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + swapCurrencies = swapCurrencies, + primaryCryptoCurrencyStatus = primaryStatus, + secondaryCryptoCurrencyStatus = secondaryStatus, appCurrency = appCurrency, swapDirection = swapDirection, clickIntents = this@SwapAmountModel, isBalanceHidden = params.isBalanceHidingFlow.value, - ).convert(Unit) - } + ), + ) + loadQuotes() } else { // todo not available currency to swap } @@ -304,7 +318,7 @@ internal class SwapAmountModel @Inject constructor( } } - private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus) { + private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus?) { primaryMinimumAmountBoundary = EnterAmountBoundary( amount = getMinimumTransactionAmountSyncUseCase .invoke( @@ -314,21 +328,25 @@ internal class SwapAmountModel @Inject constructor( fiatRate = primaryStatus.value.fiatRate, fiatAmount = primaryStatus.value.fiatAmount, ) - secondaryMinimumAmountBoundary = EnterAmountBoundary( - amount = getMinimumTransactionAmountSyncUseCase - .invoke( - userWalletId = userWallet.walletId, - cryptoCurrencyStatus = secondaryStatus, - ).getOrNull().orZero(), - fiatRate = secondaryStatus.value.fiatRate, - fiatAmount = secondaryStatus.value.fiatAmount, - ) primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus) - secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus) + + if (secondaryStatus != null) { + secondaryMinimumAmountBoundary = EnterAmountBoundary( + amount = getMinimumTransactionAmountSyncUseCase + .invoke( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = secondaryStatus, + ).getOrNull().orZero(), + fiatRate = secondaryStatus.value.fiatRate, + fiatAmount = secondaryStatus.value.fiatAmount, + ) + secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus) + } } private fun loadQuotes() { val state = uiState.value as? SwapAmountUM.Content ?: return + if (state.secondaryCryptoCurrencyStatus == null) return val (fromCryptoCurrency, toCryptoCurrency) = when (state.swapDirection) { SwapDirection.Direct -> { @@ -376,11 +394,17 @@ internal class SwapAmountModel @Inject constructor( ).takeIf { error is ExpressError.AmountError } }, ifRight = { quote: SwapQuoteModel -> - convertToSwapProviderUM( - quote = quote, - provider = provider, - differencePercent = DifferencePercent.Empty, + SwapQuoteUMConverter( + primaryCurrency = fromCryptoCurrency, + secondaryCurrency = toCryptoCurrency, swapDirection = swapDirection, + allowanceContract = quote.allowanceContract, + isApprovalNeeded = checkAllowance(state, quote), + ).convert( + SwapQuoteUMConverter.Data( + quote = quote, + provider = provider, + ), ) }, ) @@ -397,61 +421,19 @@ internal class SwapAmountModel @Inject constructor( } } - private suspend fun convertToSwapProviderUM( - quote: SwapQuoteModel, - provider: ExpressProvider, - differencePercent: DifferencePercent, - swapDirection: SwapDirection, - ): SwapQuoteUM { - // todo swap allowance + private suspend fun checkAllowance(state: SwapAmountUM.Content, quote: SwapQuoteModel): Boolean { val allowanceContract = quote.allowanceContract - return if (allowanceContract != null) { - val allowance = getAllowanceUseCase( + val allowance = if (allowanceContract != null) { + getAllowanceUseCase( userWalletId = userWallet.walletId, - cryptoCurrency = primaryCryptoCurrencyStatus.currency, + cryptoCurrency = state.primaryCryptoCurrencyStatus.currency, spenderAddress = allowanceContract, - ).getOrNull().orZero() - val isApprovalNeeded = allowance < primaryCryptoCurrencyStatus.value.amount.orZero() - - if (isApprovalNeeded) { - SwapQuoteUM.Allowance( - provider = provider, - allowanceContract = allowanceContract, - ) - } else { - SwapQuoteUM.Content( - provider = provider, - quoteAmount = quote.toTokenAmount, - diffPercent = differencePercent, - quoteAmountValue = stringReference( - quote.toTokenAmount.format { - crypto( - when (swapDirection) { - SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency - SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency - }, - ) - }, - ), - ) - } + ).getOrNull() } else { - SwapQuoteUM.Content( - provider = provider, - quoteAmount = quote.toTokenAmount, - diffPercent = differencePercent, - quoteAmountValue = stringReference( - quote.toTokenAmount.format { - crypto( - when (swapDirection) { - SwapDirection.Direct -> secondaryCryptoCurrencyStatus.currency - SwapDirection.Reverse -> primaryCryptoCurrencyStatus.currency - }, - ) - }, - ), - ) + BigDecimal.ZERO } + + return allowance.orZero() < state.primaryCryptoCurrencyStatus.value.amount.orZero() } private fun saveResult() { @@ -475,20 +457,7 @@ internal class SwapAmountModel @Inject constructor( } else { R.drawable.ic_close_24 }, - backIconClick = { - // if (!route.isEditMode) { - // todo analytics - // analyticsEventHandler.send( - // CommonSendAnalyticEvents.CloseButtonClicked( - // categoryName = params.analyticsCategoryName, - // source = SendScreenSource.Address, - // isFromSummary = false, - // isValid = state.isPrimaryButtonEnabled, - // ), - // ) - // } - params.callback.onBackClick() - }, + backIconClick = params.callback::onBackClick, primaryButton = NavigationButton( textReference = if (route.isEditMode) { resourceReference(R.string.common_continue) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index bef7ed2108..b2b853bf9d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -42,8 +42,8 @@ internal object SwapAmountQuoteUtils { } fun SwapAmountUM.updateAmount( - onPrimaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM, - onSecondaryAmount: SwapAmountFieldUM.Content.() -> SwapAmountFieldUM, + onPrimaryAmount: SwapAmountFieldUM.Content.(CryptoCurrencyStatus) -> SwapAmountFieldUM, + onSecondaryAmount: SwapAmountFieldUM.Content.(CryptoCurrencyStatus) -> SwapAmountFieldUM, ): SwapAmountUM { if (this !is SwapAmountUM.Content) return this @@ -51,10 +51,11 @@ internal object SwapAmountQuoteUtils { selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct ) { val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this - copy(primaryAmount = amountFieldUM.onPrimaryAmount()) + copy(primaryAmount = amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus)) } else { + if (secondaryCryptoCurrencyStatus == null) return this val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this - copy(secondaryAmount = amountFieldUM.onSecondaryAmount()) + copy(secondaryAmount = amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus)) } } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt new file mode 100644 index 0000000000..8abbd44af1 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -0,0 +1,66 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapQuoteModel +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SwapQuoteUMConverter( + private val swapDirection: SwapDirection, + private val allowanceContract: String?, + private val isApprovalNeeded: Boolean, + private val primaryCurrency: CryptoCurrency, + private val secondaryCurrency: CryptoCurrency, +) : Converter { + override fun convert(value: Data): SwapQuoteUM { + val (quote, provider) = value + + return if (allowanceContract != null) { + if (isApprovalNeeded) { + SwapQuoteUM.Allowance( + provider = provider, + allowanceContract = allowanceContract, + ) + } else { + SwapQuoteUM.Content( + provider = provider, + quoteAmount = quote.toTokenAmount, + diffPercent = DifferencePercent.Empty, + quoteAmountValue = stringReference( + quote.toTokenAmount.toQuoteValue(), + ), + ) + } + } else { + SwapQuoteUM.Content( + provider = provider, + quoteAmount = quote.toTokenAmount, + diffPercent = DifferencePercent.Empty, + quoteAmountValue = stringReference( + quote.toTokenAmount.toQuoteValue(), + ), + ) + } + } + + private fun BigDecimal.toQuoteValue() = format { + crypto( + when (swapDirection) { + SwapDirection.Direct -> secondaryCurrency + SwapDirection.Reverse -> primaryCurrency + }, + ) + } + + data class Data( + val quote: SwapQuoteModel, + val provider: ExpressProvider, + ) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt index e1a149555d..21938524aa 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountChangeCurrencyTransformer.kt @@ -11,18 +11,18 @@ internal class SwapAmountChangeCurrencyTransformer( override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState return prevState.updateAmount( - onPrimaryAmount = { + onPrimaryAmount = { primaryStatus -> copy( amountField = AmountCurrencyTransformer( - cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + cryptoCurrencyStatus = primaryStatus, value = isFiatSelected, ).transform(prevState.primaryAmount.amountField), ) }, - onSecondaryAmount = { + onSecondaryAmount = { secondaryStatus -> copy( amountField = AmountCurrencyTransformer( - cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + cryptoCurrencyStatus = secondaryStatus, value = isFiatSelected, ).transform(prevState.secondaryAmount.amountField), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt new file mode 100644 index 0000000000..78ba1cd720 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -0,0 +1,57 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.domain.swap.models.SwapCurrencies +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +@Suppress("LongParameterList") +internal class SwapAmountPrimaryReadyStateTransformer( + private val userWallet: UserWallet, + private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, + private val clickIntents: AmountScreenClickIntents, + private val swapDirection: SwapDirection, + private val isBalanceHidden: Boolean, +) : Transformer { + + private val amountFieldConverter = SwapAmountFieldConverter( + swapDirection = swapDirection, + isBalanceHidden = isBalanceHidden, + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + ) + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + return SwapAmountUM.Content( + isPrimaryButtonEnabled = false, + primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, + primaryAmount = amountFieldConverter.convert( + selectedType = SwapAmountType.From, + cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + ), + secondaryCryptoCurrencyStatus = null, + secondaryAmount = SwapAmountFieldUM.Empty( + SwapAmountType.To, + ), + swapDirection = swapDirection, + swapCurrencies = SwapCurrencies.EMPTY, + selectedAmountType = prevState.selectedAmountType, + swapRateType = ExpressRateType.Float, + swapQuotes = persistentListOf(), + selectedQuote = SwapQuoteUM.Empty, + appCurrency = appCurrency, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt similarity index 80% rename from features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt rename to features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 7de7bab116..8b4c3989ba 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountReadyStateConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.swap.v2.impl.amount.model.converter +package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency @@ -9,21 +9,22 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM -import com.tangem.utils.converter.Converter +import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf @Suppress("LongParameterList") -internal class SwapAmountReadyStateConverter( +internal class SwapAmountSecondaryReadyStateTransformer( private val userWallet: UserWallet, - private val swapCurrencies: SwapCurrencies, private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrency: AppCurrency, + private val swapCurrencies: SwapCurrencies, private val clickIntents: AmountScreenClickIntents, private val swapDirection: SwapDirection, private val isBalanceHidden: Boolean, -) : Converter { +) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( swapDirection = swapDirection, @@ -33,26 +34,23 @@ internal class SwapAmountReadyStateConverter( clickIntents = clickIntents, ) - override fun convert(value: Unit): SwapAmountUM { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { return SwapAmountUM.Content( + isPrimaryButtonEnabled = false, + primaryAmount = prevState.primaryAmount, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, - primaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.From, - cryptoCurrencyStatus = primaryCryptoCurrencyStatus, - ), secondaryAmount = amountFieldConverter.convert( selectedType = SwapAmountType.To, cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, ), + secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, swapCurrencies = swapCurrencies, + selectedAmountType = prevState.selectedAmountType, swapDirection = swapDirection, + swapRateType = ExpressRateType.Float, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, - selectedAmountType = SwapAmountType.From, appCurrency = appCurrency, - swapRateType = ExpressRateType.Float, - isPrimaryButtonEnabled = false, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index b11d0238e1..7213bcb3c5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -16,8 +16,8 @@ import com.tangem.utils.transformer.Transformer internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, - private val secondaryMaximumAmountBoundary: EnterAmountBoundary, - private val secondaryMinimumAmountBoundary: EnterAmountBoundary, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -46,7 +46,9 @@ internal class SwapAmountSelectQuoteTransformer( } else { prevState.primaryAmount }, - secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { + secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From && + prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null + ) { val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data) ?.amountTextField?.cryptoAmount?.value.orZero() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index d890417658..05064354de 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -17,8 +17,8 @@ import java.math.BigDecimal internal class SwapAmountSetQuotesTransformer( private val quotes: List, - private val secondaryMaximumAmountBoundary: EnterAmountBoundary, - private val secondaryMinimumAmountBoundary: EnterAmountBoundary, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt new file mode 100644 index 0000000000..dde5dd9c4c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountUpdateBalanceTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.utils.transformer.Transformer + +internal class SwapAmountUpdateBalanceTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val primaryMaximumAmountBoundary: EnterAmountBoundary, + private val primaryMinimumAmountBoundary: EnterAmountBoundary, +) : Transformer { + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + val state = prevState as? SwapAmountUM.Content ?: return prevState + val primaryAmountUM = state.primaryAmount as? SwapAmountFieldUM.Content ?: return prevState + val amountField = primaryAmountUM.amountField as? AmountState.Data ?: return prevState + val amountValue = if (amountField.amountTextField.isFiatValue) { + amountField.amountTextField.fiatValue + } else { + amountField.amountTextField.value + } + return state.copy( + primaryCryptoCurrencyStatus = cryptoCurrencyStatus, + primaryAmount = primaryAmountUM.copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = amountValue, + ).transform(prevState.primaryAmount.amountField), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt index 1c0dbd8086..5a7e9eccb3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -4,39 +4,45 @@ import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTrans import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.transformer.Transformer internal class SwapAmountValueChangeTransformer( private val primaryMaximumAmountBoundary: EnterAmountBoundary, - private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val primaryMinimumAmountBoundary: EnterAmountBoundary, - private val secondaryMinimumAmountBoundary: EnterAmountBoundary, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val value: String, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState return prevState + .copy(selectedQuote = SwapQuoteUM.Loading) .updateAmount( - onPrimaryAmount = { + onPrimaryAmount = { primaryStatus -> copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + cryptoCurrencyStatus = primaryStatus, maxEnterAmount = primaryMaximumAmountBoundary, minimumTransactionAmount = primaryMinimumAmountBoundary, value = value, ).transform(prevState.primaryAmount.amountField), ) }, - onSecondaryAmount = { - copy( - amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = value, - ).transform(prevState.secondaryAmount.amountField), - ) + onSecondaryAmount = { secondaryStatus -> + if (secondaryMaximumAmountBoundary != null) { + copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = secondaryStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = value, + ).transform(prevState.secondaryAmount.amountField), + ) + } else { + this + } }, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt index 980216e2a3..59c38f1c94 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueMaxTransformer.kt @@ -9,9 +9,9 @@ import com.tangem.utils.transformer.Transformer internal class SwapAmountValueMaxTransformer( private val primaryMaximumAmountBoundary: EnterAmountBoundary, - private val secondaryMaximumAmountBoundary: EnterAmountBoundary, private val primaryMinimumAmountBoundary: EnterAmountBoundary, - private val secondaryMinimumAmountBoundary: EnterAmountBoundary, + private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, + private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState @@ -19,25 +19,28 @@ internal class SwapAmountValueMaxTransformer( return prevState .copy(selectedQuote = SwapQuoteUM.Loading) .updateAmount( - onPrimaryAmount = { + onPrimaryAmount = { primaryStatus -> copy( amountField = AmountFieldSetMaxAmountTransformer( - cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + cryptoCurrencyStatus = primaryStatus, maxAmount = primaryMaximumAmountBoundary, minAmount = primaryMinimumAmountBoundary, ).transform(prevState.primaryAmount.amountField), ) }, - onSecondaryAmount = { - copy( - amountField = AmountFieldSetMaxAmountTransformer( - cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - maxAmount = secondaryMaximumAmountBoundary, - minAmount = secondaryMinimumAmountBoundary, - ).transform(prevState.secondaryAmount.amountField), - ) + onSecondaryAmount = { secondaryStatus -> + if (secondaryMaximumAmountBoundary != null) { + copy( + amountField = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = secondaryStatus, + maxAmount = secondaryMaximumAmountBoundary, + minAmount = secondaryMinimumAmountBoundary, + ).transform(prevState.secondaryAmount.amountField), + ) + } else { + this + } }, - ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index 212a0ca592..1ac34a19e2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -354,12 +354,12 @@ private fun AmountMaxButton(onMaxAmountClick: () -> Unit) { .padding(end = 16.dp) .clip(RoundedCornerShape(16.dp)) .background(TangemTheme.colors.background.secondary) - .padding(horizontal = 12.dp, vertical = 4.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onMaxAmountClick, - ), + ) + .padding(horizontal = 12.dp, vertical = 4.dp), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt new file mode 100644 index 0000000000..01da434698 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapUtils.kt @@ -0,0 +1,6 @@ +package com.tangem.features.swap.v2.impl.common + +internal object SwapUtils { + const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% + const val INCREASE_GAS_LIMIT_FOR_CEX = 105 // 5% +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt new file mode 100644 index 0000000000..7e3af10fa2 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -0,0 +1,24 @@ +package com.tangem.features.swap.v2.impl.common.entity + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class ConfirmUM { + + abstract val isPrimaryButtonEnabled: Boolean + + data class Content( + override val isPrimaryButtonEnabled: Boolean = false, + val isTransactionInProcess: Boolean, + val showTapHelp: Boolean, + val sendingFooter: TextReference, + val notifications: ImmutableList, + ) : ConfirmUM() + + data object Empty : ConfirmUM() { + override val isPrimaryButtonEnabled: Boolean = false + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/SendWithSwapRoute.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/SendWithSwapRoute.kt new file mode 100644 index 0000000000..3f0b9b70be --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/SendWithSwapRoute.kt @@ -0,0 +1,31 @@ +package com.tangem.features.swap.v2.impl.sendviaswap + +import com.tangem.core.decompose.navigation.Route +import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute +import com.tangem.features.swap.v2.impl.amount.SwapAmountRoute +import kotlinx.serialization.Serializable + +internal sealed class SendWithSwapRoute : Route { + + abstract val isEditMode: Boolean + + @Serializable + data class Amount( + override val isEditMode: Boolean, + ) : SendWithSwapRoute(), SwapAmountRoute + + @Serializable + data class Destination( + override val isEditMode: Boolean, + ) : SendWithSwapRoute(), DestinationRoute + + @Serializable + data object Confirm : SendWithSwapRoute() { + override val isEditMode: Boolean = false + } + + @Serializable + data object Success : SendWithSwapRoute() { + override val isEditMode: Boolean = false + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt new file mode 100644 index 0000000000..c32952b36c --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -0,0 +1,162 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm + +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.context.child +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.entity.PredefinedValues +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams +import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent +import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui.SendWithSwapConfirmContent +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.* +import java.math.BigDecimal + +internal class SendWithSwapConfirmComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: Params, + sendDestinationBlockComponent: SendDestinationBlockComponent.Factory, + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, + sendNotificationsComponentFactory: SendNotificationsComponent.Factory, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendWithSwapConfirmModel = getOrCreateModel(params = params) + + private val blockClickEnableFlow = MutableStateFlow(false) + + private val amountBlockComponent = SwapAmountBlockComponent( + appComponentContext = child("sendWithSwapConfirmAmountBlock"), + params = SwapAmountComponentParams.AmountBlockParams( + amountUM = model.uiState.value.amountUM, + analyticsCategoryName = params.analyticsCategoryName, + userWallet = params.userWallet, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + primaryCryptoCurrencyStatusFlow = params.primaryCryptoCurrencyStatusFlow, + secondaryCryptoCurrency = model.secondaryCurrency, + isBalanceHidingFlow = params.isBalanceHidingFlow, + swapDirection = params.swapDirection, + ), + onResult = model::onAmountResult, + onClick = model::showEditAmount, + ) + + private val sendDestinationBlockComponent = sendDestinationBlockComponent.create( + context = child("sendWithSwapConfirmDestinationBlock"), + params = SendDestinationComponentParams.DestinationBlockParams( + state = model.uiState.value.destinationUM, + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + cryptoCurrency = model.secondaryCurrency, + predefinedValues = PredefinedValues.Empty, + ), + onResult = model::onDestinationResult, + onClick = model::showEditDestination, + ) + + private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = child("sendWithSwapConfirmFeeBlock"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = model.uiState.value.feeSelectorUM, + onLoadFee = model::loadFee, + feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus, + cryptoCurrencyStatus = model.primaryCurrencyStatus, + suggestedFeeState = FeeSelectorParams.SuggestedFeeState.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + ), + onResult = model::onFeeResult, + ) + + private val sendNotificationsComponent = sendNotificationsComponentFactory.create( + context = appComponentContext.childByContext(child("sendWithSwapConfirmNotifications")), + params = SendNotificationsComponent.Params( + analyticsCategoryName = params.analyticsCategoryName, + userWalletId = params.userWallet.walletId, + cryptoCurrencyStatus = model.primaryCurrencyStatus, + feeCryptoCurrencyStatus = model.primaryFeePaidCurrencyStatus, + appCurrency = params.appCurrency, + notificationData = SendNotificationsComponent.Params.NotificationData( + // todo fill with data + destinationAddress = "", + memo = "", + amountValue = BigDecimal.ZERO, + reduceAmountBy = BigDecimal.ZERO, + isIgnoreReduce = false, + fee = null, + feeError = null, + ), + ), + ) + + init { + model.uiState.onEach { state -> + val confirmUM = state.confirmUM as? ConfirmUM.Content + blockClickEnableFlow.value = confirmUM?.isTransactionInProcess == false + }.launchIn(componentScope) + } + + fun updateState(sendWithSwapUM: SendWithSwapUM) { + amountBlockComponent.updateState(sendWithSwapUM.amountUM) + sendDestinationBlockComponent.updateState(sendWithSwapUM.destinationUM) + feeSelectorBlockComponent.updateState(sendWithSwapUM.feeSelectorUM) + model.updateState(sendWithSwapUM) + } + + @Composable + override fun Content(modifier: Modifier) { + val sendWithSwapUM by model.uiState.collectAsStateWithLifecycle() + val sendNotificationsUM by sendNotificationsComponent.state.collectAsStateWithLifecycle() + + SendWithSwapConfirmContent( + sendWithSwapUM = sendWithSwapUM, + amountBlockComponent = amountBlockComponent, + sendDestinationBlockComponent = sendDestinationBlockComponent, + feeSelectorBlockComponent = feeSelectorBlockComponent, + sendNotificationsComponent = sendNotificationsComponent, + sendNotificationsUM = sendNotificationsUM, + modifier = modifier, + ) + } + + data class Params( + val sendWithSwapUM: SendWithSwapUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val appCurrency: AppCurrency, + val currentRoute: Flow, + val swapDirection: SwapDirection, + val isBalanceHidingFlow: StateFlow, + val primaryCryptoCurrencyStatusFlow: StateFlow, + val primaryFeePaidCurrencyStatusFlow: StateFlow, + val callback: ModelCallback, + ) + + @AssistedFactory + interface Factory { + fun create(appComponentContext: AppComponentContext, params: Params): SendWithSwapConfirmComponent + } + + interface ModelCallback { + fun onResult(sendWithSwapUM: SendWithSwapUM) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/di/SendWithSwapConfirmModule.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/di/SendWithSwapConfirmModule.kt new file mode 100644 index 0000000000..30140080dc --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/di/SendWithSwapConfirmModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.SendWithSwapConfirmModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface SendWithSwapConfirmModule { + + @Binds + @IntoMap + @ClassKey(SendWithSwapConfirmModel::class) + fun bindsSendWithSwapConfirmModel(impl: SendWithSwapConfirmModel): Model +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt new file mode 100644 index 0000000000..d89edaf05f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -0,0 +1,239 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.left +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationUM +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.core.ui.extensions.resourceReference +import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase +import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection +import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.EstimateFeeUseCase +import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.SwapUtils.INCREASE_GAS_LIMIT_FOR_CEX +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmInitialStateTransformer +import com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers.SendWithSwapConfirmationNotificationsTransformer +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import com.tangem.lib.crypto.BlockchainFeeUtils.patchTransactionFeeForSwap +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero +import jakarta.inject.Inject +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import com.tangem.utils.transformer.update as transformerUpdate + +@Suppress("LongParameterList") +@ModelScoped +internal class SendWithSwapConfirmModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, + private val estimateFeeUseCase: EstimateFeeUseCase, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, + private val sendNotificationsUpdateTrigger: SendNotificationsUpdateTrigger, + paramsContainer: ParamsContainer, +) : Model(), FeeSelectorModelCallback { + + private val params: SendWithSwapConfirmComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow(params.sendWithSwapUM) + + val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value + val primaryFeePaidCurrencyStatus: CryptoCurrencyStatus = params.primaryFeePaidCurrencyStatusFlow.value + + private val amountUM = uiState.value.amountUM as? SwapAmountUM.Content + + val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { + "Crypto currency must not be null" + } + + private var isAmountSubtractAvailable = false + + init { + initAmountSubtractAvailability() + configConfirmNavigation() + initialState() + } + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + uiState.update { it.copy(feeSelectorUM = feeSelectorUM) } + updateConfirmNotifications() + } + + fun onAmountResult(amountUM: SwapAmountUM) { + uiState.update { it.copy(amountUM = amountUM) } + updateConfirmNotifications() + } + + fun onDestinationResult(destinationUM: DestinationUM) { + uiState.update { it.copy(destinationUM = destinationUM) } + updateConfirmNotifications() + } + + fun updateState(sendWithSwapUM: SendWithSwapUM) { + uiState.value = sendWithSwapUM + } + + fun showEditAmount() { + router.push(SendWithSwapRoute.Amount(isEditMode = true)) + } + + fun showEditDestination() { + router.push(SendWithSwapRoute.Destination(isEditMode = true)) + } + + suspend fun loadFee(): Either { + val defaultError = GetFeeError.UnknownError.left() + val quote = amountUM?.selectedQuote as? SwapQuoteUM.Content ?: return defaultError + val amountUM = uiState.value.amountUM as? SwapAmountUM.Content ?: return defaultError + + val amountField = amountUM.swapDirection.withSwapDirection( + onDirect = { amountUM.primaryAmount.amountField }, + onReverse = { amountUM.secondaryAmount.amountField }, + ) as? AmountState.Data ?: return defaultError + val amountValue = amountField.amountTextField.cryptoAmount.value ?: return defaultError + + return when (val providerType = quote.provider.type) { + ExpressProviderType.CEX -> { + estimateFeeUseCase( + amount = amountValue, + userWallet = params.userWallet, + cryptoCurrency = primaryCurrencyStatus.currency, + ).map { + it.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_CEX) + } + } + ExpressProviderType.DEX, + ExpressProviderType.DEX_BRIDGE, + -> { + // todo send with swap + GetFeeError.UnknownError.left() + } + ExpressProviderType.ONRAMP, + -> GetFeeError.DataError( + cause = IllegalStateException("Provider $providerType is not supported in Send With Swap"), + ).left() + } + } + + private fun onSendClick() { + // todo swap send tx + } + + private fun initAmountSubtractAvailability() { + modelScope.launch { + isAmountSubtractAvailable = + isAmountSubtractAvailableUseCase( + params.userWallet.walletId, + primaryCurrencyStatus.currency, + ).getOrElse { false } + } + } + + private fun initialState() { + val confirmUM = uiState.value.confirmUM + + modelScope.launch { + val isShowTapHelp = isSendTapHelpEnabledUseCase().getOrElse { false } + if (confirmUM is ConfirmUM.Empty) { + uiState.update { + it.copy( + confirmUM = SendWithSwapConfirmInitialStateTransformer( + isShowTapHelp = isShowTapHelp, + ).transform(uiState.value.confirmUM), + ) + } + updateConfirmNotifications() + } + } + } + + private fun updateConfirmNotifications() { + val amountUM = uiState.value.amountUM as? SwapAmountUM.Content ?: return + val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: return + val feeSelectorUMContent = uiState.value.feeSelectorUM as? FeeSelectorUM.Content + val feeSelectorUMError = uiState.value.feeSelectorUM as? FeeSelectorUM.Error + + val amountField = amountUM.swapDirection.withSwapDirection( + onDirect = { amountUM.primaryAmount.amountField }, + onReverse = { amountUM.secondaryAmount.amountField }, + ) as? AmountState.Data ?: return + val enteredDestination = destinationUM.addressTextField.actualAddress + + modelScope.launch { + sendNotificationsUpdateTrigger.triggerUpdate( + data = NotificationData( + destinationAddress = enteredDestination, + memo = null, + amountValue = amountField.amountTextField.cryptoAmount.value.orZero(), + reduceAmountBy = amountField.reduceAmountBy.orZero(), + isIgnoreReduce = amountField.isIgnoreReduce, + fee = feeSelectorUMContent?.selectedFeeItem?.fee, + feeError = feeSelectorUMError?.error, + ), + ) + uiState.transformerUpdate( + SendWithSwapConfirmationNotificationsTransformer(), + ) + } + } + + private fun configConfirmNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).filter { + it.second is SendWithSwapRoute.Confirm + }.onEach { (state, _) -> + val confirmUM = state.confirmUM + params.callback.onResult( + state.copy( + navigationUM = NavigationUM.Content( + title = resourceReference(id = R.string.send_with_swap_confirm_title), + subtitle = null, + backIconRes = R.drawable.ic_back_24, + backIconClick = router::pop, + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_send), + iconRes = R.drawable.ic_tangem_24, + isEnabled = confirmUM.isPrimaryButtonEnabled, + onClick = { + when (confirmUM) { + is ConfirmUM.Content -> if (confirmUM.isTransactionInProcess) { + return@NavigationButton + } else { + onSendClick() + } + else -> return@NavigationButton + } + }, + ), + ), + ), + ) + }.launchIn(modelScope) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt new file mode 100644 index 0000000000..4be5c0b56a --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmInitialStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class SendWithSwapConfirmInitialStateTransformer( + private val isShowTapHelp: Boolean, +) : Transformer { + override fun transform(prevState: ConfirmUM): ConfirmUM { + return ConfirmUM.Content( + isPrimaryButtonEnabled = false, + isTransactionInProcess = false, + showTapHelp = isShowTapHelp, + sendingFooter = TextReference.EMPTY, + notifications = persistentListOf(), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt new file mode 100644 index 0000000000..014e57650d --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmationNotificationsTransformer.kt @@ -0,0 +1,106 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.model.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooHigh +import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooLow +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class SendWithSwapConfirmationNotificationsTransformer : Transformer { + override fun transform(prevState: SendWithSwapUM): SendWithSwapUM { + val confirmUM = prevState.confirmUM as? ConfirmUM.Content ?: return prevState + val feeSelectorUM = prevState.feeSelectorUM as? FeeSelectorUM.Content ?: return prevState + + return prevState.copy( + confirmUM = confirmUM.copy( + sendingFooter = getSendingFooterText(feeSelectorUM, prevState.amountUM), + notifications = buildList { + addTooHighNotification(feeSelectorUM = feeSelectorUM) + addTooLowNotification(feeSelectorUM = feeSelectorUM) + }.toPersistentList(), + ), + ) + } + + private fun MutableList.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) { + if (checkIfCustomFeeTooLow(feeSelectorUM = feeSelectorUM)) { + add(NotificationUM.Warning.FeeTooLow) + } + } + + private fun MutableList.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) { + val (isFeeTooHigh, diff) = checkIfCustomFeeTooHigh(feeSelectorUM = feeSelectorUM) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + } + + private fun getSendingFooterText(feeSelectorUM: FeeSelectorUM.Content, swapAmountUM: SwapAmountUM): TextReference { + val amountUM = swapAmountUM.swapDirection.withSwapDirection( + onDirect = { swapAmountUM.primaryAmount.amountField }, + onReverse = { swapAmountUM.secondaryAmount.amountField }, + ) as? AmountState.Data + val feeItem = feeSelectorUM.selectedFeeItem + val feeFiatRateUM = feeSelectorUM.feeFiatRateUM + + val appCurrency = feeFiatRateUM?.appCurrency + + if (amountUM == null || appCurrency == null) return TextReference.EMPTY + + val fiatAmountValue = amountUM.amountTextField.fiatAmount.value + val fiatFeeValue = feeItem.fee.amount.value?.multiply(feeFiatRateUM.rate) + + val fiatSendingValue = if (feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat) { + fiatFeeValue?.let { fiatAmountValue?.plus(it) } + } else { + fiatAmountValue + } + + val fiatSending = fiatSendingValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + val fiatFee = TextReference.EMPTY + // todo send with swap footer + // formatFooterFiatFee( + // amount = feeItem.fee.amount.copy(value = fiatFeeValue), + // isFeeConvertibleToFiat = feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat, + // isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate, + // appCurrency = appCurrency, + // ) + + return if (feeSelectorUM.feeExtraInfo.isTronToken && feeItem.fee is Fee.Tron) { + // todo send with swap footer + // getTronTokenFeeSendingText( + // fee = feeItem.fee, + // fiatFee = fiatFee, + // fiatSending = stringReference(fiatSending), + // ) + TextReference.EMPTY + } else { + resourceReference( + id = if (feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat) { + R.string.send_summary_transaction_description + } else { + R.string.send_summary_transaction_description_no_fiat_fee + }, + formatArgs = wrappedList(fiatSending, fiatFee), + ) + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/ui/SendWithSwapConfirmContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/ui/SendWithSwapConfirmContent.kt new file mode 100644 index 0000000000..4386a93cb6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/ui/SendWithSwapConfirmContent.kt @@ -0,0 +1,72 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.confirm.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +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.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.SendNotificationsComponent +import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationBlockComponent +import com.tangem.features.swap.v2.impl.amount.SwapAmountBlockComponent +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongParameterList") +@Composable +internal fun SendWithSwapConfirmContent( + sendWithSwapUM: SendWithSwapUM, + amountBlockComponent: SwapAmountBlockComponent, + sendDestinationBlockComponent: SendDestinationBlockComponent, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + sendNotificationsComponent: SendNotificationsComponent, + sendNotificationsUM: ImmutableList, + modifier: Modifier = Modifier, +) { + val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Content + + Column(modifier = modifier) { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 12.dp), + ) { + item(key = "SendWithSwapAmountBlock") { + amountBlockComponent.Content(Modifier) + } + item(key = "SendWithSwapDestinationBlock") { + sendDestinationBlockComponent.Content(Modifier) + } + item(key = "SendWithSwapFeeBLock") { + Box( + modifier = Modifier.clip(RoundedCornerShape(16.dp)), + ) { + feeSelectorBlockComponent.Content(Modifier) + } + } + if (confirmUM != null) { + // tapHelp(isDisplay = confirmUM.showTapHelp) // todo + with(sendNotificationsComponent) { + content( + state = sendNotificationsUM, + isClickDisabled = confirmUM.isTransactionInProcess, + ) + } + // notifications( + // notifications = confirmUM.notifications, + // isClickDisabled = confirmUM.isTransactionInProcess, + // ) + } + } + SpacerHMax() + // todo + // SendingText(footerText = confirmUM?.sendingFooter ?: TextReference.EMPTY) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/entity/SendWithSwapUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/entity/SendWithSwapUM.kt new file mode 100644 index 0000000000..1a576345a6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/entity/SendWithSwapUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.entity + +import com.tangem.common.ui.navigationButtons.NavigationUM +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM + +internal data class SendWithSwapUM( + val amountUM: SwapAmountUM, + val destinationUM: DestinationUM, + val feeSelectorUM: FeeSelectorUM, + val confirmUM: ConfirmUM, + val navigationUM: NavigationUM, +) \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 28db912457..c8e001e999 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -30,7 +30,6 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError @@ -404,12 +403,7 @@ internal class DefaultSwapRepository( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - scanResponse = requireNotNull( - userWalletsListManager - .selectedUserWalletSync - ?.requireColdWallet() // TODO [REDACTED_TASK_KEY] - ?.scanResponse, - ), + userWallet = requireNotNull(userWalletsListManager.selectedUserWalletSync), ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 5633a2c403..ca12e23f1c 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -6,20 +6,18 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync -import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.swap.converters.SavedSwapTransactionListConverter import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.withContext internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, @@ -82,35 +80,35 @@ internal class DefaultSwapTransactionRepository( } } - override suspend fun getTransactions( + override fun getTransactions( userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, ): Flow?> { - return withContext(dispatchers.io) { - val txStatuses = appPreferencesStore.getObjectMapSync( - key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, - ) - appPreferencesStore.getObjectList( + return combine( + flow = appPreferencesStore.getObjectList( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, - ).map { savedTransactions -> - val currencyTxs = savedTransactions - ?.filter { - it.userWalletId == userWallet.walletId.stringValue && - ( - it.toCryptoCurrencyId == cryptoCurrencyId.value || - it.fromCryptoCurrencyId == cryptoCurrencyId.value - ) - } + ), + flow2 = appPreferencesStore.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ), + ) { savedTransactions, txStatuses -> + val currencyTxs = savedTransactions?.filter { + it.userWalletId == userWallet.walletId.stringValue && + ( + it.toCryptoCurrencyId == cryptoCurrencyId.value || + it.fromCryptoCurrencyId == cryptoCurrencyId.value + ) + } - currencyTxs?.mapNotNull { - converter.convertBack( - value = it, - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] - txStatuses = txStatuses, - ) - } - }.flowOn(dispatchers.io) + currencyTxs?.mapNotNull { + converter.convertBack( + value = it, + userWallet = userWallet, + txStatuses = txStatuses, + ) + } } + .flowOn(dispatchers.default) } override suspend fun removeTransaction( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index e0b2c21e91..5240006f5d 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -3,7 +3,7 @@ package com.tangem.feature.swap.converters import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel @@ -32,7 +32,7 @@ internal class SavedSwapTransactionListConverter( fun convertBack( value: SavedSwapTransactionListModelInner, - scanResponse: ScanResponse, + userWallet: UserWallet, txStatuses: Map, ): SavedSwapTransactionListModel? { val fromToken = value.fromTokensResponse @@ -42,11 +42,11 @@ internal class SavedSwapTransactionListConverter( } else { val fromCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency( responseToken = fromToken, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null val toCryptoCurrency = responseCryptoCurrenciesFactory.createCurrency( responseToken = toToken, - scanResponse = scanResponse, + userWallet = userWallet, ) ?: return null return SavedSwapTransactionListModel( @@ -55,7 +55,7 @@ internal class SavedSwapTransactionListConverter( val refundCurrency = status?.refundTokensResponse?.let { id -> responseCryptoCurrenciesFactory.createCurrency( responseToken = id, - scanResponse = scanResponse, + userWallet = userWallet, ) } val statusWithRefundCurrency = status?.copy(refundCurrency = refundCurrency) 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 c99f20d86a..61a4be30c1 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 @@ -324,14 +324,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( ifRight = { quotes -> quotes.allowanceContract?.let { isAllowedToSpend(networkId, fromToken.currency, amount, it) - } ?: true + } != false }, ifLeft = { false }, ) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - fetchCurrencyStatusUseCase(userWalletId, fromToken.currency.id, true) + fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = fromToken.currency.id) } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadDexSwapData( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index 6948208c03..c52ee66212 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -17,7 +17,7 @@ interface SwapTransactionRepository { transaction: SavedSwapTransactionModel, ) - suspend fun getTransactions( + fun getTransactions( userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, ): Flow?> diff --git a/features/tester/api/build.gradle.kts b/features/tester/api/build.gradle.kts index 0456459cad..8022f8d874 100644 --- a/features/tester/api/build.gradle.kts +++ b/features/tester/api/build.gradle.kts @@ -6,4 +6,8 @@ plugins { android { namespace = "com.tangem.features.tester.api" +} + +dependencies { + api(deps.lifecycle.common.java8) } \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt new file mode 100644 index 0000000000..a6338eaa73 --- /dev/null +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterMenuLauncher.kt @@ -0,0 +1,14 @@ +package com.tangem.features.tester.api + +import androidx.lifecycle.DefaultLifecycleObserver + +/** + * Interface for launching the tester menu + * +[REDACTED_AUTHOR] + */ +interface TesterMenuLauncher { + + /** Observer for detecting shake events and launching the tester menu */ + val launchOnShakeObserver: DefaultLifecycleObserver +} \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt deleted file mode 100644 index 7e647c8323..0000000000 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.tester.api - -import android.content.Intent - -/** - * Outer tester feature router - * -[REDACTED_AUTHOR] - */ -interface TesterRouter { - - /** Open tester menu */ - fun getEntryIntent(): Intent -} \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index e376aae27f..bd1dd6bf0f 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.navigation) - implementation(projects.core.deepLinks) implementation(projects.core.pagination) /** Feature Apis */ diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt new file mode 100644 index 0000000000..859469177f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterMenuLauncherModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.tester.di + +import android.content.Context +import com.tangem.feature.tester.presentation.navigation.DefaultTesterMenuLauncher +import com.tangem.features.tester.api.TesterMenuLauncher +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object TesterMenuLauncherModule { + + @Provides + fun provideTesterMenuLauncher(@ApplicationContext context: Context): TesterMenuLauncher { + return DefaultTesterMenuLauncher(context = context) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt index 433341f7f7..ec8dd8d1cd 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/TesterRouterModule.kt @@ -1,7 +1,7 @@ package com.tangem.feature.tester.di import com.tangem.feature.tester.presentation.navigation.DefaultTesterRouter -import com.tangem.features.tester.api.TesterRouter +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,5 +14,5 @@ internal interface TesterRouterModule { @Binds @ActivityScoped - fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): TesterRouter + fun bindTesterRouter(defaultTesterRouter: DefaultTesterRouter): InnerTesterRouter } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 60ea5abd62..3c28e87b46 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -32,7 +32,6 @@ import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersSc import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel -import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import kotlinx.collections.immutable.persistentSetOf import javax.inject.Inject @@ -44,9 +43,8 @@ internal class TesterActivity : ComposeActivity() { @Inject override lateinit var uiDependencies: UiDependencies - /** Router for inner feature navigation */ @Inject - lateinit var testerRouter: TesterRouter + lateinit var innerTesterRouter: InnerTesterRouter @Inject lateinit var appFinisher: AppFinisher @@ -54,11 +52,6 @@ internal class TesterActivity : ComposeActivity() { @Inject lateinit var appRouter: AppRouter - private val innerTesterRouter: InnerTesterRouter - get() = requireNotNull(testerRouter as? InnerTesterRouter) { - "TesterRouter must be InnerTesterRouter for tester feature" - } - @Composable override fun ScreenContent(modifier: Modifier) { val systemBarsColor = TangemTheme.colors.background.secondary diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt index 06ad56fc8e..21a0d5aad5 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/ui/EnvironmentTogglesScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.SegmentedButton @@ -29,6 +30,8 @@ import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM import kotlinx.collections.immutable.persistentSetOf +private const val SCROLLABLE_BUTTONS_RESTRICTION = 4 + /** * Screen with environment toggles list * @@ -53,15 +56,13 @@ internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) { items = uiModel.apiInfoList.toTypedArray(), key = { _, info -> info.name }, ) { index, info -> - EnvironmentButtons( + val isLastItem = index == uiModel.apiInfoList.toTypedArray().lastIndex + + EnvironmentConfigBlock( uiModel = info, onSelect = { isChange -> uiModel.onEnvironmentSelect(info.name, isChange) }, modifier = Modifier.padding( - bottom = if (index == uiModel.apiInfoList.toTypedArray().lastIndex) { - TangemTheme.dimens.spacing0 - } else { - TangemTheme.dimens.spacing10 - }, + bottom = if (isLastItem) 0.dp else 10.dp, ), ) } @@ -69,18 +70,20 @@ internal fun EnvironmentTogglesScreen(uiModel: EnvironmentTogglesScreenUM) { } @Composable -private fun EnvironmentButtons( +private fun EnvironmentConfigBlock( uiModel: EnvironmentTogglesScreenUM.ApiInfoUM, onSelect: (String) -> Unit, modifier: Modifier = Modifier, ) { Column( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(space = 6.dp), ) { - Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { Text( text = uiModel.name, color = TangemTheme.colors.text.primary1, @@ -99,33 +102,50 @@ private fun EnvironmentButtons( } } - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - uiModel.environments.onEachIndexed { index, environment -> - key(environment) { - SegmentedButton( - selected = environment == uiModel.select, - onClick = { onSelect(environment) }, - shape = when (index) { - 0 -> RoundedCornerShape( - topStart = TangemTheme.dimens.radius12, - bottomStart = TangemTheme.dimens.radius12, - ) - uiModel.environments.toTypedArray().lastIndex -> RoundedCornerShape( - topEnd = TangemTheme.dimens.radius12, - bottomEnd = TangemTheme.dimens.radius12, - ) - else -> RectangleShape - }, - colors = SegmentedButtonDefaults.colors( - activeContainerColor = TangemTheme.colors.control.checked, - activeContentColor = TangemTheme.colors.text.primary2, - inactiveContainerColor = TangemTheme.colors.control.unchecked, - inactiveContentColor = TangemTheme.colors.text.primary1, - ), - border = BorderStroke(0.dp, TangemTheme.colors.background.tertiary), - ) { - Text(text = environment, style = TangemTheme.typography.subtitle2) - } + EnvironmentButtonsContainer(uiModel.environments.size) { + EnvironmentButtons(uiModel = uiModel, onSelect = onSelect) + } + } +} + +@Composable +private fun EnvironmentButtonsContainer(environmentsCount: Int, block: @Composable () -> Unit) { + if (environmentsCount > SCROLLABLE_BUTTONS_RESTRICTION) { + LazyRow(contentPadding = PaddingValues(horizontal = 16.dp)) { + item { block() } + } + } else { + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + block() + } + } +} + +@Composable +private fun EnvironmentButtons(uiModel: EnvironmentTogglesScreenUM.ApiInfoUM, onSelect: (String) -> Unit) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + uiModel.environments.onEachIndexed { index, environment -> + key(environment) { + SegmentedButton( + selected = environment == uiModel.select, + onClick = { onSelect(environment) }, + shape = when (index) { + 0 -> RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp) + uiModel.environments.toTypedArray().lastIndex -> RoundedCornerShape( + topEnd = 12.dp, + bottomEnd = 12.dp, + ) + else -> RectangleShape + }, + colors = SegmentedButtonDefaults.colors( + activeContainerColor = TangemTheme.colors.control.checked, + activeContentColor = TangemTheme.colors.text.primary2, + inactiveContainerColor = TangemTheme.colors.control.unchecked, + inactiveContentColor = TangemTheme.colors.text.primary1, + ), + border = BorderStroke(0.dp, TangemTheme.colors.background.tertiary), + ) { + Text(text = environment, style = TangemTheme.typography.subtitle2) } } } @@ -137,7 +157,14 @@ private fun EnvironmentButtons( @Composable private fun PreviewFeatureTogglesScreen() { TangemThemePreview { - var select by remember { mutableStateOf(ApiEnvironment.DEV.name) } + var select by remember { + mutableStateOf( + mapOf( + "Express" to ApiEnvironment.DEV.name, + "TangemTech" to ApiEnvironment.DEV.name, + ), + ) + } EnvironmentTogglesScreen( uiModel = EnvironmentTogglesScreenUM( @@ -145,17 +172,19 @@ private fun PreviewFeatureTogglesScreen() { apiInfoList = persistentSetOf( EnvironmentTogglesScreenUM.ApiInfoUM( name = ApiConfig.ID.Express.name, - select = select, + select = select[ApiConfig.ID.Express.name] ?: ApiEnvironment.DEV.name, url = "https://api.express.tangem.com", environments = persistentSetOf( ApiEnvironment.DEV.name, + ApiEnvironment.DEV_2.name, ApiEnvironment.STAGE.name, + ApiEnvironment.MOCK.name, ApiEnvironment.PROD.name, ), ), EnvironmentTogglesScreenUM.ApiInfoUM( name = ApiConfig.ID.TangemTech.name, - select = select, + select = select[ApiConfig.ID.TangemTech.name] ?: ApiEnvironment.DEV.name, url = "https://api.express.tangem.com", environments = persistentSetOf( ApiEnvironment.DEV.name, @@ -163,7 +192,11 @@ private fun PreviewFeatureTogglesScreen() { ), ), ), - onEnvironmentSelect = { _: String, s1: String -> select = s1 }, + onEnvironmentSelect = { id, env -> + select = select.toMutableMap().apply { + this[id] = env + } + }, onBackClick = {}, ), ) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt index d20eb06ced..cdf172d0c4 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/utils/ApiEnvironmentComparator.kt @@ -13,9 +13,10 @@ internal object ApiEnvironmentComparator : Comparator { private val apiEnvironmentPriorityMap = ApiEnvironment.entries.associateWith { when (it) { ApiEnvironment.DEV -> 0 - ApiEnvironment.STAGE -> 1 - ApiEnvironment.MOCK -> 2 - ApiEnvironment.PROD -> 3 + ApiEnvironment.DEV_2 -> 1 + ApiEnvironment.STAGE -> 2 + ApiEnvironment.MOCK -> 3 + ApiEnvironment.PROD -> 4 } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt index 336f45d72d..123452f6fa 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/environments/viewmodels/EnvironmentsTogglesViewModel.kt @@ -6,7 +6,6 @@ import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.config.managers.MutableApiConfigsManager -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.tester.impl.BuildConfig import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.environments.state.EnvironmentTogglesScreenUM @@ -30,7 +29,6 @@ import javax.inject.Inject @HiltViewModel internal class EnvironmentsTogglesViewModel @Inject constructor( apiConfigsManager: ApiConfigsManager, - private val cardSdkConfigRepository: CardSdkConfigRepository, ) : ViewModel() { /** Current ui state */ @@ -98,24 +96,7 @@ internal class EnvironmentsTogglesViewModel @Inject constructor( viewModelScope.launch { val environment = ApiEnvironment.valueOf(name) - if (id == ApiConfig.ID.TangemTech.name) { - handleTangemTechConfig(environment = environment) - } - mutableApiConfigsManager.changeEnvironment(id = id, environment = environment) } } - - /** Special logic for [ApiConfig.ID.TangemTech] */ - private fun handleTangemTechConfig(environment: ApiEnvironment) { - cardSdkConfigRepository.setTangemApiProdEnvFlag( - flag = when (environment) { - ApiEnvironment.PROD -> true - ApiEnvironment.DEV, - ApiEnvironment.STAGE, - ApiEnvironment.MOCK, - -> false - }, - ) - } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt new file mode 100644 index 0000000000..fbd8a860ff --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterMenuLauncher.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.tester.presentation.navigation + +import android.content.Context +import android.content.Intent +import android.hardware.Sensor +import android.hardware.SensorManager +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.tangem.feature.tester.presentation.TesterActivity +import com.tangem.features.tester.api.TesterMenuLauncher + +/** + * Default implementation of [TesterMenuLauncher] that listens for shake events using the device's accelerometer. + * When a shake is detected, it opens the tester menu. + * + * @param context the application context used to access system services + * +[REDACTED_AUTHOR] + */ +internal class DefaultTesterMenuLauncher(private val context: Context) : TesterMenuLauncher { + + override val launchOnShakeObserver: DefaultLifecycleObserver by lazy(LazyThreadSafetyMode.NONE) { + createObserver(context) + } + + private fun createObserver(context: Context): DefaultLifecycleObserver { + return object : DefaultLifecycleObserver { + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + private val shakeEventListener = ShakeEventListener(action = ::openTesterMenu) + + override fun onResume(owner: LifecycleOwner) { + accelerometer?.let { + sensorManager.registerListener( + /* listener = */ shakeEventListener, + /* sensor = */ it, + /* samplingPeriodUs = */ SensorManager.SENSOR_DELAY_NORMAL, + ) + } + } + + override fun onPause(owner: LifecycleOwner) { + sensorManager.unregisterListener(shakeEventListener) + } + } + } + + private fun openTesterMenu() { + val intent = Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + context.startActivity(intent) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt index 0d3c250e91..62bbfefebb 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt @@ -1,31 +1,19 @@ package com.tangem.feature.tester.presentation.navigation -import android.content.Context -import android.content.Intent import androidx.navigation.NavController -import com.tangem.feature.tester.presentation.TesterActivity -import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject /** * Implementation of router for tester feature * - * @property context activity context - * [REDACTED_AUTHOR] */ @ActivityScoped -internal class DefaultTesterRouter @Inject constructor( - @ActivityContext private val context: Context, -) : InnerTesterRouter { +internal class DefaultTesterRouter @Inject constructor() : InnerTesterRouter { private var navController: NavController? = null - override fun getEntryIntent(): Intent { - return Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - override fun setNavController(navController: NavController) { this.navController = navController } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt index 0d5468767f..7135c2e34c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/InnerTesterRouter.kt @@ -1,14 +1,13 @@ package com.tangem.feature.tester.presentation.navigation import androidx.navigation.NavController -import com.tangem.features.tester.api.TesterRouter /** * Inner feature router * [REDACTED_AUTHOR] */ -internal interface InnerTesterRouter : TesterRouter { +internal interface InnerTesterRouter { /** Set up a navigation controller that bound to tester navigation graph */ fun setNavController(navController: NavController) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt new file mode 100644 index 0000000000..133628c9ac --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/ShakeEventListener.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.tester.presentation.navigation + +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import kotlin.math.sqrt + +/** + * Listener for device shake events. + * + * This class implements [SensorEventListener] and is used to detect device shaking based on accelerometer data. + * When a shake is detected, the provided action is invoked. + * + * @property action lambda function to be called when a shake is detected + * +[REDACTED_AUTHOR] + */ +internal class ShakeEventListener(private val action: () -> Unit) : SensorEventListener { + + private var lastShakeTime = 0L + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit + + override fun onSensorChanged(event: SensorEvent?) { + if (event?.sensor?.type != Sensor.TYPE_ACCELEROMETER) return + + val acceleration = calculateAcceleration(event = event) + + val currentTime = System.currentTimeMillis() + val currentShakeInterval = currentTime - lastShakeTime + + if (acceleration > SHAKE_THRESHOLD && currentShakeInterval > SHAKE_INTERVAL_MS) { + lastShakeTime = currentTime + action() + } + } + + private fun calculateAcceleration(event: SensorEvent): Float { + val (x, y, z) = event.toXYZ() + + return sqrt(x * x + y * y + z * z) - SensorManager.GRAVITY_EARTH + } + + private fun SensorEvent.toXYZ() = Triple(values[0], values[1], values[2]) + + private companion object { + private const val SHAKE_THRESHOLD: Float = 12f + private const val SHAKE_INTERVAL_MS: Long = 1000 + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt index 58fcfdbf4e..a64b604f6c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/testpush/viewmodel/transformers/markettokens/TestPushMarketTokenClickBottomSheetTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.feature.tester.presentation.testpush.viewmodel.transformers.m import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.routing.DeepLinkRoute -import com.tangem.core.deeplink.DEEPLINK_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.DEEPLINK_KEY import com.tangem.domain.markets.TokenMarket import com.tangem.feature.tester.presentation.testpush.entity.TestPushUM import com.tangem.utils.transformer.Transformer diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index a42a691fc5..68e9bdc3a5 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -53,8 +53,6 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.datasource) - implementation(projects.core.deepLinks) - implementation(projects.core.deepLinks.global) implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.common.ui) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index a2fab77e92..1bc0b755b2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -2,13 +2,13 @@ package com.tangem.feature.tokendetails.deeplink import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY +import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.deeplink.DeeplinkConst.DERIVATION_PATH_KEY -import com.tangem.core.deeplink.DeeplinkConst.NETWORK_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TRANSACTION_ID_KEY -import com.tangem.core.deeplink.DeeplinkConst.TYPE_KEY -import com.tangem.core.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -125,7 +125,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( isMultiCurrency -> fetchCurrencyStatusUseCase.invoke( userWalletId = userWallet.walletId, id = cryptoCurrency.id, - refresh = true, ) !isMultiCurrency && tokensFeatureToggles.isWalletBalanceFetcherEnabled -> walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId)) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index e67344cdce..55966eb359 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model import androidx.compose.runtime.Stable import androidx.paging.cachedIn import arrow.core.getOrElse +import arrow.core.merge import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter @@ -11,7 +12,9 @@ import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -67,7 +70,6 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.deeplink.TokenDetailsDeepLinkActionListener @@ -134,6 +136,7 @@ internal class TokenDetailsModel @Inject constructor( private val appRouter: AppRouter, private val router: InnerTokenDetailsRouter, private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : Model(), TokenDetailsClickIntents { private val params = paramsContainer.require() @@ -205,15 +208,15 @@ internal class TokenDetailsModel @Inject constructor( checkForActionUpdates() } + fun onResume() { + subscribeOnExpressTransactionsUpdates() + } + fun onPause() { expressTxStatusTaskScheduler.cancelTask() expressTxJobHolder.cancel() } - fun onResume() { - subscribeOnExpressTransactionsUpdates() - } - override fun onDestroy() { expressTxStatusTaskScheduler.cancelTask() expressTxJobHolder.cancel() @@ -252,7 +255,7 @@ internal class TokenDetailsModel @Inject constructor( .launchIn(modelScope) } - private suspend fun updateButtons(currencyStatus: CryptoCurrencyStatus) { + private fun updateButtons(currencyStatus: CryptoCurrencyStatus) { getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = currencyStatus, @@ -304,66 +307,61 @@ internal class TokenDetailsModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - modelScope.launch(dispatchers.main) { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - .distinctUntilChanged() - .onEach { maybeCurrencyStatus -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) - maybeCurrencyStatus.onRight { status -> - cryptoCurrencyStatus = status - updateButtons(currencyStatus = status) - updateWarnings(status) - subscribeOnUpdateStakingInfo(status) - } - currencyStatusAnalyticsSender.send(maybeCurrencyStatus) + getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = userWallet is UserWallet.Cold && + userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), + ) + .distinctUntilChanged() + .onEach { maybeCurrencyStatus -> + internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) + maybeCurrencyStatus.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(currencyStatus = status) + updateWarnings(status) + subscribeOnUpdateStakingInfo(status) } - .flowOn(dispatchers.main) - .launchIn(modelScope) - .saveIn(marketPriceJobHolder) - } + currencyStatusAnalyticsSender.send(maybeCurrencyStatus) + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + .saveIn(marketPriceJobHolder) } private fun subscribeOnExpressTransactionsUpdates() { - modelScope.launch(dispatchers.main) { - expressTxStatusTaskScheduler.cancelTask() - expressStatusFactory - .getExpressStatuses() - .distinctUntilChanged() - .onEach { waitForFirstExpressStatusEmmit.value = true } - .onEach { expressTxs -> - internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( - expressTxs, - ::updateNetworkToSwapBalance, - ) - expressTxStatusTaskScheduler.scheduleTask( - modelScope, - PeriodicTask( - isDelayFirst = false, - delay = EXPRESS_STATUS_UPDATE_DELAY, - task = { - runCatching { - expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs) - } - }, - onSuccess = { updatedTxs -> - internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( - updatedTxs, - ::updateNetworkToSwapBalance, - ) - }, - onError = { /* no-op */ }, - ), - ) - } - .flowOn(dispatchers.main) - .launchIn(modelScope) - .saveIn(expressTxJobHolder) - } + expressTxStatusTaskScheduler.cancelTask() + expressStatusFactory.getExpressStatuses() + .distinctUntilChanged() + .onEach { waitForFirstExpressStatusEmmit.value = true } + .onEach { expressTxs -> + internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( + expressTxs = expressTxs, + updateBalance = ::updateNetworkToSwapBalance, + ) + expressTxStatusTaskScheduler.scheduleTask( + scope = modelScope, + task = PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { + expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs) + } + }, + onSuccess = { updatedTxs -> + internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( + updatedTxs, + ::updateNetworkToSwapBalance, + ) + }, + onError = { /* no-op */ }, + ), + ) + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + .saveIn(expressTxJobHolder) } private fun updateNetworkToSwapBalance(toCryptoCurrency: CryptoCurrency) { @@ -443,22 +441,49 @@ internal class TokenDetailsModel @Inject constructor( private fun updateTopBarMenu() { modelScope.launch(dispatchers.main) { - val hasDerivations = - networkHasDerivationUseCase( - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] - network = cryptoCurrency.network, - ).getOrElse { false } + val hasDerivations = networkHasDerivationUseCase( + userWallet = userWallet, + network = cryptoCurrency.network, + ).getOrElse { false } - val isSupported = getExtendedPublicKeyForCurrencyUseCase.isSupported(userWalletId, cryptoCurrency.network) + val isSupported = isXPUBSupported() internalUiState.value = stateFactory.getStateWithUpdatedMenu( - cardTypesResolver = userWallet.scanResponse.cardTypesResolver, + userWallet = userWallet, hasDerivations = hasDerivations, isSupported = isSupported, ) } } + private suspend fun isXPUBSupported(): Boolean { + return getExtendedPublicKeyForCurrencyUseCase.isSupported( + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + .mapLeft { + analyticsExceptionHandler.sendException( + event = ExceptionAnalyticsEvent( + exception = it, + params = mapOf( + "blockchainId" to cryptoCurrency.network.id.rawId.value, + "networkId" to cryptoCurrency.network.backendId, + ), + ), + ) + + Timber.e( + /* t = */ it, + /* message = */ "Unable to get wallet manager for user wallet %s and network %s", + /* ...args = */ userWalletId, + cryptoCurrency.network, + ) + + false + } + .merge() + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -750,13 +775,7 @@ internal class TokenDetailsModel @Inject constructor( modelScope.launch(dispatchers.main) { listOf( - async { - fetchCurrencyStatusUseCase( - userWalletId = userWalletId, - id = cryptoCurrency.id, - refresh = true, - ) - }, + async { fetchCurrencyStatusUseCase(userWalletId = userWalletId, id = cryptoCurrency.id) }, async { updateTxHistory( refresh = true, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 05bb854ee4..a78e619a41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -14,7 +14,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.* @@ -101,7 +100,7 @@ internal class TokenDetailsSkeletonStateConverter( val isBitcoin = isBitcoin(cryptoCurrency.network.rawId) val hasDerivations = networkHasDerivationUseCase( - scanResponse = userWallet.requireColdWallet().scanResponse, // TODO [REDACTED_TASK_KEY] + userWallet = userWallet, network = cryptoCurrency.network, ).getOrElse { false } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 5bdf57f4e7..82a04112f4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -24,6 +24,7 @@ import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero +import timber.log.Timber import java.math.BigDecimal internal class TokenDetailsStakingInfoConverter( @@ -45,6 +46,7 @@ internal class TokenDetailsStakingInfoConverter( state: TokenDetailsState, stakingAvailability: StakingAvailability, ): StakingBlockUM? { + Timber.i("Define staking block for [${status.currency.id.value}] with availability:\n$stakingAvailability") return when (stakingAvailability) { StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable StakingAvailability.Unavailable -> null @@ -60,6 +62,15 @@ internal class TokenDetailsStakingInfoConverter( val iconState = state.tokenInfoBlockState.iconState + Timber.i( + """ + getStakingInfoBlock: + – yieldBalance: $yieldBalance + – stakingCryptoAmount: $stakingCryptoAmount + – stakingEntryInfo: $stakingEntryInfo + """.trimIndent(), + ) + return when { stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { if (pendingBalances.isEmpty()) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 4819797954..1f62bb2fc0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.NetworkHasDerivationUseCase -import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress @@ -29,6 +29,7 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents @@ -348,7 +349,7 @@ internal class TokenDetailsStateFactory( } fun getStateWithUpdatedMenu( - cardTypesResolver: CardTypesResolver, + userWallet: UserWallet, hasDerivations: Boolean, isSupported: Boolean, ): TokenDetailsState { @@ -356,7 +357,7 @@ internal class TokenDetailsStateFactory( copy( topAppBarConfig = topAppBarConfig.copy( tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig - ?.updateMenu(cardTypesResolver, hasDerivations, isSupported), + ?.updateMenu(userWallet, hasDerivations, isSupported), ), ) } @@ -385,11 +386,16 @@ internal class TokenDetailsStateFactory( } private fun TokenDetailsAppBarMenuConfig.updateMenu( - cardTypesResolver: CardTypesResolver, + userWallet: UserWallet, hasDerivations: Boolean, isSupported: Boolean, ): TokenDetailsAppBarMenuConfig? { - if (cardTypesResolver.isSingleWalletWithToken()) return null + if (userWallet is UserWallet.Cold && + userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + ) { + return null + } + return copy( items = buildList { if (isSupported && hasDerivations) { 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 c12a0e8048..1e567bfc6a 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 @@ -52,7 +52,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( ) } - suspend operator fun invoke(): Flow> { + operator fun invoke(): Flow> { return swapTransactionRepository.getTransactions( userWallet = userWallet, cryptoCurrencyId = cryptoCurrency.id, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 4e9ee06a08..2378fa4b52 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -65,14 +65,12 @@ internal class ExpressStatusFactory @AssistedInject constructor( ) } - suspend fun getExpressStatuses(): Flow> = combine( + fun getExpressStatuses(): Flow> = combine( flow = exchangeStatusFactory(), flow2 = onrampStatusFactory(), ) { maybeExchange, maybeOnramp -> - persistentListOf( - maybeOnramp, - maybeExchange, - ).flatten() + persistentListOf(maybeOnramp, maybeExchange) + .flatten() .sortedByDescending { it.info.timestamp } .toPersistentList() } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index b85bb4de06..2d629ddca7 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -16,7 +16,9 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.* +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO @@ -24,6 +26,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.nft.DisableWalletNFTUseCase import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase +import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.wallets.models.UserWallet @@ -39,11 +42,11 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.ItemsBuilder -import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -63,15 +66,16 @@ internal class WalletSettingsModel @Inject constructor( private val analyticsContextProxy: AnalyticsContextProxy, private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, - private val nftFeatureToggles: NFTFeatureToggles, - private val getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, + getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase, private val enableWalletNFTUseCase: EnableWalletNFTUseCase, private val disableWalletNFTUseCase: DisableWalletNFTUseCase, private val notificationsToggles: NotificationsFeatureToggles, - private val getWalletNotificationsEnabledUseCase: GetWalletNotificationsEnabledUseCase, + getWalletNotificationsEnabledUseCase: GetWalletNotificationsEnabledUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val settingsManager: SettingsManager, private val permissionsRepository: PermissionRepository, + private val getApplicationIdUseCase: GetApplicationIdUseCase, + private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -101,7 +105,6 @@ internal class WalletSettingsModel @Inject constructor( userWallet = wallet, dialogNavigation = dialogNavigation, isRenameWalletAvailable = isRenameWalletAvailable, - isNFTFeatureEnabled = nftFeatureToggles.isNFTEnabled, isNFTEnabled = nftEnabled, isNotificationsEnabled = notificationsEnabled, isNotificationsFeatureEnabled = notificationsToggles.isNotificationsEnabled, @@ -127,7 +130,6 @@ internal class WalletSettingsModel @Inject constructor( userWallet: UserWallet, dialogNavigation: SlotNavigation, isRenameWalletAvailable: Boolean, - isNFTFeatureEnabled: Boolean, isNFTEnabled: Boolean, isNotificationsFeatureEnabled: Boolean, isNotificationsEnabled: Boolean, @@ -141,7 +143,7 @@ internal class WalletSettingsModel @Inject constructor( isManageTokensAvailable = userWallet.isMultiCurrency, isRenameWalletAvailable = isRenameWalletAvailable, renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, - isNFTFeatureEnabled = isNFTFeatureEnabled && userWallet.isMultiCurrency, + isNFTFeatureEnabled = userWallet.isMultiCurrency, isNFTEnabled = isNFTEnabled, onCheckedNFTChange = ::onCheckedNFTChange, forgetWallet = { @@ -194,10 +196,20 @@ internal class WalletSettingsModel @Inject constructor( if (hasUserWallets) { router.pop() } else { + clearWalletsAssociatedWithApplicationId() router.replaceAll(AppRoute.Home) } } + private fun clearWalletsAssociatedWithApplicationId() = modelScope.launch(NonCancellable) { + getApplicationIdUseCase().onRight { applicationId -> + associateWalletsWithApplicationIdUseCase(applicationId, emptyList()) + .onLeft { + Timber.e("Unable to associate empty wallets with application ID: $it") + } + } + } + private fun onLinkMoreCardsClick(scanResponse: ScanResponse) { analyticsEventHandler.send(Settings.ButtonCreateBackup) analyticsContextProxy.addContext(scanResponse) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index a81c3a07e1..4f26ae1d9a 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -56,8 +56,6 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(projects.core.deepLinks) - implementation(projects.core.deepLinks.global) implementation(projects.core.decompose) implementation(projects.core.datasource) implementation(projects.core.res) @@ -99,6 +97,7 @@ dependencies { /** Feature Apis */ implementation(projects.features.details.api) + implementation(projects.features.hotWallet.api) implementation(projects.features.manageTokens.api) implementation(projects.features.markets.api) implementation(projects.features.onboardingV2.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 4891a75416..327c2b8470 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -4,16 +4,11 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.deeplink.global.ReferralDeepLink import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.nft.ObserveAndClearNFTCacheIfNeedUseCase @@ -27,7 +22,6 @@ import com.tangem.domain.wallets.models.isMultiCurrency import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender @@ -73,7 +67,6 @@ internal class WalletModel @Inject constructor( private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, - private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, @@ -81,12 +74,9 @@ internal class WalletModel @Inject constructor( private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, private val analyticsEventsHandler: AnalyticsEventHandler, - private val deepLinksRegistry: DeepLinksRegistry, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val walletContentFetcher: WalletContentFetcher, private val tokensFeatureToggles: TokensFeatureToggles, - private val appRouter: AppRouter, - private val routingFeatureToggle: RoutingFeatureToggle, private val observeAndClearNFTCacheIfNeedUseCase: ObserveAndClearNFTCacheIfNeedUseCase, private val walletDeepLinkActionListener: WalletDeepLinkActionListener, val screenLifecycleProvider: ScreenLifecycleProvider, @@ -241,12 +231,6 @@ internal class WalletModel @Inject constructor( selectedWalletAnalyticsSender.send(selectedWallet) } - if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { - // Registering here, because `WalletDeepLinksHandler` unregisters deeplink when scope is cancelled - // This is temporary solution, will be removed with complete deeplink navigation overhaul - addReferralDeepLink(selectedWallet) - walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet) - } subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } @@ -268,20 +252,6 @@ internal class WalletModel @Inject constructor( } } - private fun addReferralDeepLink(userWallet: UserWallet) { - deepLinksRegistry.register( - ReferralDeepLink( - onReceive = { - if (userWallet !is UserWallet.Cold || userWallet.cardTypesResolver.isTangemWallet()) { - appRouter.push( - AppRoute.ReferralProgram(userWalletId = userWallet.walletId), - ) - } - }, - ), - ) - } - // We need to update the current wallet quotes if the application was in the background for more than 10 seconds // and then returned to the foreground private fun subscribeToScreenBackgroundState() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 8502c266f8..25205137ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -10,7 +10,6 @@ import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase -import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -140,7 +139,7 @@ internal class WalletClickIntents @Inject constructor( val maybeFetchResult = if (isSingleWalletWithToken) { fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) } else { - fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) + fetchTokenListUseCase(userWalletId = userWallet.walletId) } maybeFetchResult.onLeft { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 724ac04b1b..cbc0e161ef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -81,6 +81,8 @@ internal interface WalletWarningsClickIntents { fun onSeedPhraseSecondNotificationAccept() fun onSeedPhraseSecondNotificationReject() + + fun onFinishWalletActivationClick() } @Suppress("LongParameterList") @@ -369,6 +371,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onFinishWalletActivationClick() { + // TODO implement wallet activation process + } + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 9a3d8d6a28..ec549bf481 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -117,6 +117,7 @@ internal object WalletScreenPreviewData { buttons = persistentListOf(buyButton), warnings = persistentListOf( WalletNotification.Warning.SomeNetworksUnreachable, + WalletNotification.FinishWalletActivation { }, ), bottomSheetConfig = null, tokensListState = textContentTokensState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt deleted file mode 100644 index 987fcedb41..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.feature.wallet.presentation.deeplink - -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.core.deeplink.DeepLink -import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.deeplink.global.SellCurrencyDeepLink -import com.tangem.domain.tokens.GetCryptoCurrencyUseCase -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.launchOnCancellation -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList") -internal class WalletDeepLinksHandler @Inject constructor( - private val deepLinksRegistry: DeepLinksRegistry, - private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, - private val router: AppRouter, -) { - - private var deepLinksMap = mutableMapOf>() - - fun registerForWallet(scope: CoroutineScope, userWallet: UserWallet) { - val deepLinks = deepLinksMap.getOrPut(userWallet.walletId) { - getDeepLinks(userWallet, scope) - } - deepLinksRegistry.unregisterByIds(deepLinks.map { it.id }) - deepLinksRegistry.register(deepLinks = deepLinks) - - // When navigation to another screen scope is Cancelled and deeplinks are hot handled - scope.launchOnCancellation { - deepLinksRegistry.unregister(deepLinks) - } - } - - private fun getDeepLinks(userWallet: UserWallet, scope: CoroutineScope): List { - val sellCurrencyDeepLink = SellCurrencyDeepLink( - onReceive = { data -> - scope.launch { - onSellCurrencyDeepLink(userWallet, data) - } - }, - shouldHandleDelayed = true, - ) - - return buildList { - add(sellCurrencyDeepLink) - } - } - - private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) { - val cryptoCurrency = getCryptoCurrencyUseCase(userWallet, data.currencyId).getOrNull() - - if (cryptoCurrency == null) { - Timber.e("onSellCurrencyDeepLink cryptoCurrency is null") - return - } - - val route = AppRoute.Send( - currency = cryptoCurrency, - userWalletId = userWallet.walletId, - transactionId = data.transactionId, - destinationAddress = data.depositWalletAddress, - amount = data.baseCurrencyAmount, - tag = data.depositWalletAddressTag, - ) - - router.push(route) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 0db4af387d..d75237b768 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -61,6 +61,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.NetworksUnreachable, is WalletNotification.UsedOutdatedData, is WalletNotification.UnlockVisaAccess, + is WalletNotification.FinishWalletActivation, -> null is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index d8755f0f4a..8cbf18fc93 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -55,6 +55,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + addFinishWalletActivationNotification(userWallet, clickIntents) + addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -259,6 +261,23 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( if (condition) add(element = element) } + private fun MutableList.addFinishWalletActivationNotification( + userWallet: UserWallet, + clickIntents: WalletClickIntents, + ) { + if (userWallet !is UserWallet.Hot) return + + // TODO [REDACTED_TASK_KEY] set an actual value + val shouldShowFinishActivation = false + + addIf( + element = WalletNotification.FinishWalletActivation( + onFinishClick = clickIntents::onFinishWalletActivationClick, + ), + condition = shouldShowFinishActivation, + ) + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt index e931183ffe..3a7aca03bb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt @@ -2,21 +2,17 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.features.nft.NFTFeatureToggles import kotlinx.coroutines.flow.firstOrNull class IsWalletNFTEnabledSyncUseCase( private val walletsRepository: WalletsRepository, - private val nftFeatureToggles: NFTFeatureToggles, ) { - suspend operator fun invoke(userWalletId: UserWalletId): Boolean = if (nftFeatureToggles.isNFTEnabled) { - walletsRepository - .nftEnabledStatuses() + suspend operator fun invoke(userWalletId: UserWalletId): Boolean { + val isNFTEnabled = walletsRepository.nftEnabledStatuses() .firstOrNull() - ?.let { it[userWalletId] } - ?: false - } else { - false + ?.get(userWalletId) + + return isNFTEnabled == true } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 789ebeb403..0dbcd8efeb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -39,10 +39,22 @@ internal object WalletAdditionalInfoFactory { wallet.resolveSingleCurrencyInfo(currencyAmount) } } - is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") + is UserWallet.Hot -> wallet.resolveAdditionalInfo() } } + private fun UserWallet.Hot.resolveAdditionalInfo(): WalletAdditionalInfo { + return WalletAdditionalInfo( + hideable = false, + content = TextReference.Res(R.string.hw_mobile_wallet) + + when { + isLocked -> DIVIDER + TextReference.Res(R.string.common_locked) + backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) + else -> TextReference.Str("") + }, + ) + } + private fun UserWallet.Cold.resolveMultiCurrencyInfo(): WalletAdditionalInfo { return if (isLocked) { WalletAdditionalInfo( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index ed1f9e71a9..b509593f42 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -24,7 +24,11 @@ internal class WalletImageResolver @Inject constructor( /** Get a specified wallet [userWallet] image */ @Suppress("CyclomaticComplexMethod") @DrawableRes - fun resolve(userWallet: UserWallet.Cold): Int? { + fun resolve(userWallet: UserWallet): Int? { + if (userWallet !is UserWallet.Cold) { + return null + } + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver val cobrandImage = Wallet2CobrandImage.entries.firstOrNull { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt index 31c9e2f4ac..3d4fba0c0b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -31,7 +31,7 @@ internal class WalletContentLoaderFactory @Inject constructor( userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isVisaWallet() -> { visaWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh) } - !userWallet.isMultiCurrency -> { + userWallet is UserWallet.Cold && !userWallet.isMultiCurrency -> { singleWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh) } else -> null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 007c74fcc6..f06e06fe21 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,8 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -21,11 +19,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActionButtonsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber -import com.tangem.features.nft.NFTFeatureToggles @Suppress("LongParameterList") @ModelScoped @@ -45,11 +38,8 @@ internal class MultiWalletContentLoader( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val deepLinksRegistry: DeepLinksRegistry, - private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, private val currenciesRepository: CurrenciesRepository, - private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -64,19 +54,17 @@ internal class MultiWalletContentLoader( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, - deepLinksRegistry = deepLinksRegistry, - routingFeatureToggle = routingFeatureToggle, ).let(::add) - if (nftFeatureToggles.isNFTEnabled) { - WalletNFTListSubscriber( - userWallet = userWallet, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - stateHolder = stateHolder, - walletsRepository = walletsRepository, - clickIntents = clickIntents, - currenciesRepository = currenciesRepository, - ).let(::add) - } + + WalletNFTListSubscriber( + userWallet = userWallet, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, + stateHolder = stateHolder, + walletsRepository = walletsRepository, + clickIntents = clickIntents, + currenciesRepository = currenciesRepository, + ).let(::add) + MultiWalletWarningsSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -85,11 +73,13 @@ internal class MultiWalletContentLoader( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, ).let(::add) + MultiWalletActionButtonsSubscriber( userWallet = userWallet, stateHolder = stateHolder, getStoryContentUseCase = getStoryContentUseCase, ).let(::add) + WalletDropDownItemsSubscriber( stateHolder = stateHolder, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 9cdb3f5767..49216f3aac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -1,8 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -20,7 +18,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.features.nft.NFTFeatureToggles import javax.inject.Inject @Suppress("LongParameterList") @@ -38,11 +35,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val deepLinksRegistry: DeepLinksRegistry, - private val nftFeatureToggles: NFTFeatureToggles, private val walletsRepository: WalletsRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val routingFeatureToggle: RoutingFeatureToggle, private val currenciesRepository: CurrenciesRepository, ) { @@ -62,11 +56,8 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - deepLinksRegistry = deepLinksRegistry, - nftFeatureToggles = nftFeatureToggles, walletsRepository = walletsRepository, getNFTCollectionsUseCase = getNFTCollectionsUseCase, - routingFeatureToggle = routingFeatureToggle, currenciesRepository = currenciesRepository, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index 81601a2e31..1ac6fa6d5a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -11,21 +11,15 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.feature.wallet.presentation.wallet.subscribers.PrimaryCurrencySubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletButtonsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletNotificationsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.TxHistorySubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletDropDownItemsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber @Suppress("LongParameterList") internal class SingleWalletContentLoader( - private val userWallet: UserWallet, + private val userWallet: UserWallet.Cold, private val clickIntents: WalletClickIntents, private val isRefresh: Boolean, private val stateHolder: WalletStateController, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt index 22f8884acd..028f50a096 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -12,10 +12,10 @@ import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import javax.inject.Inject @ModelScoped @@ -36,7 +36,7 @@ internal class SingleWalletContentLoaderFactory @Inject constructor( private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, ) { - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { + fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { return SingleWalletContentLoader( userWallet = userWallet, clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 811b27a4f4..ea3298c5cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -1,7 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.common.routing.RoutingFeatureToggle -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase @@ -19,7 +17,7 @@ import com.tangem.feature.wallet.presentation.wallet.subscribers.* @Suppress("LongParameterList") internal class SingleWalletWithTokenContentLoader( - private val userWallet: UserWallet, + private val userWallet: UserWallet.Cold, private val clickIntents: WalletClickIntents, private val stateHolder: WalletStateController, private val tokenListAnalyticsSender: TokenListAnalyticsSender, @@ -32,8 +30,6 @@ internal class SingleWalletWithTokenContentLoader( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val deepLinksRegistry: DeepLinksRegistry, - private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -47,8 +43,6 @@ internal class SingleWalletWithTokenContentLoader( tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, - deepLinksRegistry = deepLinksRegistry, - routingFeatureToggle = routingFeatureToggle, ).let(::add) MultiWalletWarningsSubscriber( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 8f7e5cb7cf..f9f9697522 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -1,13 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender @@ -15,7 +14,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import javax.inject.Inject // TODO: Refactor @@ -33,11 +31,9 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val deepLinksRegistry: DeepLinksRegistry, - private val routingFeatureToggle: RoutingFeatureToggle, ) { - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { + fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { return SingleWalletWithTokenContentLoader( userWallet = userWallet, clickIntents = clickIntents, @@ -52,8 +48,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - deepLinksRegistry = deepLinksRegistry, - routingFeatureToggle = routingFeatureToggle, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt index 496e989123..9b0f1d7258 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoader.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscribe import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents internal class VisaWalletContentLoader( - private val userWallet: UserWallet, + private val userWallet: UserWallet.Cold, private val clickIntents: WalletClickIntents, private val isRefresh: Boolean, private val stateController: WalletStateController, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt index 30dcbd7652..13d116fcaa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/VisaWalletContentLoaderFactory.kt @@ -15,7 +15,7 @@ internal class VisaWalletContentLoaderFactory @Inject constructor( private val getVisaTxHistoryUseCase: GetVisaTxHistoryUseCase, ) { - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { + fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { return VisaWalletContentLoader( userWallet = userWallet, clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 8315a590cd..965b55df30 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -255,6 +255,20 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) + data class FinishWalletActivation( + val onFinishClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(R.string.hw_activation_need_title), + subtitle = resourceReference(R.string.hw_activation_need_description), + iconResId = R.drawable.img_knight_shield_32, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = onFinishClick, + ), + ), + ) + data class ReferralPromo( val onCloseClick: () -> Unit, val onClick: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 1fe2ece7a1..c5e1a1ba70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.isLocked -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* @@ -55,8 +54,6 @@ internal class InitializeWalletsTransformer( } private fun createLockedState(userWallet: UserWallet): WalletState { - userWallet.requireColdWallet() - return userWallet.createStateByWalletType( multiCurrencyCreator = { WalletState.MultiCurrency.Locked( @@ -87,7 +84,7 @@ internal class InitializeWalletsTransformer( ) } - private fun UserWallet.Cold.toLockedWalletCardState(): WalletCardState { + private fun UserWallet.toLockedWalletCardState(): WalletCardState { return WalletCardState.LockedContent( id = walletId, title = name, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index bbb5f0894b..161bf4e8ff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -6,7 +6,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -57,8 +56,6 @@ internal class SetTokenListErrorTransformer( } private fun WalletCardState.toLoadedState(): WalletCardState { - selectedWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - return WalletCardState.Content( id = id, title = title, @@ -68,7 +65,10 @@ internal class SetTokenListErrorTransformer( balance = BigDecimal.ZERO.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, - cardCount = selectedWallet.getCardsCount(), + cardCount = when (selectedWallet) { + is UserWallet.Cold -> selectedWallet.getCardsCount() + is UserWallet.Hot -> null + }, isZeroBalance = true, isBalanceFlickering = false, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index afa9bd0b99..036c752346 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -13,7 +13,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.visa.exception.RefreshTokenExpiredException import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState @@ -27,7 +26,7 @@ import org.joda.time.DateTime import org.joda.time.Days internal class SetVisaInfoTransformer( - private val userWallet: UserWallet, + private val userWallet: UserWallet.Cold, private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntents, ) : TypedWalletStateTransformer( @@ -76,8 +75,6 @@ internal class SetVisaInfoTransformer( } private fun getContentWalletCardState(prevState: WalletCardState, visaCurrency: VisaCurrency): WalletCardState { - userWallet.requireColdWallet() // TODO [REDACTED_TASK_KEY] - return with(prevState) { WalletCardState.Content( id = id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 4cca1c04bb..746d921e5c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter @@ -48,8 +47,6 @@ internal class MultiWalletCardStateConverter( } private fun WalletCardState.toWalletCardState(fiatBalance: TotalFiatBalance.Loaded): WalletCardState { - selectedWallet.requireColdWallet() - return WalletCardState.Content( id = id, title = title, @@ -60,7 +57,10 @@ internal class MultiWalletCardStateConverter( fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, isZeroBalance = fiatBalance.amount.isZero(), - cardCount = selectedWallet.getCardsCount(), + cardCount = when (selectedWallet) { + is UserWallet.Cold -> selectedWallet.getCardsCount() + is UserWallet.Hot -> null + }, isBalanceFlickering = fiatBalance.source == StatusSource.CACHE, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 809632aca9..b12ae61d00 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.StringsSigns.DASH_SIGN @@ -64,7 +63,10 @@ internal class SingleWalletCardStateConverter( imageResId = imageResId, dropDownItems = dropDownItems, balance = formatFiatAmount(status = status, appCurrency = appCurrency), - cardCount = selectedWallet.requireColdWallet().getCardsCount(), // TODO [REDACTED_TASK_KEY] + cardCount = when (selectedWallet) { + is UserWallet.Cold -> selectedWallet.getCardsCount() + is UserWallet.Hot -> null + }, isZeroBalance = status.fiatAmount?.isZero(), isBalanceFlickering = (status as? CryptoCurrencyStatus.Loaded)?.sources?.total == StatusSource.CACHE, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt index a99ddbd3a5..805796e869 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt @@ -4,14 +4,17 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -internal inline fun UserWallet.Cold.createStateByWalletType( +internal inline fun UserWallet.createStateByWalletType( multiCurrencyCreator: () -> WalletState.MultiCurrency, singleCurrencyCreator: () -> WalletState.SingleCurrency, visaWalletCreator: () -> WalletState.Visa, -): WalletState = when { - isVisaWallet() -> visaWalletCreator() - isWalletWithTokens() -> multiCurrencyCreator() - else -> singleCurrencyCreator() +): WalletState = when (this) { + is UserWallet.Cold -> when { + isVisaWallet() -> visaWalletCreator() + isWalletWithTokens() -> multiCurrencyCreator() + else -> singleCurrencyCreator() + } + is UserWallet.Hot -> multiCurrencyCreator() } private fun UserWallet.Cold.isWalletWithTokens(): Boolean { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 5efd04f762..b95fcb4bb0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -6,7 +6,6 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver @@ -26,12 +25,35 @@ internal class WalletLoadingStateFactory( ) { fun create(userWallet: UserWallet): WalletState { - userWallet.requireColdWallet() + return when (userWallet) { + is UserWallet.Cold -> { + userWallet.createStateByWalletType( + multiCurrencyCreator = { createLoadingMultiCurrencyContent(userWallet) }, + singleCurrencyCreator = { createLoadingSingleCurrencyContent(userWallet) }, + visaWalletCreator = { createLoadingVisaWalletContent(userWallet) }, + ) + } + is UserWallet.Hot -> { + createLoadingHotWalletContent(userWallet) + } + } + } - return userWallet.createStateByWalletType( - multiCurrencyCreator = { createLoadingMultiCurrencyContent(userWallet) }, - singleCurrencyCreator = { createLoadingSingleCurrencyContent(userWallet) }, - visaWalletCreator = { createLoadingVisaWalletContent(userWallet) }, + private fun createLoadingHotWalletContent(userWallet: UserWallet.Hot): WalletState.MultiCurrency.Content { + return WalletState.MultiCurrency.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletCardState = WalletCardState.Loading( + id = userWallet.walletId, + title = userWallet.name, + additionalInfo = null, // TODO [REDACTED_TASK_KEY] + imageResId = null, // TODO [REDACTED_TASK_KEY] + dropDownItems = persistentListOf(), + ), + buttons = createMultiWalletActions(userWallet), + warnings = persistentListOf(), + bottomSheetConfig = null, + tokensListState = WalletTokensListState.ContentState.Loading, + nftState = WalletNFTItemUM.Hidden, ) } @@ -99,15 +121,21 @@ internal class WalletLoadingStateFactory( ) } - private fun createMultiWalletActions(userWallet: UserWallet.Cold): PersistentList { - val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + private fun createMultiWalletActions(userWallet: UserWallet): PersistentList { + val isSingleWalletWithToken = + userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() if (isSingleWalletWithToken) return persistentListOf() return persistentListOf( WalletManageButton.Buy( enabled = true, dimContent = false, - onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId, WALLET_TYPE) }, + onClick = { + clickIntents.onMultiWalletBuyClick( + userWalletId = userWallet.walletId, + WALLET_TYPE, + ) + }, ), WalletManageButton.Swap( enabled = true, @@ -124,14 +152,24 @@ internal class WalletLoadingStateFactory( private fun createVisaDimmedButtons(): PersistentList { return persistentListOf( - WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Receive( + enabled = true, + dimContent = true, + onClick = {}, + onLongClick = null, + ), WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), ) } private fun createDimmedButtons(): PersistentList { return persistentListOf( - WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Receive( + enabled = true, + dimContent = true, + onClick = {}, + onLongClick = null, + ), WalletManageButton.Send(enabled = true, dimContent = true, onClick = {}), WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), WalletManageButton.Sell(enabled = true, dimContent = true, onClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index 5370e45d02..56ca8030c6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -1,10 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import arrow.core.getOrElse -import com.tangem.common.routing.RoutingFeatureToggle -import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.deeplink.global.ReferralDeepLink -import com.tangem.core.deeplink.global.SellCurrencyDeepLink import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce @@ -39,8 +35,6 @@ internal abstract class BasicTokenListSubscriber( private val walletWithFundsChecker: WalletWithFundsChecker, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, - private val deepLinksRegistry: DeepLinksRegistry, - private val routingFeatureToggle: RoutingFeatureToggle, ) : WalletSubscriber() { private val sendAnalyticsJobHolder = JobHolder() @@ -48,6 +42,8 @@ internal abstract class BasicTokenListSubscriber( protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow + protected abstract suspend fun onTokenListReceived(maybeTokenList: Lce) + override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( flow = tokenListFlow(coroutineScope) @@ -58,11 +54,9 @@ internal abstract class BasicTokenListSubscriber( } .distinctUntilChanged() .onEach { maybeTokenList -> - if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { - coroutineScope.launch { - onTokenListReceived(maybeTokenList) - }.saveIn(onTokenListReceivedJobHolder) - } + coroutineScope.launch { + onTokenListReceived(maybeTokenList) + }.saveIn(onTokenListReceivedJobHolder) coroutineScope.launch { startCheck(maybeTokenList) } }, @@ -77,8 +71,7 @@ internal abstract class BasicTokenListSubscriber( ifLoading = { maybeContent -> val isRefreshing = stateHolder.getWalletState(userWallet.walletId) ?.pullToRefreshConfig - ?.isRefreshing - ?: false + ?.isRefreshing == true maybeContent ?.takeIf { !isRefreshing } @@ -117,19 +110,6 @@ internal abstract class BasicTokenListSubscriber( } } - protected open suspend fun onTokenListReceived(maybeTokenList: Lce) { - /* no-op */ - // Handling sell deeplink requires full content in order to correctly open Send screen - if (maybeTokenList.getOrNull(false) != null) { - deepLinksRegistry.triggerDelayedDeeplink(deepLinkClass = SellCurrencyDeepLink::class.java) - } - // Handling referral deeplink requires only selected wallet to be loaded - // This is temporary solution, will be removed with complete deeplink navigation overhaul - if (maybeTokenList.getOrNull(true) != null) { - deepLinksRegistry.triggerDelayedDeeplink(deepLinkClass = ReferralDeepLink::class.java) - } - } - private suspend fun sendTokenListAnalytics(maybeTokenList: Lce) { val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index fafdfc910f..d236857b9f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -1,7 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import com.tangem.common.routing.RoutingFeatureToggle -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow @@ -30,8 +28,6 @@ internal class MultiWalletTokenListSubscriber( walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, - deepLinksRegistry: DeepLinksRegistry, - routingFeatureToggle: RoutingFeatureToggle, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -40,8 +36,6 @@ internal class MultiWalletTokenListSubscriber( walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, - deepLinksRegistry = deepLinksRegistry, - routingFeatureToggle = routingFeatureToggle, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { @@ -52,7 +46,6 @@ internal class MultiWalletTokenListSubscriber( override suspend fun onTokenListReceived(maybeTokenList: Lce) { updateSortingIfNeeded(maybeTokenList) - super.onTokenListReceived(maybeTokenList) } private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index 467d739893..f551f2885c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -1,8 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import com.tangem.common.routing.RoutingFeatureToggle -import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError @@ -17,7 +16,7 @@ import kotlinx.coroutines.CoroutineScope @Suppress("LongParameterList") internal class SingleWalletWithTokenListSubscriber( - private val userWallet: UserWallet, + private val userWallet: UserWallet.Cold, private val tokenListStore: MultiWalletTokenListStore, stateHolder: WalletStateController, clickIntents: WalletClickIntents, @@ -25,8 +24,6 @@ internal class SingleWalletWithTokenListSubscriber( walletWithFundsChecker: WalletWithFundsChecker, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, - deepLinksRegistry: DeepLinksRegistry, - routingFeatureToggle: RoutingFeatureToggle, ) : BasicTokenListSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -35,8 +32,6 @@ internal class SingleWalletWithTokenListSubscriber( walletWithFundsChecker = walletWithFundsChecker, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, - deepLinksRegistry = deepLinksRegistry, - routingFeatureToggle = routingFeatureToggle, ) { override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { @@ -44,4 +39,6 @@ internal class SingleWalletWithTokenListSubscriber( return tokenListStore.getOrThrow(userWallet.walletId) } + + override suspend fun onTokenListReceived(maybeTokenList: Lce) = Unit } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index efb3aae1cd..5765489893 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -13,7 +13,6 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -32,7 +31,7 @@ typealias MaybeTxHistoryItems = Either - val blockchain = userWallet.requireColdWallet().scanResponse.cardTypesResolver.getBlockchain() + val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() val itemConverter = TxHistoryItemStateConverter( symbol = blockchain.currency, decimals = blockchain.decimals(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt index 03e49e67f0..af5c1ec03e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.map import timber.log.Timber internal class VisaWalletSubscriber( - private val userWallet: UserWallet, + private val userWallet: UserWallet.Cold, private val stateController: WalletStateController, private val isRefresh: Boolean, private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index a4104329ed..ace0bd4279 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -22,7 +22,6 @@ import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.isMultiCurrency -import com.tangem.domain.wallets.models.requireColdWallet import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R @@ -88,8 +87,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( private fun loadArtworks(wallets: List): Flow> { return flow { emit(hashMapOf()) // emits right away so the transform doesn't wait for the images' loading to finish - wallets.forEach { wallet -> - wallet.requireColdWallet() + wallets.filterIsInstance().forEach { wallet -> val artwork = getCardImageUseCase( cardId = wallet.cardId, manufacturerName = wallet.scanResponse.card.manufacturer.name, diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index f0cd4ec491..9bccd9b7a3 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.common.routing) implementation(projects.common.ui) + implementation(projects.core.analytics) /** Domain models */ implementation(projects.domain.appCurrency.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt index 0871ed2cba..02928cb95d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt @@ -11,6 +11,7 @@ data class WcConnectedAppInfoUM( val notification: WcAppInfoSecurityNotification? = null, val appSubtitle: String, val walletName: String, + val connectingTime: Long?, val networks: ImmutableList, val disconnectButtonConfig: WcPrimaryButtonConfig, val onDismiss: () -> Unit, 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 303e583776..b35c69510f 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 @@ -69,7 +69,9 @@ internal class WcConnectedAppInfoModel @Inject constructor( verifiedDAppState = extractVerifiedState(session), appSubtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData), walletName = session.wallet.name, + connectingTime = session.connectingTime, networks = session.networks + .distinctBy { network -> network.rawId } .map { WcNetworkInfoItem.Required( id = it.rawId, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 7dcf11a07d..d027a899cb 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -19,6 +20,7 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSession @@ -47,6 +49,7 @@ internal class WcConnectionsModel @Inject constructor( private val wcDisconnectUseCase: WcDisconnectUseCase, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, + private val analytics: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -56,6 +59,7 @@ internal class WcConnectionsModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { + analytics.send(WcAnalyticEvents.ScreenOpened) listenQrUpdates() listenWcSessions() } 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 40353377db..dab4eecca2 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 @@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.connections.model import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.stack.* import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -11,6 +12,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.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -42,10 +44,12 @@ private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2 @Stable @ModelScoped +@Suppress("LongParameterList") internal class WcPairModel @Inject constructor( private val router: Router, private val messageSender: UiMessageSender, override val dispatchers: CoroutineDispatcherProvider, + private val analytics: AnalyticsEventHandler, wcPairUseCaseFactory: WcPairUseCase.Factory, getWalletsUseCase: GetWalletsUseCase, paramsContainer: ParamsContainer, @@ -165,10 +169,22 @@ internal class WcPairModel @Inject constructor( } private fun showUnknownDomainAlert() { + val event = WcAnalyticEvents.NoticeSecurityAlert( + dAppMetaData = sessionProposal.dAppMetaData, + securityStatus = sessionProposal.securityStatus, + source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain, + ) + analytics.send(event) stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnknownDomain)) } private fun showSecurityRiskAlert() { + val event = WcAnalyticEvents.NoticeSecurityAlert( + dAppMetaData = sessionProposal.dAppMetaData, + securityStatus = sessionProposal.securityStatus, + source = WcAnalyticEvents.NoticeSecurityAlert.Source.Domain, + ) + analytics.send(event) stackNavigation.pushNew(WcAppInfoRoutes.Alert(WcAppInfoRoutes.Alert.Type.UnsafeDomain)) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt index 95f99d2965..f4662ea4ae 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt @@ -24,9 +24,13 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday +import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.ImmutableList @@ -44,6 +48,15 @@ internal fun WcConnectedAppInfoBS(state: WcConnectedAppInfoUM) { title = { TangemModalBottomSheetTitle( title = resourceReference(R.string.wc_wallet_connect), + subtitle = state.connectingTime?.let { timestamp -> + resourceReference( + R.string.send_date_format, + wrappedList( + timestamp.toDateFormatWithTodayYesterday(DateTimeFormatters.dateFormatter), + timestamp.toTimeFormat(), + ), + ) + }, endIconRes = R.drawable.ic_close_24, onEndClick = state.onDismiss, ) @@ -185,6 +198,7 @@ private fun WcConnectedAppInfoBS_Preview() { isVerified = true, appSubtitle = "react-app.walletconnect.com", walletName = "Tangem 2.0", + connectingTime = System.currentTimeMillis(), networks = persistentListOf( WcNetworkInfoItem.Required( id = "1", 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 7089bc96f9..4ecb4fd6c9 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 @@ -45,6 +45,7 @@ internal fun getWcCommonScreen( params = FeeSelectorParams.FeeSelectorDetailsParams( state = state.feeSelectorUM, onLoadFee = model::loadFee, + feeCryptoCurrencyStatus = model.cryptoCurrencyStatus, cryptoCurrencyStatus = model.cryptoCurrencyStatus, callback = model, suggestedFeeState = model.suggestedFeeState, 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 26b445e05c..9448938815 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 @@ -26,6 +26,7 @@ internal class WcSendTransactionComponent( state = state.feeSelectorUM, onLoadFee = model::loadFee, cryptoCurrencyStatus = model.cryptoCurrencyStatus, + feeCryptoCurrencyStatus = model.cryptoCurrencyStatus, suggestedFeeState = model.suggestedFeeState, feeDisplaySource = FeeSelectorParams.FeeDisplaySource.BottomSheet, ), 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 db845051ad..23e9e56b77 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 @@ -8,6 +8,7 @@ import com.arkivanov.decompose.router.stack.pushNew import com.domain.blockaid.models.transaction.ValidationResult import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -21,7 +22,10 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory +import com.tangem.domain.walletconnect.model.WcRequestError +import com.tangem.domain.walletconnect.model.WcRequestError.Companion.message import com.tangem.domain.walletconnect.usecase.method.* import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback @@ -56,6 +60,7 @@ internal class WcSendTransactionModel @Inject constructor( private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter, private val getFeeUseCase: GetFeeUseCase, private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase, + private val analytics: AnalyticsEventHandler, ) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback { private val params = paramsContainer.require() @@ -217,6 +222,19 @@ internal class WcSendTransactionModel @Inject constructor( } else { sign() } + securityCheck?.result?.validation?.let { securityStatus -> + val event = WcAnalyticEvents.NoticeSecurityAlert( + dAppMetaData = useCase.session.sdkModel.appMetaData, + securityStatus = useCase.session.securityStatus, + source = WcAnalyticEvents.NoticeSecurityAlert.Source.SmartContract, + ) + when (securityStatus) { + ValidationResult.SAFE -> Unit + ValidationResult.UNSAFE, + ValidationResult.FAILED_TO_VALIDATE, + -> analytics.send(event) + } + } } fun onClickDoneCustomAllowance(value: BigDecimal, isUnlimited: Boolean) { @@ -252,10 +270,10 @@ internal class WcSendTransactionModel @Inject constructor( return false } - private fun handleSigningError(result: Either, useCase: WcSignUseCase<*>) { + private fun handleSigningError(result: Either, useCase: WcSignUseCase<*>) { if (result.isLeft()) { val error = WcTransactionRoutes.Alert.Type.UnknownError( - errorMessage = result.leftOrNull()?.message, + errorMessage = result.leftOrNull()?.message(), onDismiss = { cancel(useCase) }, ) stackNavigation.pushNew(WcTransactionRoutes.Alert(error)) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 5cd5b4641b..c1b01e6007 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,12 +5,14 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1104" +tangemBlockchainSdk = "develop-1110" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-487" +tangemCardSdk = "develop-489" #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 ^ +tangemHotSdk = "develop-438" +#tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ @@ -18,6 +20,8 @@ tangemVico = "2.0.0-alpha.25-tangem12" blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } +hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } +hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainFeeUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainFeeUtils.kt new file mode 100644 index 0000000000..30f9d06686 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainFeeUtils.kt @@ -0,0 +1,81 @@ +package com.tangem.lib.crypto + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import java.math.BigInteger +import java.math.RoundingMode + +/** + * !!!IMPORTANT!!! + * Methods for tuning transaction fee of different blockchains + * + * Temporary solution for domain specific logic for Blockchain. + * Instead of creating repositories and unnecessary and overkill use cases + */ +object BlockchainFeeUtils { + + private val HUNDRED_PERCENT = BigInteger("100") + + /** + * We need to increase gasLimit for Ethereum fees for 2 cases + * + * DEX: for dex calculated gasLimit for given data might be changed when transaction processing + * for that case dex providers recommend to increase gasLimit for few percents to ensure transaction completes + * + * CEX: for that case we calculate fee for random generated address and gasLimit might be different for it + * and result address to send. That's why we should increase gasLimit a little + * + */ + fun TransactionFee.patchTransactionFeeForSwap(increaseBy: Int): TransactionFee { + return when (this) { + is TransactionFee.Choosable -> { + this.copy( + minimum = this.minimum.increaseEthGasLimitInNeeded(increaseBy), + normal = this.normal.increaseEthGasLimitInNeeded(increaseBy), + priority = this.priority.increaseEthGasLimitInNeeded(increaseBy), + ) + } + is TransactionFee.Single -> this.copy(normal = this.normal.increaseEthGasLimitInNeeded(increaseBy)) + } + } + + private fun Fee.increaseEthGasLimitInNeeded(increaseBy: Int): Fee { + return when (this) { + is Fee.Ethereum.EIP1559, + is Fee.Ethereum.Legacy, + -> this.increaseGasLimitBy(increaseBy) + is Fee.Alephium, + is Fee.Aptos, + is Fee.Bitcoin, + is Fee.CardanoToken, + is Fee.Common, + is Fee.Filecoin, + is Fee.Hedera, + is Fee.Kaspa, + is Fee.Sui, + is Fee.Tron, + is Fee.VeChain, + -> this + } + } + + /** + * Increase gasLimit for Fee.Ethereum + */ + private fun Fee.increaseGasLimitBy(percentage: Int): Fee { + if (this !is Fee.Ethereum) return this + val gasLimit = this.gasLimit + val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals) + ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) + val increasedGasLimit = gasLimit + .multiply(percentage.toBigInteger()) + .divide(HUNDRED_PERCENT) + val increasedAmount = this.amount.copy( + value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), + ) + return when (this) { + is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + } + } +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt index 1f27687d60..513b751434 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt @@ -2,7 +2,6 @@ package com.tangem.sdk.api.di import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.operations.attestation.CardArtworksProvider -import com.tangem.operations.attestation.OnlineCardVerifier import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -15,13 +14,10 @@ internal object CardSdkModule { @Provides @Singleton - fun provideOnlineCardVerifier(): OnlineCardVerifier = OnlineCardVerifier() - - @Provides - @Singleton - fun provideCardArtworksProvider(sdkRepository: CardSdkConfigRepository): CardArtworksProvider = - CardArtworksProvider( - sdkRepository.sdk.config.isTangemAttestationProdEnv, - sdkRepository.sdk.secureStorage, + fun provideCardArtworksProvider(sdkRepository: CardSdkConfigRepository): CardArtworksProvider { + return CardArtworksProvider( + tangemApiBaseUrlProvider = { sdkRepository.sdk.config.tangemApiBaseUrl }, + secureStorage = sdkRepository.sdk.secureStorage, ) + } } \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt index 7eb52af432..95cc6b5096 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt @@ -5,9 +5,4 @@ package com.tangem.sdk.api.featuretoggles * [REDACTED_AUTHOR] */ -interface CardSdkFeatureToggles { - - val isNewAttestationEnabled: Boolean - - val isNewArtworkLoadingEnabled: Boolean -} \ No newline at end of file +interface CardSdkFeatureToggles \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt index 0a12b3ce37..56584647da 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt @@ -4,12 +4,5 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import javax.inject.Inject internal class DefaultCardSdkFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) : CardSdkFeatureToggles { - - override val isNewAttestationEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ATTESTATION_ENABLED") - - override val isNewArtworkLoadingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ARTWORK_LOADING") -} \ No newline at end of file + @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, +) : CardSdkFeatureToggles \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index faba1334a8..f8a60bd046 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,7 @@ dependencyResolutionManagement { mavenLocal { content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeGroupAndSubgroups("com.tangem.tangem-hot-sdk-kotlin") includeGroupAndSubgroups("com.tangem.vico") includeModule("com.tangem", "blstlib") includeModule("com.tangem", "blockchain") @@ -54,6 +55,15 @@ dependencyResolutionManagement { } content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") } } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/tangem-hot-sdk-kotlin") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { includeGroupAndSubgroups("com.tangem.tangem-hot-sdk-kotlin") } + } maven { // setting any repository from tangem project allows maven search all packages in the project url = uri("https://maven.pkg.github.com/tangem/blst-android") @@ -146,8 +156,6 @@ include(":core:navigation") include(":core:res") include(":core:ui") include(":core:utils") -include(":core:deep-links") -include(":core:deep-links:global") include(":core:decompose") include(":core:pagination") include(":core:error")