diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1514e89d5..53abf2183b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,6 +152,7 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) @@ -304,6 +305,8 @@ dependencies { implementation(projects.features.tokenRecieve.impl) implementation(projects.features.yieldSupply.api) implementation(projects.features.yieldSupply.impl) + implementation(projects.features.approval.api) + implementation(projects.features.approval.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index a7aa7666e3..08985168ae 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -146,11 +146,8 @@ abstract class BaseTestCase : TestCase( return ApplicationInjectionExecutionRule( toggleStates = mapOf( "SWAP_REDESIGN_ENABLED" to false, - "NEW_ONRAMP_MAIN_ENABLED" to true, "HOT_WALLET_ENABLED" to true, - "YIELD_SUPPLY_FEATURE_ENABLED" to true, "ACCOUNTS_FEATURE_ENABLED" to true, - "FEED_ENABLED" to true, "GASLESS_TRANSACTIONS_ENABLED" to true, ) ) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt index 9bf7545be1..105eac0ecc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt @@ -1,9 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeVertical import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.openMainScreen @@ -22,30 +20,32 @@ class SendTest : BaseTestCase() { @DisplayName("Send: check fee notification") @Test fun checkFeeNotificationTest() { - val currencyName = "POL (ex-MATIC)" - val feeCurrencyName = "Ethereum" - val feeCurrencySymbol = "ETH" - val scenarioName = "eth_network_balance" - val scenarioState = "Empty" + val currencyName = "USDC" + val feeCurrencyName = "Solana" + val feeCurrencySymbol = "SOL" + val balanceScenarioName = "solana_balance" + val tokensScenarioName = "user_tokens_api" + val balanceState = "Empty" + val tokensState = "SolanaUSDC" setupHooks( additionalAfterSection = { - resetWireMockScenarioState(scenarioName) + resetWireMockScenarioState(balanceScenarioName) + resetWireMockScenarioState(tokensScenarioName) } ).run { - step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { - setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + step("Set WireMock scenario: '$tokensScenarioName' to state: '$tokensState'") { + setWireMockScenarioState(scenarioName = tokensScenarioName, state = tokensState) + } + step("Set WireMock scenario: '$balanceScenarioName' to state: '$balanceState'") { + setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceState) } - step("Open 'Main Screen'") { openMainScreen() } step("Synchronize addresses") { synchronizeAddresses() } - step("Swipe up") { - swipeVertical(SwipeDirection.UP) - } step("Click on token with name: $currencyName") { onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt index c545a4100b..5105441296 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt @@ -23,7 +23,7 @@ class KusamaWarningsTest : BaseTestCase() { private val tokenName = "Kusama" private val amountToLeaveLessThanDeposit = "0.300333" private val amountToLeaveGreaterThanDeposit = "0.1" - private val depositAmount = "KSM 0.000333333333" + private val depositAmount = "KSM 0.000003333" private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title) private val warningMessage = getResourceString( diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt index be40b64a78..31cf64adce 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt @@ -23,7 +23,7 @@ class PolkadotWarningsTest : BaseTestCase() { private val tokenName = "Polkadot" private val amountToLeaveLessThanDeposit = "1.299" private val amountToLeaveGreaterThanDeposit = "0.2" - private val depositAmount = "DOT 1.00" + private val depositAmount = "DOT 0.01" private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title) private val warningMessage = getResourceString( diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 97f825c6ba..ba01875acf 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -48,7 +48,6 @@ import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -58,12 +57,12 @@ import dagger.hilt.components.SingletonComponent @Suppress("TooManyFunctions") interface ApplicationEntryPoint { - fun getEnvironmentConfigStorage(): EnvironmentConfigStorage - fun getAppStateHolder(): AppStateHolder fun getIssuersConfigStorage(): IssuersConfigStorage + fun getEnvironmentConfig(): EnvironmentConfig + fun getFeatureTogglesManager(): FeatureTogglesManager fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager @@ -120,8 +119,6 @@ interface ApplicationEntryPoint { fun getOnboardingRepository(): OnboardingRepository - fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider - fun getExcludedBlockchains(): ExcludedBlockchains fun getAppLogsStore(): AppLogsStore diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index cf540d0ec0..ea612e1a2d 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -44,7 +44,6 @@ import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase -import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService @@ -161,9 +160,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase - @Inject - internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles - private val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index a22cc0dd22..7e7ecd4baa 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -36,7 +36,6 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -73,23 +72,19 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.rekotlin.Store import timber.log.Timber lateinit var store: Store val foregroundActivityObserver = ForegroundActivityObserver -internal lateinit var derivationsFinder: DerivationsFinder open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider { @@ -100,12 +95,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appStateHolder: AppStateHolder get() = entryPoint.getAppStateHolder() - private val environmentConfigStorage: EnvironmentConfigStorage - get() = entryPoint.getEnvironmentConfigStorage() - private val issuersConfigStorage: IssuersConfigStorage get() = entryPoint.getIssuersConfigStorage() + private val environmentConfig: EnvironmentConfig + get() = entryPoint.getEnvironmentConfig() + private val featureTogglesManager: FeatureTogglesManager get() = entryPoint.getFeatureTogglesManager() @@ -190,9 +185,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val onboardingRepository: OnboardingRepository get() = entryPoint.getOnboardingRepository() - private val dispatchers: CoroutineDispatcherProvider - get() = entryPoint.getCoroutineDispatcherProvider() - private val excludedBlockchains: ExcludedBlockchains get() = entryPoint.getExcludedBlockchains() @@ -312,9 +304,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. Timber.i(excludedBlockchainsManager.toString()) } - runBlocking { - initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) - } + initWithConfigDependency(environmentConfig = environmentConfig) abTestsManager.init() @@ -348,15 +338,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } - derivationsFinder = DerivationsFinder( - userTokensResponseStore = userTokensResponseStore, - dispatchers = dispatchers, - ) - appStateHolder.mainStore = store wcInitializeUseCase.init( - projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, + projectId = environmentConfig.walletConnectProjectId, ) } @@ -387,7 +372,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - environmentConfigStorage = environmentConfigStorage, onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingRepository = onboardingRepository, excludedBlockchains = excludedBlockchains, diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index b43504d06c..ae347d0807 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -6,7 +6,6 @@ import com.tangem.tap.common.redux.legacy.LegacyMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware -import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState import org.rekotlin.Middleware @@ -29,7 +28,6 @@ data class AppState( AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, LegacyMiddleware.legacyMiddleware, - TradeCryptoMiddleware.middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt new file mode 100644 index 0000000000..89c89aef25 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.data + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.offramp.repository.OfframpRepository +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.network.exchangeServices.SellService + +/** + * Default implementation of [OfframpRepository] + * + * @property sellService sell service for getting offramp URL + */ +internal class DefaultOfframpRepository( + private val sellService: SellService, +) : OfframpRepository { + + override fun getOfframpUrl( + cryptoCurrency: CryptoCurrency, + fiatCurrencyCode: String, + walletAddress: String, + ): String? { + return sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, + ) + } +} \ No newline at end of file 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 1f40496096..e5600cc208 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di import com.tangem.datasource.api.moonpay.MoonPayApi -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager @@ -69,14 +69,14 @@ internal object ActivityModule { @Provides @Singleton fun provideExchangeService( - environmentConfigStorage: EnvironmentConfigStorage, getSelectedWalletUseCase: GetSelectedWalletUseCase, moonPayApi: MoonPayApi, + environmentConfig: EnvironmentConfig, ): SellService { return MoonPayService( api = moonPayApi, - apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey }, - secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey }, + apiKey = environmentConfig.moonPayApiKey, + secretKey = environmentConfig.moonPayApiSecretKey, userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() }, ) } diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 38e66db21b..21dfcaa7f1 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler @@ -40,6 +41,7 @@ internal class TangemSdkManagerModule { appFinisher: AppFinisher, sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, analyticsExceptionHandler: AnalyticsExceptionHandler, + blockchainToDeriveFinder: BlockchainToDeriveFinder, dispatchers: CoroutineDispatcherProvider, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { @@ -56,6 +58,7 @@ internal class TangemSdkManagerModule { appFinisher = appFinisher, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, analyticsExceptionHandler = analyticsExceptionHandler, + blockchainToDeriveFinder = blockchainToDeriveFinder, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt index 3ae0f6d5c6..d6555cd643 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -1,9 +1,9 @@ package com.tangem.tap.di.domain +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.earn.usecase.* -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,15 +19,15 @@ object EarnDomainModule { } @Provides - fun provideManageEarnNetworksUseCase( + fun provideGetEarnNetworksUseCase( earnRepository: EarnRepository, + multiAccountListSupplier: MultiAccountListSupplier, userWalletsListRepository: UserWalletsListRepository, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, ): GetEarnNetworksUseCase { return GetEarnNetworksUseCase( earnRepository = earnRepository, + multiAccountListSupplier = multiAccountListSupplier, userWalletsListRepository = userWalletsListRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 34ffd4aaba..ad1ba0be09 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,9 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.domain.offramp.GetOfframpUrlUseCase +import com.tangem.domain.offramp.repository.OfframpRepository import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.* import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.tap.data.DefaultOfframpRepository +import com.tangem.tap.network.exchangeServices.SellService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -275,4 +279,16 @@ internal object OnrampDomainModule { promoRepository = promoRepository, ) } + + @Provides + @Singleton + fun provideOfframpRepository(sellService: SellService): OfframpRepository { + return DefaultOfframpRepository(sellService) + } + + @Provides + @Singleton + fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase { + return GetOfframpUrlUseCase(offrampRepository) + } } \ 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 5bb200ad04..ecba7042ef 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 @@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher @@ -176,20 +177,6 @@ internal object TokensDomainModule { return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier) } - @Provides - @Singleton - fun provideToggleTokenListGroupingUseCase( - dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListGroupingUseCase { - return ToggleTokenListGroupingUseCase(dispatchers) - } - - @Provides - @Singleton - fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { - return ToggleTokenListSortingUseCase(dispatchers) - } - @Provides @Singleton fun provideApplyTokenListSortingUseCase( @@ -378,6 +365,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { @@ -388,6 +376,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index a9c061c5f7..4db814b36c 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -53,11 +53,7 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent -import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.tasks.product.CreateProductWalletTask -import com.tangem.tap.domain.tasks.product.ResetBackupCardTask -import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask -import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.product.* import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask @@ -85,6 +81,7 @@ internal class DefaultTangemSdkManager( private val appFinisher: AppFinisher, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder, dispatchers: CoroutineDispatcherProvider, ) : TangemSdkManager { @@ -172,7 +169,7 @@ internal class DefaultTangemSdkManager( runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, - derivationsFinder = derivationsFinder, + blockchainToDeriveFinder = blockchainToDeriveFinder, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt new file mode 100644 index 0000000000..9118279751 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.tap.features.demo.DemoHelper +import javax.inject.Inject + +/** + * Finder of blockchains to derive. + * Returns only saved, default or demo blockchains without any additional logic + * (no cardano/ethereum additions or unnecessary blockchain removals). + */ +class BlockchainToDeriveFinder @Inject constructor( + private val walletAccountsFetcher: WalletAccountsFetcher, +) { + + suspend fun find(card: CardDTO): Set { + if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() + val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() + + val derivationStyle = card.derivationStyleProvider.getDerivationStyle() + + val blockchains = getBlockchains(userWalletId).ifEmpty { + if (DemoHelper.isDemoCardId(card.cardId)) { + getDemoBlockchains(derivationStyle, card.cardId) + } else { + getDefaultBlockchains(derivationStyle) + } + } + + return blockchains + } + + private suspend fun getBlockchains(userWalletId: UserWalletId): Set { + return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() + .flatMap { accountDTO -> + accountDTO.tokens.orEmpty() + .filter { it.contractAddress == null } + } + .mapNotNull { coin -> + val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null + val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + .toSet() + } + + private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set { + return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set { + val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) + return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun Set.mapToBlockchainsWithDerivations( + derivationStyle: DerivationStyle?, + ): Set { + return mapNotNullTo(hashSetOf()) { blockchain -> + val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null + BlockchainToDerive(blockchain, derivationPath) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt deleted file mode 100644 index eb99c14c88..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.tangem.tap.domain.tasks.product - -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.wallets.derivations.DerivationStyleProvider -import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -internal data class BlockchainToDerive( - val blockchain: Blockchain, - val derivationPath: DerivationPath?, -) - -// FIXME: May be move to DI, currently unnecessary -internal class DerivationsFinder( - private val userTokensResponseStore: UserTokensResponseStore, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun findBlockchainsToDerive( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): Set { - if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() - val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() - val derivationStyle = derivationStyleProvider.getDerivationStyle() - - val blockchains = withContext(dispatchers.io) { - getBlockchains(userWalletId) - }.ifEmpty { - if (DemoHelper.isDemoCardId(card.cardId)) { - getDemoBlockchains(derivationStyle, card.cardId) - } else { - getDefaultBlockchains(derivationStyle) - } - } - - // we should generate second key for cardano - // because cardano address generation for wallet2 requires keys from 2 derivations - // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ - blockchains.addSecondCardanoDerivationIfPresent() - - if (card.settings.isHDWalletAllowed) { - blockchains.addEthereumBlockchains(derivationStyle) - } - - // pay attention to this - if (!card.hasOldStyleDerivation) { - blockchains.removeUnnecessaryBlockchains() - } - - return blockchains - } - - private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens - ?: return hashSetOf() - - return responseTokens.asSequence() - .filter { it.contractAddress == null } - .mapNotNull { coin -> - val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null - val derivationPath = coin.derivationPath?.let(::DerivationPath) - - BlockchainToDerive(blockchain, derivationPath) - } - .toMutableSet() - } - - // TODO: Move to user wallet config - private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet { - return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) - } - - // TODO: Move to user wallet config - private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet { - val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) - - return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) - } -} - -private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) { - val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet) - .mapToBlockchainsWithDerivations(derivationStyle) - - addAll(ethereumBlockchains) -} - -private fun MutableSet.removeUnnecessaryBlockchains() { - val unnecessaryBlockchains = listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ) - - removeAll { it.blockchain in unnecessaryBlockchains } -} - -private fun MutableSet.addSecondCardanoDerivationIfPresent() { - val cardanoDerivation = this - .firstOrNull { it.blockchain == Blockchain.Cardano } - ?.derivationPath - ?: return - - val secondCardanoBlockchain = BlockchainToDerive( - blockchain = Blockchain.Cardano, - derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation), - ) - - add(secondCardanoBlockchain) -} - -private fun Set.mapToBlockchainsWithDerivations( - derivationStyle: DerivationStyle?, -): MutableSet { - return mapTo(hashSetOf()) { blockchain -> - BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 4c334fd027..eb14c89bb4 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.data.wallets.derivations.MissedDerivationsFinder import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.card.common.TwinsHelper -import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities -import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX @@ -45,11 +43,10 @@ import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlin.collections.set internal class ScanProductTask( private val card: Card?, - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, @@ -81,7 +78,7 @@ internal class ScanProductTask( readVisaCard( session = session, cardDto = cardDto, - scanWalletProcessor = ScanWalletProcessor(derivationsFinder), + scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder), callback = callback, ) return @@ -89,7 +86,7 @@ internal class ScanProductTask( val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() - else -> ScanWalletProcessor(derivationsFinder) + else -> ScanWalletProcessor(blockchainToDeriveFinder) } commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { @@ -170,7 +167,7 @@ internal class ScanProductTask( } private class ScanWalletProcessor( - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null @@ -293,7 +290,6 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit, ) { val productType = getWalletProductType(card) - val config = CardConfig.createConfig(card) scope.launch { val scanResponse = ScanResponse( card = card, @@ -301,8 +297,7 @@ private class ScanWalletProcessor( walletData = session.environment.walletData, primaryCard = primaryCard, ) - val derivations = - collectDerivations(card, config, scanResponse.derivationStyleProvider) + val derivations = collectDerivations(card, scanResponse) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback(CompletionResult.Success(scanResponse)) return@launch @@ -332,32 +327,13 @@ private class ScanWalletProcessor( private suspend fun collectDerivations( card: CardDTO, - config: CardConfig, - derivationStyleProvider: DerivationStyleProvider, + scanResponse: ScanResponse, ): Map> { - val derivations = mutableMapOf>() - val blockchains = derivationsFinder - ?.findBlockchainsToDerive(card, derivationStyleProvider) - ?: return derivations + val blockchains = blockchainToDeriveFinder + ?.find(card) + ?: return emptyMap() - blockchains.forEach { blockchain -> - val curve = config.primaryCurve(blockchain.blockchain) - val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach - if (wallet.chainCode == null) return@forEach - - val key = wallet.publicKey.toMapKey() - val path = blockchain.derivationPath - if (path != null) { - val addedDerivations = derivations[key] - if (addedDerivations != null) { - derivations[key] = addedDerivations + path - } else { - derivations[key] = listOf(path) - } - } - } - - return derivations + return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains) } } 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 444b1f4032..4e27097a9d 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 @@ -4,5 +4,9 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.tokens.TokensFeatureToggles internal class DefaultTokensFeatureToggles( - @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, -) : TokensFeatureToggles \ No newline at end of file + private val featureTogglesManager: FeatureTogglesManager, +) : TokensFeatureToggles { + + override val isMultiAddressUtxoEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 0f7ba29f04..c851c06bdf 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -28,7 +28,7 @@ class FinalizeTwinTask( is CompletionResult.Success -> ScanProductTask( card = readResult.data, - derivationsFinder = null, + blockchainToDeriveFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, shouldCheckIsAlreadyActivated = false, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index a45ca9387a..2f49d686db 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -88,7 +88,7 @@ internal class DefaultUserWalletsListRepository( .map { wallets.updateWith(it) } } .doOnSuccess { loadedWallets -> - userWallets.update { toUpdate -> + userWallets.update { _ -> val selectedUserWalletId = selectedUserWalletRepository.get() selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId } ?: loadedWallets.firstOrNull()?.also { @@ -240,7 +240,7 @@ internal class DefaultUserWalletsListRepository( } } - @Suppress("CyclomaticComplexMethod") + @Suppress("CyclomaticComplexMethod", "LongMethod") override suspend fun unlock( userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod, @@ -315,7 +315,13 @@ internal class DefaultUserWalletsListRepository( sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> - updateWallets { it?.updateWith(sensitiveInfo) } + updateWallets { wallets -> + // It is necessary to update derivations because when scanning we obtain the missing keys + wallets?.updateWith( + walletIdToSensitiveInformation = sensitiveInfo, + walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys), + ) + } trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card) } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } 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 c9184f1335..51aefb10ab 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 @@ -1,8 +1,10 @@ package com.tangem.tap.domain.userWalletList.utils +import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation @@ -72,7 +74,10 @@ internal fun List.toUserWallets(): List return this.map { it.toUserWallet() } } -internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet { +internal fun UserWallet.updateWith( + sensitiveInformation: UserWalletSensitiveInformation, + derivedKeys: Map?, +): UserWallet { return when (this) { is UserWallet.Cold -> { copy( @@ -80,6 +85,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo card = scanResponse.card.copy( wallets = requireNotNull(sensitiveInformation.wallets), ), + derivedKeys = derivedKeys ?: scanResponse.derivedKeys, // visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus, ), ) @@ -92,14 +98,20 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo internal fun List.updateWith( walletIdToSensitiveInformation: Map, + walletIdToDerivedKeys: Map>? = null, ): List { return if (walletIdToSensitiveInformation.isEmpty()) { this } else { this.map { wallet -> - walletIdToSensitiveInformation[wallet.walletId] - ?.let(wallet::updateWith) - ?: wallet + val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId] + val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId) + + if (sensitiveInformation != null) { + wallet.updateWith(sensitiveInformation, derivedKeys) + } else { + wallet + } } } } 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 797ef6c056..2b26905caf 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 @@ -221,7 +221,7 @@ internal class CardSettingsModel @Inject constructor( val card = scanResponse.card modelScope.launch { - val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true + val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true store.dispatchNavigationAction { push( route = AppRoute.ResetToFactory( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt deleted file mode 100644 index 6cbc795c53..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.tap.features.wallet.redux.middlewares - -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import org.rekotlin.Middleware - -@Deprecated("Will be removed soon") -object TradeCryptoMiddleware { - - val middleware: Middleware = { _, appState -> - { nextDispatch -> - { action -> - if (action is TradeCryptoAction) { - handle(appState, action) - } - nextDispatch(action) - } - } - } - - private fun handle(state: () -> AppState?, action: TradeCryptoAction) { - if (DemoHelper.tryHandle(state)) return - - when (action) { - is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is TradeCryptoAction.Sell -> proceedSellAction(action) - } - } - - private fun proceedSellAction(action: TradeCryptoAction.Sell) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress - ?.defaultAddress - ?.let(NetworkAddress.Address::value) - ?: return - val currency = action.cryptoCurrencyStatus.currency - - store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl( - cryptoCurrency = currency, - fiatCurrencyName = action.appCurrencyCode, - walletAddress = networkAddress, - isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, - )?.let { url -> - store.dispatchOpenUrl(url) - Analytics.send(Token.Withdraw.ScreenOpened()) - } - } - - private fun openReceiptUrl(transactionId: String) { - store.dispatchNavigationAction(AppRouter::pop) - - val sellService = store.inject(DaggerGraphState::appStateHolder).sellService - sellService?.getSellCryptoReceiptUrl(transactionId = transactionId) - ?.let(store::dispatchOpenUrl) - } -} \ No newline at end of file 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 1c102c9e0c..027441c202 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 @@ -3,7 +3,7 @@ package com.tangem.tap.network.auth import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.ApiEnvironment -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.Provider @@ -11,7 +11,7 @@ import com.tangem.utils.ProviderSuspend internal class DefaultAuthProvider( private val userWalletsListRepository: UserWalletsListRepository, - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : AuthProvider { override suspend fun getCardPublicKey(): String { @@ -47,11 +47,11 @@ internal class DefaultAuthProvider( ApiEnvironment.DEV, ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, - -> environmentConfigStorage.getConfigSync().tangemApiKeyDev + -> environmentConfig.tangemApiKeyDev ApiEnvironment.STAGE_2, ApiEnvironment.STAGE, - -> environmentConfigStorage.getConfigSync().tangemApiKeyStage - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey + -> environmentConfig.tangemApiKeyStage + ApiEnvironment.PROD -> environmentConfig.tangemApiKey } ?: error("No tangem tech api config provided") } } @@ -60,8 +60,8 @@ internal class DefaultAuthProvider( return ProviderSuspend { when (apiEnvironment.invoke()) { ApiEnvironment.DEV, - -> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey + -> environmentConfig.gaslessTxApiKeyDev + ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}") } ?: error("No gasless tx api config provided") } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt index 93b0595647..9ed1541c3e 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt @@ -1,15 +1,15 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig import com.tangem.lib.auth.P2PEthPoolAuthProvider internal class DefaultP2PEthPoolAuthProvider( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : P2PEthPoolAuthProvider { override fun getApiKey(): String { - val keys = environmentConfigStorage.getConfigSync().p2pApiKey + val keys = environmentConfig.p2pApiKey ?: error("No P2P api keys provided") return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt index ba0534ae88..079cad327a 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -1,13 +1,13 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.lib.auth.StakeKitAuthProvider internal class DefaultStakeKitAuthProvider( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : StakeKitAuthProvider { override fun getApiKey(): String { - return environmentConfigStorage.getConfigSync().stakeKitApiKey ?: error("No StakeKit api key provided") + return environmentConfig.stakeKitApiKey ?: error("No StakeKit api key provided") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index e6cba79706..6ef9e06f96 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider @@ -22,11 +22,11 @@ internal class AuthModule { @Singleton fun provideAuthProvider( userWalletsListRepository: UserWalletsListRepository, - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, ): AuthProvider { return DefaultAuthProvider( userWalletsListRepository = userWalletsListRepository, - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, ) } @@ -38,14 +38,14 @@ internal class AuthModule { @Provides @Singleton - fun provideStakeKitAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): StakeKitAuthProvider { - return DefaultStakeKitAuthProvider(environmentConfigStorage) + fun provideStakeKitAuthProvider(environmentConfig: EnvironmentConfig): StakeKitAuthProvider { + return DefaultStakeKitAuthProvider(environmentConfig) } @Provides @Singleton - fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider { - return DefaultP2PEthPoolAuthProvider(environmentConfigStorage) + fun provideP2PEthPoolAuthProvider(environmentConfig: EnvironmentConfig): P2PEthPoolAuthProvider { + return DefaultP2PEthPoolAuthProvider(environmentConfig) } @Provides diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt index 82aa59fe63..4075e7dd57 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt @@ -21,6 +21,4 @@ interface SellService { walletAddress: String, isDarkTheme: Boolean, ): String? - - fun getSellCryptoReceiptUrl(transactionId: String): String? } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 60491ab4c7..742e44ec5e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -19,7 +19,6 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency -import com.tangem.utils.Provider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import timber.log.Timber @@ -28,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec class MoonPayService( private val api: MoonPayApi, - private val apiKeyProvider: Provider, - private val secretKeyProvider: Provider, + private val apiKey: String, + private val secretKey: String, private val userWalletProvider: () -> UserWallet?, ) : SellService { @@ -47,18 +46,18 @@ class MoonPayService( _initializationStatus.value = lceLoading() performRequest { - val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) { + val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { is Result.Failure -> { - Timber.e("Failed to load user status", result.error) + Timber.e(result.error, "Failed to load user status") _initializationStatus.value = result.error.lceError() return@performRequest } is Result.Success -> result.data } - val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) { + val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { is Result.Failure -> { - Timber.e("Failed to load currencies", result.error) + Timber.e(result.error, "Failed to load currencies") _initializationStatus.value = result.error.lceError() return@performRequest } @@ -163,7 +162,7 @@ class MoonPayService( val uri = Uri.Builder() .scheme(SCHEME) .authority(URL_SELL) - .appendQueryParameter("apiKey", apiKeyProvider()) + .appendQueryParameter("apiKey", apiKey) .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase()) .appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}") @@ -177,17 +176,9 @@ class MoonPayService( return uri.build().toString() } - override fun getSellCryptoReceiptUrl(transactionId: String): String { - return Uri.Builder() - .scheme(SCHEME) - .authority(URL_SELL) - .appendPath("transaction_receipt") - .appendQueryParameter("transactionId", transactionId).build().toString() - } - private fun createSignature(data: String): String { val sha256Hmac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256") + val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256") sha256Hmac.init(secretKey) val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray()) return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) 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 133c325afc..d86379f650 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 @@ -11,7 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore @@ -63,7 +62,6 @@ data class DaggerGraphState( val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, val transactionSignerFactory: TransactionSignerFactory? = null, - val environmentConfigStorage: EnvironmentConfigStorage? = null, val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null, val onboardingRepository: OnboardingRepository? = null, val excludedBlockchains: ExcludedBlockchains? = null, 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 aa9b5efd77..631b5f6ba3 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 @@ -18,7 +18,6 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryRoute -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.* import com.tangem.features.kyc.KycComponent @@ -26,7 +25,6 @@ import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource -import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.tokenlist.MarketsTokenListComponent import com.tangem.features.nft.component.NFTComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent @@ -67,7 +65,6 @@ internal class ChildFactory @Inject constructor( private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, - private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen, private val onrampComponentFactory: OnrampComponent.Factory, private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory, @@ -117,7 +114,6 @@ internal class ChildFactory @Inject constructor( private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory, - private val feedFeatureToggle: FeedFeatureToggle, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -193,39 +189,21 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.MarketsTokenDetails -> { - if (feedFeatureToggle.isFeedEnabled) { - createComponentChild( - context = context, - params = FeedEntryRoute.MarketTokenDetails( - token = route.token, - appCurrency = route.appCurrency, - shouldShowPortfolio = route.shouldShowPortfolio, - analyticsParams = route.analyticsParams?.let { params -> - FeedEntryRoute.MarketTokenDetails.AnalyticsParams( - blockchain = params.blockchain, - source = params.source, - ) - }, - ), - componentFactory = feedEntryComponentFactory, - ) - } else { - createComponentChild( - context = context, - params = MarketsTokenDetailsComponent.Params( - token = route.token, - appCurrency = route.appCurrency, - shouldShowPortfolio = route.shouldShowPortfolio, - analyticsParams = route.analyticsParams?.let { params -> - MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = params.blockchain, - source = params.source, - ) - }, - ), - componentFactory = marketsTokenDetailsComponentFactory, - ) - } + createComponentChild( + context = context, + params = FeedEntryRoute.MarketTokenDetails( + token = route.token, + appCurrency = route.appCurrency, + shouldShowPortfolio = route.shouldShowPortfolio, + analyticsParams = route.analyticsParams?.let { params -> + FeedEntryRoute.MarketTokenDetails.AnalyticsParams( + blockchain = params.blockchain, + source = params.source, + ) + }, + ), + componentFactory = feedEntryComponentFactory, + ) } is AppRoute.Onramp -> { createComponentChild( diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 27c065e869..4bc37fc48e 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -7,7 +7,6 @@ import com.tangem.common.routing.DeepLinkScheme import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler @@ -53,7 +52,6 @@ internal class DeepLinkFactory @Inject constructor( private val promoDeepLink: PromoDeeplinkHandler.Factory, private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, - private val feedFeatureToggle: FeedFeatureToggle, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -127,7 +125,7 @@ internal class DeepLinkFactory @Inject constructor( onboardVisaDeepLink.create(deeplinkUri) return } - deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> { + deeplinkUri.path?.startsWith("/news") == true -> { newsDetailsDeepLink.create(coroutineScope, deeplinkUri) return } diff --git a/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt new file mode 100644 index 0000000000..16ba7e686f --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.data + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.network.exchangeServices.SellService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultOfframpRepositoryTest { + + private val sellService: SellService = mockk() + private val repository = DefaultOfframpRepository(sellService) + + private val cryptoCurrency: CryptoCurrency = mockk() + private val fiatCurrencyCode = "USD" + private val walletAddress = "0x1234567890abcdef" + + @BeforeEach + fun setUp() { + mockkObject(MutableAppThemeModeHolder) + } + + @AfterEach + fun tearDown() { + clearMocks(sellService) + unmockkObject(MutableAppThemeModeHolder) + } + + @Test + fun `getOfframpUrl should return url when sellService returns url with light theme`() { + // Arrange + val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light" + every { MutableAppThemeModeHolder.isDarkThemeActive } returns false + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } returns expectedUrl + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isEqualTo(expectedUrl) + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } + } + + @Test + fun `getOfframpUrl should return url when sellService returns url with dark theme`() { + // Arrange + val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark" + every { MutableAppThemeModeHolder.isDarkThemeActive } returns true + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = true, + ) + } returns expectedUrl + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isEqualTo(expectedUrl) + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = true, + ) + } + } + + @Test + fun `getOfframpUrl should return null when sellService returns null`() { + // Arrange + every { MutableAppThemeModeHolder.isDarkThemeActive } returns false + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } returns null + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isNull() + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } + } +} diff --git a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt new file mode 100644 index 0000000000..5be7a5b371 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt @@ -0,0 +1,252 @@ +package com.tangem.tap.domain.tasks.product + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class BlockchainToDeriveFinderTest { + + private val walletAccountsFetcher = mockk() + private val finder = BlockchainToDeriveFinder( + walletAccountsFetcher = walletAccountsFetcher, + ) + + @AfterEach + fun tearDown() { + clearMocks(walletAccountsFetcher) + } + + @Test + fun `GIVEN card is not HD wallet THEN return empty set`() = runTest { + // Arrange + val card = mockk { + every { this@mockk.settings.isHDWalletAllowed } returns false + } + + // Act + val actual = finder.find(card) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `GIVEN card has empty wallets THEN return empty set`() = runTest { + // Arrange + val card = mockk { + every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.wallets } returns emptyList() + } + + // Act + val actual = finder.find(card) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest { + // Arrange + val card = createCardDTO() + + val response = createResponse(Blockchain.Bitcoin) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest { + // Arrange + val demoCardId = "AC01000000045754" + val card = createCardDTO(cardId = demoCardId) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.Dogecoin), + createExpected(Blockchain.Solana), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest { + // Arrange + val demoCardId = "DE00" + val card = createCardDTO(cardId = demoCardId) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.Dogecoin), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store THEN return default blockchains`() = runTest { + // Arrange + val card = createCardDTO() + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN saved cardano THEN return only cardano`() = runTest { + // Arrange + val card = createCardDTO() + + val response = createResponse(Blockchain.Cardano) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.find(card) + + // Assert + val expected = setOf( + createExpected(Blockchain.Cardano), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest { + // Arrange + val card = createCardDTO() + + val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon) + + val response = createResponse(*blockchains.toTypedArray()) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.find(card) + + // Assert + val expected = blockchains.mapTo(hashSetOf(), ::createExpected) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO { + val wallet = mockk { + every { this@mockk.publicKey } returns byteArrayOf(0) + } + + return mockk { + every { this@mockk.cardId } returns cardId + every { this@mockk.batchId } returns batchId + every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.settings.isKeysImportAllowed } returns true + every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion( + major = 6, + minor = 33, + patch = 0, + type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release, + ) + every { this@mockk.wallets } returns listOf(wallet) + } + } + + private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse { + val tokens = blockchains.map { blockchain -> + mockk { + every { this@mockk.networkId } returns blockchain.toNetworkId() + every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath + every { this@mockk.contractAddress } returns null + } + } + + val account = mockk { + every { this@mockk.tokens } returns tokens + } + + return mockk { + every { this@mockk.accounts } returns listOf(account) + } + } + + private fun createExpected( + blockchain: Blockchain, + derivationPath: DerivationPath = blockchain.getDerivationPath(), + ): BlockchainToDerive { + return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath) + } + + private fun Blockchain.getDerivationPath(): DerivationPath { + return derivationPath(DerivationStyle.V3)!! + } + + private companion object { + + // for byteArrayOf(0) + val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7") + } +} diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 19782fca91..4d40266dc8 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -5,7 +5,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler @@ -87,7 +86,6 @@ class DeepLinkFactoryTest { private val newsDeeplink = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } - private val feedFeatureToggle = mockk() private val mockedUri = mockk(relaxed = true) private val isFromOnNewIntent: Boolean = false @@ -112,7 +110,6 @@ class DeepLinkFactoryTest { promoDeepLink = promoDeepLinkFactory, onboardVisaDeepLink = onboardVisaDeepLink, newsDetailsDeepLink = newsDeeplink, - feedFeatureToggle = feedFeatureToggle, ) @OptIn(ExperimentalCoroutinesApi::class) diff --git a/build.gradle.kts b/build.gradle.kts index c8864ff496..b61f203435 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,3 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import java.util.concurrent.ConcurrentHashMap - plugins { alias(deps.plugins.kotlin.android) apply false alias(deps.plugins.kotlin.jvm) apply false @@ -33,83 +30,13 @@ interface Injected { val fs: FileSystemOperations } -data class TestStats( - val total: Long = 0, - val passed: Long = 0, - val failed: Long = 0, - val skipped: Long = 0, -) - -val testResultsByModule = ConcurrentHashMap() - // Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules val unitTest by tasks.registering { group = "verification" description = "Run unit tests for debug/googleDebug variant and all JVM modules" - - doLast { - if (testResultsByModule.isNotEmpty()) { - val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats -> - TestStats( - total = acc.total + stats.total, - passed = acc.passed + stats.passed, - failed = acc.failed + stats.failed, - skipped = acc.skipped + stats.skipped, - ) - } - - println("\n" + "=".repeat(80)) - println("TEST SUMMARY") - println("=".repeat(80)) - - testResultsByModule.toSortedMap().forEach { (module, stats) -> - println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)") - } - - println("-".repeat(80)) - println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules") - println(" Passed: ${totalStats.passed}") - println(" Failed: ${totalStats.failed}") - println(" Skipped: ${totalStats.skipped}") - println("=".repeat(80)) - } - } } -// Test Logging and testCI dependencies subprojects { - tasks.withType().configureEach { - println("Test task scheduled: $path") - - testLogging { - exceptionFormat = TestExceptionFormat.FULL - showStandardStreams = true - - afterSuite(KotlinClosure2({ desc, result -> - if (desc.parent == null) { // will match the outermost suite - testResultsByModule[path] = TestStats( - total = result.testCount, - passed = result.successfulTestCount, - failed = result.failedTestCount, - skipped = result.skippedTestCount, - ) - - val output = - "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" - val startItem = "| " - val endItem = " |" - val repeatLength = startItem.length + output.length + endItem.length - println( - "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( - repeatLength - ) - ) - } - })) - } - } - - // Register testCI dependencies // App module plugins.withId("com.android.application") { afterEvaluate { 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 a514a89100..a926dcb3ec 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 @@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class EditAccount( - val account: Account, + val account: Account.CryptoPortfolio, ) : AppRoute(path = "/edit_account/${account.accountId.value}") @Serializable data class AccountDetails( - val account: Account, + val account: Account.CryptoPortfolio, ) : AppRoute(path = "/account_details/${account.accountId.value}") @Serializable diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt index 6dc7f0d926..ab45c7159d 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt @@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine object TangemSiteUrlBuilder { + const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10" + suspend fun getUtmTags(campaign: String?): String { val langCode = Locale.getDefault().language val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty() diff --git a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt index a4a646a6ab..973f6c6895 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt @@ -73,7 +73,7 @@ object MockScanResponseFactory { CardDTO.Wallet( CardWallet( publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name - chainCode = null, + chainCode = ByteArray(32), // chainCode must not be null for HD wallets curve = curve, settings = createSettings(), totalSignedHashes = null, diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt index 943fb98735..1955e4b9a0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt @@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter( ) return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(this), + iconState = AccountIconItemStateConverter().convert(this), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content { return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 102be5a837..6cc4b2bf7e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -1,11 +1,14 @@ package com.tangem.common.ui.account +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.utils.converter.Converter -object AccountIconItemStateConverter : Converter { +class AccountIconItemStateConverter( + val size: AccountIconSize = AccountIconSize.Default, +) : Converter { override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { is Account.CryptoPortfolio -> when { @@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter CurrencyIconState.CryptoPortfolio.Icon( resId = value.icon.value.getResId(), color = value.icon.color.getUiColor(), isGrayscale = false, + size = size, ) } is Account.Payment -> TODO("[REDACTED_JIRA]") diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt deleted file mode 100644 index 89855cf6ce..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.common.ui.alerts - -import com.tangem.common.ui.alerts.models.AlertDemoModeUM -import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.utils.converter.Converter - -class TransactionErrorAlertConverter( - private val popBackStack: () -> Unit, - private val onFailedTxEmailClick: (String) -> Unit, -) : Converter { - override fun convert(value: SendTransactionError): AlertUM? { - return when (value) { - is SendTransactionError.DemoCardError -> AlertDemoModeUM( - onConfirmClick = popBackStack, - ) - is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM( - code = value.code.toString(), - cause = null, - causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), - onConfirmClick = { onFailedTxEmailClick(value.code.toString()) }, - ) - is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM( - code = value.code.toString(), - cause = value.message, - onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, - ) - is SendTransactionError.DataError -> AlertTransactionErrorUM( - code = "", - cause = value.message, - onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, - ) - is SendTransactionError.NetworkError -> AlertTransactionErrorUM( - code = value.code.orEmpty(), - cause = value.message.orEmpty(), - onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, - ) - is SendTransactionError.UnknownError -> AlertTransactionErrorUM( - code = "", - cause = value.ex?.localizedMessage, - onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, - ) - is SendTransactionError.CreateAccountUnderfunded -> AlertTransactionErrorUM( - code = "", - cause = null, - causeTextReference = resourceReference(R.string.no_account_polkadot, wrappedList(value.amount)), - onConfirmClick = popBackStack, - ) - else -> null - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt new file mode 100644 index 0000000000..c5476fa263 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt @@ -0,0 +1,83 @@ +package com.tangem.common.ui.alerts + +import com.tangem.common.ui.R +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.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.transaction.error.SendTransactionError +import javax.inject.Inject + +class TransactionErrorDialogFactory @Inject constructor() { + + fun create( + error: SendTransactionError, + popBackStack: () -> Unit, + onFailedTxEmailClick: (String) -> Unit, + ): DialogMessage? { + return when (error) { + is SendTransactionError.DemoCardError -> demoModeDialog(popBackStack) + is SendTransactionError.TangemSdkError -> transactionErrorDialog( + causeTextReference = resourceReference(error.messageRes, wrappedList(error.args)), + code = error.code.toString(), + onConfirmClick = { onFailedTxEmailClick(error.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> transactionErrorDialog( + cause = error.message, + code = error.code.toString(), + onConfirmClick = { onFailedTxEmailClick("${error.code}: ${error.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> transactionErrorDialog( + cause = error.message, + code = "", + onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> transactionErrorDialog( + cause = error.message.orEmpty(), + code = error.code.orEmpty(), + onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> transactionErrorDialog( + cause = error.ex?.localizedMessage, + code = "", + onConfirmClick = { onFailedTxEmailClick(error.ex?.localizedMessage.orEmpty()) }, + ) + is SendTransactionError.CreateAccountUnderfunded -> transactionErrorDialog( + causeTextReference = resourceReference( + R.string.no_account_polkadot, + wrappedList(error.amount), + ), + code = "", + onConfirmClick = popBackStack, + ) + else -> null + } + } + + private fun demoModeDialog(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) + + private fun transactionErrorDialog( + cause: String? = null, + causeTextReference: TextReference? = null, + code: String, + onConfirmClick: () -> Unit, + ): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.send_alert_transaction_failed_title), + message = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt deleted file mode 100644 index e63ccb0229..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference - -data class AlertDemoModeUM( - override val onConfirmClick: () -> Unit, -) : AlertUM { - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) - override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt deleted file mode 100644 index 6d9cea587a..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList - -data class AlertTransactionErrorUM( - val code: String, - val cause: String?, - val causeTextReference: TextReference? = null, - override val onConfirmClick: () -> Unit, -) : AlertUM { - override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) - override val message: TextReference = resourceReference( - id = R.string.send_alert_transaction_failed_text, - formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), - ) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt deleted file mode 100644 index 1cf955bebe..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference - -@Immutable -interface AlertUM { - val title: TextReference? - val message: TextReference - val confirmButtonText: TextReference - val onConfirmClick: (() -> Unit)? -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 4b4ca2eb11..7af3019d50 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList +@Deprecated("Use GiveApprovalComponent") @Composable fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { var isPermissionAlertShow by remember { mutableStateOf(false) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 7f8ec96750..39c4d36faa 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -1,17 +1,35 @@ package com.tangem.common.ui.notifications +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.ds.TangemPagerIndicator import com.tangem.core.ui.ds.message.TangemMessage +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf fun LazyListScope.notifications( notifications: ImmutableList, @@ -106,8 +124,107 @@ fun LazyListScope.notifications( contentColor = contentColor, modifier = modifier .padding(top = topPadding) - .animateItem(), + .animateItem(null, null, null), ) }, ) -} \ No newline at end of file +} + +/** + * Displays a list of notifications in a stacked manner using a HorizontalPager. + * If there are multiple notifications, a PagerIndicator is shown below the notifications. + * + * @param notifications List of TangemMessageUM objects to be displayed. + * @param containerColor Color to be used for the background of the notifications. + * @param modifier Optional Modifier for the notifications. + */ +fun LazyListScope.stackedNotifications( + notifications: ImmutableList?, + containerColor: Color, + modifier: Modifier = Modifier, +) { + item { + if (!notifications.isNullOrEmpty()) { + val notificationsPagerState = rememberPagerState( + pageCount = { notifications.size }, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + HorizontalPager( + state = notificationsPagerState, + modifier = Modifier + .fillMaxSize() + .animateItem(null, null, null), + ) { page -> + TangemMessage( + messageUM = notifications[page], + contentColor = containerColor, + modifier = modifier, + ) + } + if (notifications.size > 1) { + TangemPagerIndicator( + pagerState = notificationsPagerState, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun StackedNotifications_Preview( + @PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList, +) { + TangemThemePreviewRedesign { + val contentColor = TangemTheme.colors2.surface.level1 + LazyColumn( + modifier = Modifier + .background(contentColor) + .padding(16.dp), + ) { + stackedNotifications( + notifications = params, + containerColor = contentColor, + ) + } + } +} + +private class StackedNotificationsPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + ), + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + TangemMessageUM( + id = "1", + title = stringReference("Second notification"), + subtitle = stringReference("This is the second notification"), + messageEffect = TangemMessageEffect.Card, + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt index 3934f4ba5c..30c1e3b07e 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt @@ -5,8 +5,7 @@ import com.tangem.core.abtests.BuildConfig import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager import com.tangem.core.abtests.manager.impl.StubABTestsManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.utils.Provider +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,7 +23,7 @@ internal object ABTestsManagerModule { @Singleton fun provideABTestsManager( application: Application, - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, dispatchers: CoroutineDispatcherProvider, ): ABTestsManager { return if (BuildConfig.AB_TESTS_ENABLED) { @@ -32,7 +31,7 @@ internal object ABTestsManagerModule { } else { AmplitudeABTestsManager( application = application, - apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey }, + apiKey = environmentConfig.amplitudeApiKey, scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), ) } diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt index d2b6dbf52c..2307363d90 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt @@ -7,14 +7,13 @@ import com.amplitude.experiment.ExperimentConfig import com.amplitude.experiment.ExperimentUser import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.utils.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber internal class AmplitudeABTestsManager( val application: Application, - val apiKeyProvider: Provider, + val apiKey: String, val scope: CoroutineScope, ) : ABTestsManager { @@ -28,7 +27,7 @@ internal class AmplitudeABTestsManager( client = Experiment.initializeWithAmplitudeAnalytics( application = application, - apiKey = apiKeyProvider(), + apiKey = apiKey, config = ExperimentConfig .builder() .automaticFetchOnAmplitudeIdentityChange(true) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt new file mode 100644 index 0000000000..e0271457e7 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent + +/** + * Offramp (withdraw/sell) analytics events + */ +sealed class OfframpAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) { + + /** + * Withdraw screen opened event + */ + data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened") +} \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index c4ccee2282..f79453a6f5 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 @@ -7,14 +7,7 @@ "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" }, - { - "name": "STAKING_TON_ENABLED", - "version": "5.28.0" - }, - { - "name": "STAKING_CARDANO_ENABLED", - "version": "5.31.1" - }, + { "name": "STAKING_ETH_ENABLED", "version": "undefined" @@ -31,30 +24,10 @@ "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, - { - "name": "TANGEM_PAY_ENABLED", - "version": "5.31.0" - }, - { - "name": "YIELD_SUPPLY_FEATURE_ENABLED", - "version": "5.30.0" - }, - { - "name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", - "version": "5.33.0" - }, - { - "name": "NEW_ONRAMP_MAIN_ENABLED", - "version": "5.31.0" - }, { "name": "ACCOUNTS_FEATURE_ENABLED", "version": "5.33.0" }, - { - "name": "FEED_ENABLED", - "version": "5.33.0" - }, { "name": "APP_REDESIGN_ENABLED", "version": "undefined" @@ -78,5 +51,17 @@ { "name": "WALLET_REORDER_FEATURE_ENABLED", "version": "5.34" + }, + { + "name": "GASLESS_APPROVAL_ENABLED", + "version": "undefined" + }, + { + "name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED", + "version": "undefined" + }, + { + "name": "MULTI_ADDRESS_UTXO_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 8df1d5a9f9..7e80091db4 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -1,4 +1,6 @@ +import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants +import com.tangem.plugin.configuration.model.BuildType plugins { alias(deps.plugins.android.library) @@ -10,14 +12,53 @@ plugins { id("configuration") } +abstract class GenerateEnvironmentConfigTask : DefaultTask() { + + @get:InputFile + abstract val configFile: RegularFileProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val input = configFile.get().asFile + require(input.exists()) { "Config file not found: ${input.absolutePath}" } + logger.lifecycle("Generating EnvironmentConfig from ${input.name}") + EnvironmentConfigGenerator.generate(input, outputDir.get().asFile) + } +} + android { namespace = "com.tangem.datasource" + sourceSets["main"].java.srcDir(layout.buildDirectory.dir("generated/source/environment-config")) + room { schemaDirectory("$projectDir/schemas") } } +androidComponents { + onVariants { variant -> + val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug + val configFile = rootProject.file( + "app/src/main/assets/tangem-app-config/config_${buildType.environment}.json", + ) + + tasks.register( + "generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}", + ) { + this.configFile.set(configFile) + outputDir.set(layout.buildDirectory.dir("generated/source/environment-config")) + } + } +} + +tasks.named("preBuild") { + dependsOn(tasks.matching { it.name.startsWith("generateEnvironmentConfig") }) +} + tasks.withType().configureEach { useJUnitPlatform() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt index f05797c816..7fcda27661 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt @@ -1,11 +1,10 @@ package com.tangem.datasource.api.common.config -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend -import kotlinx.coroutines.flow.first internal class BlockAid( - private val configStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -21,9 +20,7 @@ internal class BlockAid( put( key = "X-API-KEY", value = ProviderSuspend { - requireNotNull( - configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey, - ) + requireNotNull(environmentConfig.blockAidApiKey) }, ) put("accept", ProviderSuspend { "application/json" }) 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 4104dae025..b9923936b3 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 @@ -1,7 +1,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.utils.RequestHeader import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.utils.ProviderSuspend @@ -11,13 +11,13 @@ import com.tangem.utils.version.AppVersionProvider /** * Express [ApiConfig] * - * @property environmentConfigStorage environment config storage + * @property environmentConfig environment config * @property expressAuthProvider express auth provider * @property appVersionProvider app version provider * @property appInfoProvider app info provider */ internal class Express( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val expressAuthProvider: ExpressAuthProvider, private val appVersionProvider: AppVersionProvider, private val appInfoProvider: AppInfoProvider, @@ -100,9 +100,9 @@ internal class Express( private fun getApiKey(isProd: Boolean): String { return if (isProd) { - environmentConfigStorage.getConfigSync().express + environmentConfig.express } else { - environmentConfigStorage.getConfigSync().devExpress + environmentConfig.devExpress } ?.apiKey ?: error("No express config provided") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt index b494cbc70c..5c20eca2cf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider internal sealed class TangemPay( + private val environmentConfig: EnvironmentConfig, private val appVersionProvider: AppVersionProvider, - private val environmentConfigStorage: EnvironmentConfigStorage, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() @@ -61,8 +61,8 @@ internal sealed class TangemPay( return when (apiEnvironment) { ApiEnvironment.MOCK, ApiEnvironment.DEV, - -> environmentConfigStorage.getConfigSync().bffStaticTokenDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken + -> environmentConfig.bffStaticTokenDev + ApiEnvironment.PROD -> environmentConfig.bffStaticToken ApiEnvironment.STAGE, ApiEnvironment.STAGE_2, ApiEnvironment.DEV_2, @@ -72,9 +72,9 @@ internal sealed class TangemPay( } class Bff( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ) : TangemPay(appVersionProvider, environmentConfigStorage) { + ) : TangemPay(environmentConfig, appVersionProvider) { override fun getBaseUrl(apiEnvironment: ApiEnvironment): String { return when (apiEnvironment) { ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/" @@ -90,9 +90,9 @@ internal sealed class TangemPay( } class Auth( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ) : TangemPay(appVersionProvider, environmentConfigStorage) { + ) : TangemPay(environmentConfig, appVersionProvider) { override fun getBaseUrl(apiEnvironment: ApiEnvironment): String { return when (apiEnvironment) { ApiEnvironment.DEV -> "https://api.dev.us.paera.com/" diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index b5e7ea38c6..ed04143824 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.utils.RequestHeader import com.tangem.utils.ProviderSuspend import com.tangem.utils.info.AppInfoProvider @@ -10,7 +10,7 @@ import com.tangem.utils.version.AppVersionProvider /** YieldSupply [ApiConfig] */ internal class YieldSupply( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val appVersionProvider: AppVersionProvider, private val authProvider: AuthProvider, private val appInfoProvider: AppInfoProvider, @@ -78,8 +78,8 @@ internal class YieldSupply( ApiEnvironment.DEV_3, ApiEnvironment.STAGE, ApiEnvironment.STAGE_2, - -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey + -> environmentConfig.yieldModuleApiKeyDev + ApiEnvironment.PROD -> environmentConfig.yieldModuleApiKey } ?: error("No tangem tech api config provided") } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index 48cc8fc4fa..e8ce00ce4f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -12,7 +12,7 @@ data class OrderResponse( @Json(name = "id") val id: String, @Json(name = "customer_id") val customerId: String?, @Json(name = "type") val type: String?, - @Json(name = "status") val status: String, + @Json(name = "status") val status: Status, @Json(name = "step") val step: String?, @Json(name = "data") val data: Data, @Json(name = "step_change_code") val stepChangeCode: Int?, @@ -29,5 +29,20 @@ data class OrderResponse( @Json(name = "payment_account_id") val paymentAccountId: String?, @Json(name = "transaction_hash") val transactionHash: String?, ) + + @JsonClass(generateAdapter = false) + enum class Status { + @Json(name = "NEW") + NEW, + + @Json(name = "PROCESSING") + PROCESSING, + + @Json(name = "COMPLETED") + COMPLETED, + + @Json(name = "CANCELED") + CANCELED, + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt index 6938d499d7..40ffd06873 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt @@ -5,10 +5,10 @@ import com.tangem.crypto.CryptoUtils 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.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig internal class Sha256SignatureVerifier( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val apiConfigsManager: ApiConfigsManager, ) : DataSignatureVerifier { @@ -24,8 +24,8 @@ internal class Sha256SignatureVerifier( private fun getPubKey(): String? { val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express) return when (expressConfig.environment) { - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().express?.signVerifierPublicKey - else -> environmentConfigStorage.getConfigSync().devExpress?.signVerifierPublicKey + ApiEnvironment.PROD -> environmentConfig.express?.signVerifierPublicKey + else -> environmentConfig.devExpress?.signVerifierPublicKey } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 20d4c632d5..e8db8ec219 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -21,13 +21,13 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideExpressConfig( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, expressAuthProvider: ExpressAuthProvider, appVersionProvider: AppVersionProvider, appInfoProvider: AppInfoProvider, ): ApiConfig { return Express( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, expressAuthProvider = expressAuthProvider, appVersionProvider = appVersionProvider, appInfoProvider = appInfoProvider, @@ -73,12 +73,12 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideYieldSupplyConfig( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, authProvider: AuthProvider, appInfoProvider: AppInfoProvider, ): ApiConfig = YieldSupply( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, authProvider = authProvider, appInfoProvider = appInfoProvider, @@ -87,21 +87,21 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideTangemPayBffConfig( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage) + ): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider) @Provides @IntoSet fun provideTangemPayAuthConfig( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage) + ): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider) @Provides @IntoSet - fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { - return BlockAid(environmentConfigStorage) + fun provideBlockAidConfig(environmentConfig: EnvironmentConfig): ApiConfig { + return BlockAid(environmentConfig) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index c1e4a4f7dc..de63c7bbcd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import dagger.Module @@ -45,6 +46,15 @@ class MoshiModule { .withSubtype(NetworkStatusDM.Verified::class.java, "amounts") .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), ) + .add( + NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) + .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") + .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") + .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") + .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") + .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") + .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"), + ) .add( PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") .withSubtype(NFTCollection.Identifier.EVM::class.java, "evm") diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt index 82d2ca69da..c04614c1f7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.crypto.Sha256SignatureVerifier -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,9 +17,9 @@ internal object SecurityModule { @Provides @Singleton fun provideDataSignatureVerifier( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, apiConfigsManager: ApiConfigsManager, ): DataSignatureVerifier { - return Sha256SignatureVerifier(environmentConfigStorage, apiConfigsManager) + return Sha256SignatureVerifier(environmentConfig, apiConfigsManager) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt index 0ed4457201..c8052af564 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt @@ -1,9 +1,8 @@ package com.tangem.datasource.di.local.config import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.converter.GeneratedEnvironmentConfigConverter import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage @@ -23,11 +22,8 @@ internal object ConfigModule { @Provides @Singleton - fun provideEnvironmentConfigStorage(assetLoader: AssetLoader): EnvironmentConfigStorage { - return DefaultEnvironmentConfigStorage( - assetLoader = assetLoader, - environmentConfigStore = RuntimeStateStore(defaultValue = EnvironmentConfig()), - ) + fun provideEnvironmentConfig(): EnvironmentConfig { + return GeneratedEnvironmentConfigConverter.convert() } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt deleted file mode 100644 index 1ad363973d..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.datasource.local.config.environment - -import com.tangem.datasource.BuildConfig -import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.config.environment.converter.EnvironmentConfigConverter -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.datasource.local.datastore.RuntimeStateStore -import kotlinx.coroutines.flow.Flow -import timber.log.Timber - -/** - * Default implementation for storing [EnvironmentConfig] - * - * @property assetLoader asset loader - * @property environmentConfigStore config store - */ -internal class DefaultEnvironmentConfigStorage( - private val assetLoader: AssetLoader, - private val environmentConfigStore: RuntimeStateStore, -) : EnvironmentConfigStorage { - - override suspend fun initialize(): EnvironmentConfig { - val environmentConfigModel = assetLoader.load(fileName = CONFIG_FILE_NAME) - ?: return environmentConfigStore.get().value - - val config = EnvironmentConfigConverter.convert(value = environmentConfigModel) - environmentConfigStore.store(value = config) - - Timber.i("Config [$CONFIG_FILE_NAME] loaded successfully") - - return config - } - - override fun getConfig(): Flow = environmentConfigStore.get() - - override fun getConfigSync(): EnvironmentConfig = environmentConfigStore.get().value - - private companion object { - const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}" - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt deleted file mode 100644 index a46edc5d9a..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.local.config.environment - -import kotlinx.coroutines.flow.Flow - -/** - * Storage for [EnvironmentConfig] - * -[REDACTED_AUTHOR] - */ -interface EnvironmentConfigStorage { - - /** Initialize and return [EnvironmentConfig] */ - suspend fun initialize(): EnvironmentConfig - - /** Get [EnvironmentConfig] as [Flow] */ - fun getConfig(): Flow - - /** Get [EnvironmentConfig] synchronously */ - fun getConfigSync(): EnvironmentConfig -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt new file mode 100644 index 0000000000..56df165e72 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt @@ -0,0 +1,181 @@ +package com.tangem.datasource.local.config.environment.converter + +import com.tangem.blockchain.common.* +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey +import com.tangem.datasource.local.config.environment.models.ExpressModel +import com.tangem.datasource.local.config.environment.models.P2PKeys + +/** + * Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig] + * + * This converter maps the auto-generated config (from JSON) to the domain model. + * The generated config has nested objects that mirror the JSON structure. + */ +internal object GeneratedEnvironmentConfigConverter { + + fun convert(): EnvironmentConfig { + return EnvironmentConfig( + moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey, + moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey, + mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId, + mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret, + blockchainSdkConfig = createBlockchainSdkConfig(), + amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey, + appsFlyerApiKey = AppsFlyer.appsFlyerDevKey, + appsAppId = AppsFlyer.appsFlyerAppID, + walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId, + express = createExpressModel( + apiKey = Express.apiKey, + signVerifierPublicKey = Express.signVerifierPublicKey, + ), + devExpress = createExpressModel( + apiKey = DevExpress.apiKey, + signVerifierPublicKey = DevExpress.signVerifierPublicKey, + ), + stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey, + p2pApiKey = createP2PKeys(), + blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey, + tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey, + tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev, + tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage, + yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey, + yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev, + bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken, + bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev, + gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev, + gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey, + ) + } + + private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? { + return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) { + ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey) + } else { + null + } + } + + private fun createP2PKeys(): P2PKeys? { + val mainnet = P2pApiKey.mainnet + val hoodi = P2pApiKey.hoodi + return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) { + P2PKeys(mainnet = mainnet, hoodi = hoodi) + } else { + null + } + } + + private fun createBlockchainSdkConfig(): BlockchainSdkConfig { + return BlockchainSdkConfig( + blockchairCredentials = BlockchairCredentials( + apiKey = GeneratedEnvironmentConfig.blockchairApiKeys, + authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken, + ), + blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(), + quickNodeSolanaCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodeApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain, + ), + quickNodeBscCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey, + subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain, + ), + quickNodePlasmaCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain, + ), + quickNodeMonadCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain, + ), + infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId, + tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey, + nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey), + getBlockCredentials = createGetBlockCredentials(), + kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl, + tonCenterCredentials = TonCenterCredentials( + mainnetApiKey = TonCenterApiKey.mainnet, + testnetApiKey = TonCenterApiKey.testnet, + ), + chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey, + chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey, + hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey, + polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey, + bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey, + bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey, + dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey, + koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey, + alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey, + moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey, + etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey, + blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey, + tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey, + ) + } + + private fun createGetBlockCredentials(): GetBlockCredentials { + return GetBlockCredentials( + xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc), + cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta), + avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc), + eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc), + etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc), + fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc), + rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc), + bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc), + polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc), + gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc), + cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc), + solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc), + ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc), + tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest), + cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest), + near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc), + aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest), + dogecoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest, + ), + litecoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest, + ), + dash = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Dash.jsonRpc, + blockBookRest = GetBlockAccessTokens.Dash.blockBookRest, + ), + bitcoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest, + ), + algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest), + zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc), + polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc), + base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc), + blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc), + filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc), + arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc), + bitcoinCash = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc, + blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest, + ), + kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc), + moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc), + optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc), + polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc), + shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc), + sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc), + telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc), + tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest), + monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest), + stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt new file mode 100644 index 0000000000..589fb4d915 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt @@ -0,0 +1,53 @@ +@file:Suppress("BooleanPropertyNaming") +package com.tangem.datasource.local.visa.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.domain.models.kyc.KycStatus +import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType +import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel +import java.math.BigDecimal + +/** + * Payment account status for storage in the local cache. + * + * @see [com.tangem.domain.pay.PaymentAccountStatus] + */ +@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) +sealed interface PaymentAccountStatusDM { + + @NameLabel("not_created") + data class NotCreated( + @Json(name = "not_created") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("kyc_status") + data class UnderReview( + @Json(name = "kyc_status") val kycStatus: KycStatus, + ) : PaymentAccountStatusDM + + @NameLabel("issuing_card") + data class IssuingCard( + @Json(name = "issuing_card") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("locked") + data class Locked( + @Json(name = "locked") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("balance") + data class Loaded( + @Json(name = "card_id") val cardId: String, + @Json(name = "last_four_digits") val lastFourDigits: String, + @Json(name = "balance") val balance: BigDecimal, + @Json(name = "currency_code") val currencyCode: String, + @Json(name = "deposit_address") val depositAddress: String?, + @Json(name = "is_pin_set") val isPinSet: Boolean, + ) : PaymentAccountStatusDM + + @NameLabel("card_issue_failed") + data class CardIssueFailed( + @Json(name = "card_issue_failed") val marker: Boolean = true, + ) : PaymentAccountStatusDM +} \ 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 index 2e0ccb145b..5650f72e16 100644 --- 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 @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config import com.google.common.truth.Truth import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend import io.mockk.clearMocks import io.mockk.every @@ -19,6 +20,7 @@ class ApiConfigTest { private val appAuthProvider = mockk() private val apiKeyProvider = mockk>() + private val environmentConfig = mockk() @BeforeEach fun setup() { @@ -47,7 +49,7 @@ class ApiConfigTest { when (it) { ApiConfig.ID.Express -> { Express( - environmentConfigStorage = mockk(), + environmentConfig = environmentConfig, expressAuthProvider = mockk(), appVersionProvider = mockk(), appInfoProvider = mockk(), @@ -55,7 +57,7 @@ class ApiConfigTest { } ApiConfig.ID.YieldSupply -> { YieldSupply( - environmentConfigStorage = mockk(), + environmentConfig = environmentConfig, appVersionProvider = mockk(), authProvider = appAuthProvider, appInfoProvider = mockk(), @@ -70,14 +72,14 @@ class ApiConfigTest { } ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk()) ApiConfig.ID.TangemPay -> TangemPay.Bff( + environmentConfig = environmentConfig, appVersionProvider = mockk(), - environmentConfigStorage = mockk() ) ApiConfig.ID.TangemPayAuth -> TangemPay.Auth( + environmentConfig = environmentConfig, appVersionProvider = mockk(), - environmentConfigStorage = mockk() ) - ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk()) + ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig) ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk()) ApiConfig.ID.News -> News( diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt deleted file mode 100644 index 530d4de06e..0000000000 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.datasource.api.common.config.managers - -import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.datasource.local.config.environment.models.ExpressModel -import kotlinx.coroutines.flow.flowOf - -/** - * Mock [EnvironmentConfigStorage] implementation for [ProdApiConfigsManagerTest] - * -[REDACTED_AUTHOR] - */ -internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { - - private val environmentConfig = EnvironmentConfig( - express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"), - devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"), - blockAidApiKey = BLOCK_AID_API_KEY, - tangemApiKey = TANGEM_API_KEY, - tangemApiKeyDev = TANGEM_API_KEY_DEV, - bffStaticToken = TANGEM_PAY_BFF_KEY, - bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV, - tangemApiKeyStage = TANGEM_API_KEY_STAGE, - yieldModuleApiKey = YIELD_MODULE_KEY, - yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV, - ) - - override suspend fun initialize() = environmentConfig - override fun getConfig() = flowOf(environmentConfig) - override fun getConfigSync() = environmentConfig - - companion object { - const val EXPRESS_API_KEY = "express_api_key" - const val EXPRESS_DEV_API_KEY = "express_dev_api_key" - const val BLOCK_AID_API_KEY = "block_aid_api_key" - const val TANGEM_API_KEY = "tangem_api_key" - const val TANGEM_API_KEY_DEV = "tangem_api_key_dev" - const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key" - const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev" - const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key" - const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage" - const val YIELD_MODULE_KEY = "yield_module_api_key" - const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev" - } -} \ 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 76572d804f..7b324258f2 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 @@ -10,10 +10,8 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.models.ExpressModel import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider @@ -39,7 +37,7 @@ import java.util.TimeZone @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class ProdApiConfigsManagerTest { - private val environmentConfigStorage = MockEnvironmentConfigStorage() + private val environmentConfig = createMockEnvironmentConfig() private val appVersionProvider = mockk() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() @@ -94,7 +92,7 @@ internal class ProdApiConfigsManagerTest { when (it) { ApiConfig.ID.Express -> { Express( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, expressAuthProvider = expressAuthProvider, appVersionProvider = appVersionProvider, appInfoProvider = appInfoProvider, @@ -102,7 +100,7 @@ internal class ProdApiConfigsManagerTest { } ApiConfig.ID.YieldSupply -> { YieldSupply( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, authProvider = appAuthProvider, appInfoProvider = appInfoProvider, @@ -117,14 +115,14 @@ internal class ProdApiConfigsManagerTest { } ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider) ApiConfig.ID.TangemPay -> TangemPay.Bff( + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, - environmentConfigStorage = environmentConfigStorage, ) ApiConfig.ID.TangemPayAuth -> TangemPay.Auth( + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, - environmentConfigStorage = environmentConfigStorage, ) - ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage) + ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig) ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider) ApiConfig.ID.News -> News( @@ -188,9 +186,9 @@ internal class ProdApiConfigsManagerTest { headers = mapOf( "api-key" to ProviderSuspend { if (environment == ApiEnvironment.PROD) { - MockEnvironmentConfigStorage.EXPRESS_API_KEY + EXPRESS_API_KEY } else { - MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY + EXPRESS_DEV_API_KEY } }, "session-id" to ProviderSuspend { EXPRESS_SESSION_ID }, @@ -237,7 +235,7 @@ internal class ProdApiConfigsManagerTest { environment = ApiEnvironment.PROD, baseUrl = "https://yield.tangem.org/", headers = mapOf( - "api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY }, + "api-key" to ProviderSuspend { YIELD_MODULE_KEY }, "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, "version" to ProviderSuspend { VERSION_NAME }, @@ -426,5 +424,48 @@ internal class ProdApiConfigsManagerTest { const val P2P_API_KEY = "p2p_api_key" const val APP_CARD_ID = "app_card_id" const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key" + + // Mock config values + const val TANGEM_API_KEY = "tangem_api_key" + const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key" + const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev" + const val BLOCK_AID_API_KEY = "block_aid_api_key" + const val EXPRESS_API_KEY = "express_api_key" + const val EXPRESS_DEV_API_KEY = "express_dev_api_key" + const val YIELD_MODULE_KEY = "yield_module_key" + + fun createMockEnvironmentConfig(): EnvironmentConfig { + return EnvironmentConfig( + moonPayApiKey = "moon_pay_api_key", + moonPayApiSecretKey = "moon_pay_secret_key", + mercuryoWidgetId = "mercuryo_widget_id", + mercuryoSecret = "mercuryo_secret", + blockchainSdkConfig = mockk(relaxed = true), + amplitudeApiKey = "amplitude_api_key", + appsFlyerApiKey = "appsflyer_api_key", + appsAppId = "apps_app_id", + walletConnectProjectId = "wallet_connect_project_id", + express = ExpressModel( + apiKey = EXPRESS_API_KEY, + signVerifierPublicKey = "express_public_key", + ), + devExpress = ExpressModel( + apiKey = EXPRESS_DEV_API_KEY, + signVerifierPublicKey = "express_dev_public_key", + ), + stakeKitApiKey = STAKE_KIT_API_KEY, + p2pApiKey = null, + blockAidApiKey = BLOCK_AID_API_KEY, + tangemApiKey = TANGEM_API_KEY, + tangemApiKeyDev = TANGEM_API_KEY, + tangemApiKeyStage = TANGEM_API_KEY, + yieldModuleApiKey = YIELD_MODULE_KEY, + yieldModuleApiKeyDev = YIELD_MODULE_KEY, + bffStaticToken = TANGEM_PAY_BFF_KEY_DEV, + bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV, + gaslessTxApiKeyDev = TANGEM_GASLESS_API_KEY, + gaslessTxApiKey = TANGEM_GASLESS_API_KEY, + ) + } } } \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index db9bc30c60..482fb4ddd7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -61,12 +61,12 @@ dependencies { api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) - implementation(deps.haze) { + api(deps.haze) { exclude(module = "activity-compose") exclude(module = "activity") exclude(module = "activity-ktx") } - implementation(deps.haze.materials) { + api(deps.haze.materials) { exclude(module = "activity-compose") exclude(module = "activity") exclude(module = "activity-ktx") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt new file mode 100644 index 0000000000..4b7090b987 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt @@ -0,0 +1,70 @@ +@file:Suppress("MagicNumber", "UnnecessaryParentheses") +package com.tangem.core.ui.components.background + +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import com.tangem.core.ui.shader.TangemShader +import com.tangem.core.ui.shader.runtime.buildEffect +import kotlin.math.round + +@Composable +fun Modifier.shaderBackground( + shader: TangemShader, + speed: Float = 1f, + fallback: () -> Brush = { + Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent)) + }, +): Modifier { + val runtimeEffect = remember(shader) { buildEffect(shader) } + var size: Size by remember { mutableStateOf(Size(-1f, -1f)) } + val speedModifier = shader.speedModifier + + val time by if (runtimeEffect.isSupported) { + var startMillis = remember(shader) { -1L } + produceState(0f, speedModifier) { + while (true) { + withInfiniteAnimationFrameMillis { frameTimeMillis -> + if (startMillis < 0) startMillis = frameTimeMillis + value = ((frameTimeMillis - startMillis) / 16.6f) / 10f + } + } + } + } else { + remember { mutableFloatStateOf(-1f) } + } + + return this then Modifier.onGloballyPositioned { + size = Size(it.size.width.toFloat(), it.size.height.toFloat()) + }.drawBehind { + runtimeEffect.update( + shader = shader, + time = (time * speed * speedModifier).round(3), + width = size.width, + height = size.height, + ) // set uniforms for the shaders + + if (runtimeEffect.isReady) { + drawRect(brush = runtimeEffect.build()) + } else { + drawRect(brush = fallback()) + } + } +} + +private fun Float.round(decimals: Int): Float { + var multiplier = 1.0f + repeat(decimals) { multiplier *= 10 } + return round(this * multiplier) / multiplier +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt new file mode 100644 index 0000000000..f4e9988a2b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt @@ -0,0 +1,169 @@ +@file:Suppress("MagicNumber") +package com.tangem.core.ui.components.background.northernlights + +import androidx.compose.runtime.Composable +import android.graphics.BlurMaskFilter +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas + +@Suppress("LongMethod") +@Composable +internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "FluidMeshGradient") + + // ── Circle 1 (left) ────────────────────────────────────────────────────── + val color1 by transition.animateColor( + initialValue = Color(0xFF3355EE), + targetValue = Color(0xFF5577FF), + animationSpec = infiniteRepeatable( + animation = tween(4_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "color1", + ) + val x1 by transition.animateFloat( + initialValue = 0.05f, + targetValue = 0.28f, + animationSpec = infiniteRepeatable( + animation = tween(5_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "x1", + ) + val y1 by transition.animateFloat( + initialValue = 0.0f, + targetValue = 0.18f, + animationSpec = infiniteRepeatable( + animation = tween(6_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "y1", + ) + + // ── Circle 2 (right) ───────────────────────────────────────────────────── + val color2 by transition.animateColor( + initialValue = Color(0xFF7733CC), + targetValue = Color(0xFF4455EE), + animationSpec = infiniteRepeatable( + animation = tween(5_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(1_500), + ), + label = "color2", + ) + val x2 by transition.animateFloat( + initialValue = 0.68f, + targetValue = 0.92f, + animationSpec = infiniteRepeatable( + animation = tween(7_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "x2", + ) + val y2 by transition.animateFloat( + initialValue = 0.02f, + targetValue = 0.20f, + animationSpec = infiniteRepeatable( + animation = tween(5_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(2_000), + ), + label = "y2", + ) + + // ── Oval (center) ──────────────────────────────────────────────────────── + val ovalColor by transition.animateColor( + initialValue = Color(0xFF5533CC), + targetValue = Color(0xFF8844EE), + animationSpec = infiniteRepeatable( + animation = tween(7_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(2_500), + ), + label = "ovalColor", + ) + // ── Circle 3 (center) ──────────────────────────────────────────────────── + val color3 by transition.animateColor( + initialValue = Color(0xFF9933BB), + targetValue = Color(0xFFBB44DD), + animationSpec = infiniteRepeatable( + animation = tween(6_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(3_000), + ), + label = "color3", + ) + val x3 by transition.animateFloat( + initialValue = 0.35f, + targetValue = 0.58f, + animationSpec = infiniteRepeatable( + animation = tween(6_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(1_000), + ), + label = "x3", + ) + val y3 by transition.animateFloat( + initialValue = 0.0f, + targetValue = 0.15f, + animationSpec = infiniteRepeatable( + animation = tween(4_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(500), + ), + label = "y3", + ) + + var blurRadiusState by remember { mutableFloatStateOf(0f) } + val circlePaint1 = remember { Paint() } + val circlePaint2 = remember { Paint() } + val circlePaint3 = remember { Paint() } + val ovalPaint = remember { Paint() } + + Canvas(modifier = modifier) { + val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f) + val circleRadius = size.width * 0.52f + + // Update maskFilter only when blur radius changes meaningfully + if (blurRadiusState != blurRadius) { + blurRadiusState = blurRadius + val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL) + circlePaint1.asFrameworkPaint().maskFilter = mf + circlePaint2.asFrameworkPaint().maskFilter = mf + circlePaint3.asFrameworkPaint().maskFilter = mf + ovalPaint.asFrameworkPaint().maskFilter = mf + } + + circlePaint1.color = color1.copy(alpha = 0.85f) + circlePaint2.color = color2.copy(alpha = 0.85f) + circlePaint3.color = color3.copy(alpha = 0.85f) + ovalPaint.color = ovalColor.copy(alpha = 0.80f) + + drawIntoCanvas { canvas -> + canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1) + canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2) + canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3) + + val halfW = size.width * 0.68f + val halfH = size.width * 0.24f + val ovalCx = size.width * 0.50f + val ovalCy = 0f + + canvas.drawOval( + Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH), + ovalPaint, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt new file mode 100644 index 0000000000..a59ce0f0e6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt @@ -0,0 +1,151 @@ +@file:Suppress("MagicNumber") + +package com.tangem.core.ui.components.background.northernlights + +import android.os.Build +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.background.shaderBackground +import com.tangem.core.ui.res.LocalPowerSavingState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader + +/** + * Animated northern lights background. + * Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode. + */ +@Composable +fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) { + val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() + if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) { + NorthernLightsBackgroundWithShader(modifier) + } else { + MovingColorfulBlubsBackground(modifier) + } +} + +@Suppress("LongMethod") +@Composable +private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2") + val backgroundColor = TangemTheme.colors2.surface.level1 + + // Each track cycles through 4 states (matching the screenshot frames): + // deep/dark → saturated+bright → light/pastel → vibrant/vivid → back + // 16 s total per track, staggered so no two tracks peak simultaneously. + + // ── Color 1 – indigo → bright blue → lavender → hot violet ────────────── + val color1 by transition.animateColor( + initialValue = Color(0xFF2A1480), + targetValue = Color(0xFF2A1480), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF2A1480) at 0 using FastOutSlowInEasing + Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing + Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing + Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + ), + label = "color1", + ) + + // ── Color 2 – dark blue → cyan-blue → sky → teal ───────────────────────── + val color2 by transition.animateColor( + initialValue = Color(0xFF1444AA), + targetValue = Color(0xFF1444AA), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF1444AA) at 0 using FastOutSlowInEasing + Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing + Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing + Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(4_000), + ), + label = "color2", + ) + + // ── Color 3 – dark purple → medium purple → rose pink → magenta ────────── + val color3 by transition.animateColor( + initialValue = Color(0xFF4422BB), + targetValue = Color(0xFF4422BB), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF4422BB) at 0 using FastOutSlowInEasing + Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing + Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing + Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(8_000), + ), + label = "color3", + ) + + // ── Color 4 – dark violet → medium violet → light pink → hot pink ──────── + val color4 by transition.animateColor( + initialValue = Color(0xFF331199), + targetValue = Color(0xFF331199), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF331199) at 0 using FastOutSlowInEasing + Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing + Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing + Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(2_000), + ), + label = "color4", + ) + + // Keep a stable shader instance so the RuntimeShader is never recreated. + // Colors are pushed each recomposition via updateColors(). + val shader = remember { + NorthernLightsMeshGradientShader( + colors = arrayOf( + Color(0xFF2A1480), + Color(0xFF1444AA), + Color(0xFF4422BB), + Color(0xFF331199), + backgroundColor, + ), + speed = 0.5f, + scale = 4f, + ) + } + val colorsArray = remember { Array(5) { Color.Unspecified } } + colorsArray[0] = color1 + colorsArray[1] = color2 + colorsArray[2] = color3 + colorsArray[3] = color4 + colorsArray[4] = backgroundColor + shader.updateColors(colorsArray) + + Box( + modifier = modifier + .background(backgroundColor) + .fillMaxSize() + .shaderBackground(shader), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt index 386f268756..19e1491cf4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview fun TangemPullToRefreshContainer( config: PullToRefreshConfig, modifier: Modifier = Modifier, + indicatorModifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val state = rememberPullToRefreshState() @@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer( modifier = modifier, indicator = { Indicator( - modifier = Modifier.align(Alignment.TopCenter), + modifier = indicatorModifier.align(Alignment.TopCenter), isRefreshing = config.isRefreshing, state = state, containerColor = TangemTheme.colors.background.tertiary, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt index 2c05dbfe7d..1d0dbc825f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt @@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle { override fun createShader(size: Size): Shader { val center = Offset(size.width / 2f, size.height / 2f) val diagonal = sqrt(size.width * size.width + size.height * size.height) - val direction = Offset(x = 1f, y = 0.5f) - val halfDist = diagonal / 2f - val baseStart = center - direction * halfDist - val baseEnd = center + direction * halfDist - val shift = direction * offset * diagonal + // Subtle diagonal angle, similar to iOS shimmer + val direction = Offset(x = 1f, y = 0.3f) + // Half-width of the blob (80% of diagonal total — wide, soft sweep) + val bandHalf = diagonal * 0.40f + + // Sweep the highlight center from left-of-element to right-of-element. + // offset 0..1 maps to a full pass including off-screen padding on both sides. + val shift = direction * ((offset - 0.5f) * diagonal * 1.5f) + val highlightCenter = center + shift + + // Full color text with a wide, gradual low-alpha dip sweeping left → right return LinearGradientShader( - colors = listOf(textColor.copy(alpha = 0.2f), textColor), - from = baseStart + shift, - to = baseEnd + shift, - colorStops = listOf(0.0f, 0.15f), - tileMode = TileMode.Mirror, + colors = listOf( + textColor, + textColor.copy(alpha = 0.75f), + textColor.copy(alpha = 0.45f), + textColor.copy(alpha = 0.3f), + textColor.copy(alpha = 0.45f), + textColor.copy(alpha = 0.75f), + textColor, + ), + from = highlightCenter - direction * bandHalf, + to = highlightCenter + direction * bandHalf, + colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f), + tileMode = TileMode.Clamp, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt new file mode 100644 index 0000000000..5528190513 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt @@ -0,0 +1,370 @@ +package com.tangem.core.ui.ds + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.roundToInt + +private const val ANIMATION_DURATION = 300 +private const val MAX_VISIBLE_DOTS = 5 +private const val MIN_HIDDEN_FOR_SMALL_DOT = 2 +private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 +private const val MIN_DISTANCE_FOR_HINT_DOT = 2 + +private val SPACING = 4.dp +private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp) +private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) +private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) +private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) + +/** + * // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation. + * + * A pager indicator that adapts to the number of pages and the current page index. + * + * For 5 or fewer pages, it shows all dots with the current page highlighted. + * For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position. + * + * @param pagerState state of the pager to observe + * @param activeIndicatorColor color for the active page indicator + * @param inactiveIndicatorColor color for the inactive page indicators + * @param modifier modifier for styling + */ +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun TangemPagerIndicator( + pagerState: PagerState, + modifier: Modifier = Modifier, + activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary, + inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary, +) { + val totalPages = pagerState.pageCount + val currentIndex = pagerState.currentPage + + if (totalPages == 0) return + + val density = LocalDensity.current + + val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) + + var displayLower by remember { mutableIntStateOf(targetLower) } + var displayUpper by remember { mutableIntStateOf(targetUpper) } + var prevTargetLower by remember { mutableIntStateOf(targetLower) } + + val slideOffset = remember { Animatable(0f) } + var isSliding by remember { mutableStateOf(false) } + var slideDirection by remember { mutableIntStateOf(0) } + val fadeProgress = remember { Animatable(0f) } + var fadeJob by remember { mutableStateOf(null) } + + LaunchedEffect(targetLower) { + if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } + val halfEdge = edgeDotSize / 2 + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayLower = prevTargetLower + displayUpper = targetUpper + slideOffset.snapTo(halfEdge) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-halfEdge) + } + + prevTargetLower = targetLower + + fadeJob = launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -halfEdge else halfEdge, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 + } + } + val visibleIndices = (displayLower until displayUpper).toList() + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.offset { + IntOffset(slideOffset.value.roundToInt(), 0) + }, + horizontalArrangement = Arrangement.spacedBy(SPACING), + verticalAlignment = Alignment.CenterVertically, + ) { + visibleIndices.forEach { index -> + val dotAlpha = when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value + slideDirection < 0 && index == displayLower -> fadeProgress.value + else -> 1f + } + + key(index) { + Dot( + index = index, + currentIndex = currentIndex, + totalPages = totalPages, + activeColor = activeIndicatorColor, + inactiveColor = inactiveIndicatorColor, + modifier = Modifier.graphicsLayer { alpha = dotAlpha }, + ) + } + } + } + } +} + +private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { + if (totalPages <= MAX_VISIBLE_DOTS) { + return 0 to totalPages + } + val lowerBound = when { + currentIndex <= 1 -> 0 + currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS + else -> currentIndex - 2 + } + val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) + return lowerBound to upperBound +} + +private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { + if (index == currentIndex) { + return CURRENT_DOT_SIZE + } + if (totalPages <= MAX_VISIBLE_DOTS) { + return NORMAL_DOT_SIZE + } + val params = DotSizeParams.create(index, currentIndex, totalPages) + return params.calculateSize() +} + +private class DotSizeParams private constructor( + val posInWindow: Int, + val currentPosInWindow: Int, + val hiddenLeft: Int, + val hiddenRight: Int, + val distanceFromCurrent: Int, +) { + private val lastPos = MAX_VISIBLE_DOTS - 1 + private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1 + + fun calculateSize(): DpSize = when { + isCentered -> getCenteredSize() + hiddenRight >= 1 -> getRightEdgeSize() + hiddenLeft >= 1 -> getLeftEdgeSize() + else -> NORMAL_DOT_SIZE + } + + private fun getCenteredSize(): DpSize = when (posInWindow) { + 0, lastPos -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + + private fun getRightEdgeSize(): DpSize { + val isLastPos = posInWindow == lastPos + val isSecondToLast = posInWindow == lastPos - 1 + val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isLastPos && isModerateDistance -> HINT_DOT_SIZE + isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + private fun getLeftEdgeSize(): DpSize { + val isFirstPos = posInWindow == 0 + val isSecondPos = posInWindow == 1 + val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isFirstPos && isModerateDistance -> HINT_DOT_SIZE + isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + companion object { + fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams { + val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex) + val posInWindow = index - windowStart + val currentPosInWindow = currentIndex - windowStart + return DotSizeParams( + posInWindow = posInWindow, + currentPosInWindow = currentPosInWindow, + hiddenLeft = windowStart, + hiddenRight = totalPages - windowEnd, + distanceFromCurrent = abs(posInWindow - currentPosInWindow), + ) + } + } +} + +@Composable +private fun Dot( + index: Int, + currentIndex: Int, + totalPages: Int, + activeColor: Color, + inactiveColor: Color, + modifier: Modifier = Modifier, +) { + val isActive = index == currentIndex + val size = getDotSize(index, currentIndex, totalPages) + + val animSpec = tween(ANIMATION_DURATION) + val colorSpec = tween(ANIMATION_DURATION) + + val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") + val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") + val animatedColor by animateColorAsState( + targetValue = if (isActive) activeColor else inactiveColor, + animationSpec = colorSpec, + label = "c$index", + ) + + val shape = RoundedCornerShape(animatedHeight / 2) + + Box( + modifier = modifier + .width(animatedWidth) + .height(animatedHeight) + .background(animatedColor, shape), + ) +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 5 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator6ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 6 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator7ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 7 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator10ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 10 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorSmallCountsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + TangemPagerIndicator(rememberPagerState(0) { 1 }) + TangemPagerIndicator(rememberPagerState(1) { 2 }) + TangemPagerIndicator(rememberPagerState(1) { 3 }) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index 7f9f95b4d3..aba734064b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -72,14 +72,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { */ @Composable fun TangemBadge( - text: TextReference, modifier: Modifier = Modifier, + text: TextReference? = null, @DrawableRes iconRes: Int? = null, size: TangemBadgeSize = X9, shape: TangemBadgeShape = TangemBadgeShape.Default, color: TangemBadgeColor = TangemBadgeColor.Gray, type: TangemBadgeType = TangemBadgeType.Solid, - iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None, onClick: (() -> Unit)? = null, ) { val iconColor = getIconColor(type = type, color = color) @@ -94,7 +94,7 @@ fun TangemBadge( .clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }), ) { AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start, + visible = iconRes != null && iconPosition != TangemBadgeIconPosition.End, modifier = Modifier.size(size = size.toContentSize()), label = "Start Icon Visibility", ) { @@ -105,13 +105,18 @@ fun TangemBadge( tint = iconColor, ) } - Text( - text = text.resolveReference(), - style = size.toTextStyle(), - maxLines = 1, - color = getTextColor(type = type, color = color), - ) - + AnimatedVisibility( + visible = text != null, + label = "Text Visibility", + ) { + val wrappedText = remember(this) { requireNotNull(text) } + Text( + text = wrappedText.resolveReference(), + style = size.toTextStyle(), + maxLines = 1, + color = getTextColor(type = type, color = color), + ) + } AnimatedVisibility( visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End, modifier = Modifier.size(size = size.toContentSize()), @@ -178,14 +183,17 @@ enum class TangemBadgeSize { X4 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp) } X6 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp) } X9 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp) } } @@ -222,6 +230,7 @@ enum class TangemBadgeSize { enum class TangemBadgeIconPosition { Start, End, + None, } /** @@ -240,6 +249,7 @@ enum class TangemBadgeColor { Blue, Red, Gray, + Green, } @ReadOnlyComposable @@ -258,6 +268,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when -> TangemTheme.colors2.markers.iconRed TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant } + TangemBadgeColor.Green -> when (type) { + TangemBadgeType.Outline, + TangemBadgeType.Tinted, + -> TangemTheme.colors2.markers.iconGreen + TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant + } } @ReadOnlyComposable @@ -276,8 +292,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when -> TangemTheme.colors2.markers.textRed TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant } + TangemBadgeColor.Green -> when (type) { + TangemBadgeType.Outline, + TangemBadgeType.Tinted, + -> TangemTheme.colors2.markers.textGreen + TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant + } } +@Suppress("CyclomaticComplexMethod") @ReadOnlyComposable @Composable private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) { @@ -286,6 +309,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen }, ) TangemBadgeType.Tinted -> background( @@ -293,6 +317,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen }, ) TangemBadgeType.Outline -> { @@ -301,6 +326,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen }, shape = shape, width = 1.dp, @@ -320,16 +346,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl .background(TangemTheme.colors2.surface.level1) .padding(8.dp), ) { - repeat(2) { yIndex -> + repeat(3) { yIndex -> Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { repeat(TangemBadgeType.entries.size) { index -> TangemBadge( - text = stringReference("Title"), + text = stringReference("Title").takeIf { yIndex < 2 }, iconRes = R.drawable.ic_information_24, type = TangemBadgeType.entries[index], color = params, shape = TangemBadgeShape.entries[yIndex % 2], - iconPosition = TangemBadgeIconPosition.entries[yIndex % 2], + iconPosition = TangemBadgeIconPosition.entries[yIndex], ) } } @@ -344,6 +370,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider Unit, +) { + val isInDarkTheme = LocalIsInDarkTheme.current + val overlayColor = remember(isInDarkTheme) { + if (isInDarkTheme) { + Color(OVERLAY_DARK) + } else { + Color.White + } + } + + Box(modifier = modifier) { + BackgroundLayer(icon = icon) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 30.dp, + spread = 5.dp, + color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f), + offset = DpOffset(0.dp, 0.dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 100.dp, + spread = (-39).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(.3f), + offset = DpOffset(0.dp, (-56).dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 40.dp, + spread = (-19).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f), + offset = DpOffset(0.dp, (-16).dp), + ), + ) + .drawWithContent { + drawRect(color = overlayColor.copy(alpha = .7f)) + drawContent() + val outline = shape.createOutline(size, layoutDirection, this) + drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx())) + }, + content = content, + ) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) { + when (icon) { + is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius) + is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius) + is TangemIconUM.Ident -> Unit + is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) { + when (state) { + is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CustomTokenIcon -> SolidColorBackground( + color = state.background, + blurRadius = blurRadius, + ) + is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius) + is CurrencyIconState.CoinIcon -> { + state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + } + is CurrencyIconState.FiatIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + is CurrencyIconState.TokenIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + SolidColorBackground( + color = state.fallbackBackground, + blurRadius = blurRadius, + ) + } + CurrencyIconState.Loading -> Unit + CurrencyIconState.Locked -> Unit + } +} + +@Composable +private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) { + val context = LocalContext.current + + val imageRequest = remember(imageUrl) { + if (imageUrl.isNullOrBlank()) { + null + } else { + ImageRequest.Builder(context) + .data(imageUrl) + .crossfade(true) + .build() + } + } + + if (imageRequest != null) { + AsyncImage( + model = imageRequest, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) + } +} + +@Composable +private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) { + Image( + painter = painterResource(res), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +@Composable +private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = color) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +private const val SCALE_FACTOR = 1.5f +private const val INNER_SHADOW_COLOR_START = 0x00000000 +private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF + +private const val BORDER_COLOR = 0xFFF0F0F0 +private const val OVERLAY_DARK = 0xFF141414 + +// region Previews + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun OpportunitiesBGPreview() { + TangemThemePreview { + OpportunitiesBG( + modifier = Modifier.size(400.dp), + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_solana_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + content = {}, + ) + } +} + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt index b1a24c2067..d917b39b7f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt @@ -16,8 +16,8 @@ import kotlin.math.max /** * A custom layout composable that arranges its children in a row with specific layout IDs. */ -internal enum class TangemRowLayoutId { - HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP +enum class TangemRowLayoutId { + HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM } /** @@ -29,7 +29,7 @@ internal enum class TangemRowLayoutId { */ @Suppress("LongMethod") @Composable -internal fun TangemRowContainer( +fun TangemRowContainer( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3), content: @Composable () -> Unit, @@ -37,6 +37,7 @@ internal fun TangemRowContainer( val density = LocalDensity.current val localDirection = LocalLayoutDirection.current val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() } + val extraContentPadding = with(density) { TangemTheme.dimens2.x2.roundToPx() } val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() } val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() } val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() } @@ -110,6 +111,10 @@ internal fun TangemRowContainer( layoutId = TangemRowLayoutId.EXTRA_TOP, constraints = constraints, ) + val extraBottomPlaceable = measurables.measure( + layoutId = TangemRowLayoutId.EXTRA_BOTTOM, + constraints = constraints, + ) val mainLayoutHeight = maxOf( headPlaceable.heightOrZero(), @@ -124,7 +129,13 @@ internal fun TangemRowContainer( contentTopPadding } - val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding + val mainContentBottomPadding = if (extraBottomPlaceable != null) { + extraBottomPlaceable.heightOrZero() + contentBottomPadding + } else { + contentBottomPadding + } + + val layoutHeight = mainLayoutHeight + mainContentTopPadding + mainContentBottomPadding layout(width = constraints.maxWidth, height = layoutHeight) { extraTopPlaceable?.placeRelative(x = 0, y = 0) @@ -174,6 +185,11 @@ internal fun TangemRowContainer( x = layoutWidth - tailPlaceable.width + contentEndPadding, y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2), ) + + extraBottomPlaceable?.placeRelative( + x = 0, + y = mainContentTopPadding + mainLayoutHeight + extraContentPadding, + ) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt index a0b50fe199..da31f1d96a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt @@ -52,15 +52,6 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_ICON), ) - TokenRowPromoBanner( - promoBannerUM = tokenRowUM.promoBannerUM, - modifier = Modifier - .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP) - .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) - .padding(horizontal = TangemTheme.dimens2.x3) - .fillMaxWidth(), - ) - TokenRowTitle( titleUM = tokenRowUM.titleUM, modifier = Modifier @@ -77,17 +68,21 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), ) - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = tokenRowUM.topEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.bodySemibold16, + textColor = TangemTheme.colors2.text.neutral.primary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_TOP) .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), ) - TokenRowEndBottomContent( + TokenRowEndContent( endContentUM = tokenRowUM.bottomEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.captionSemibold12, + textColor = TangemTheme.colors2.text.neutral.secondary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), @@ -100,6 +95,15 @@ fun TangemTokenRow( .layoutId(layoutId = TangemRowLayoutId.TAIL) .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), ) + + TokenRowPromoBanner( + promoBannerUM = tokenRowUM.promoBannerUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.EXTRA_BOTTOM) + .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .padding(start = TangemTheme.dimens2.x10, bottom = TangemTheme.dimens2.x2) + .fillMaxWidth(), + ) }, modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), ) @@ -159,17 +163,21 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), ) - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = tokenRowUM.topEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.bodySemibold16, + textColor = TangemTheme.colors2.text.neutral.primary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_TOP) .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), ) - TokenRowEndBottomContent( + TokenRowEndContent( endContentUM = tokenRowUM.bottomEndContentUM, isBalanceHidden = isBalanceHidden, + textStyle = TangemTheme.typography2.captionSemibold12, + textColor = TangemTheme.colors2.text.neutral.secondary, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt index 98027ff5dd..b95af763cd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt @@ -133,7 +133,8 @@ sealed class TangemTokenRowUM : TangemRowUM { val text: TextReference, val isAvailable: Boolean = true, val isFlickering: Boolean = false, - val icons: ImmutableList = persistentListOf(), + val startIcons: ImmutableList = persistentListOf(), + val endIcons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt index ce68a8771d..a18f605cca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt @@ -142,7 +142,7 @@ internal object TangemTokenRowPreviewData { ) }), ), - icons = persistentListOf( + startIcons = persistentListOf( TangemIconUM.Icon(R.drawable.ic_staking_mini_10), TangemIconUM.Icon(R.drawable.ic_attention_12), TangemIconUM.Icon(R.drawable.ic_error_sync_24), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt deleted file mode 100644 index 376e670bda..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.core.ui.ds.row.token.internal - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.width -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.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.marketprice.PriceChangeState -import com.tangem.core.ui.components.text.applyBladeBrush -import com.tangem.core.ui.ds.row.token.TangemTokenRowUM -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreviewRedesign - -@Composable -internal fun TokenRowEndBottomContent( - endContentUM: TangemTokenRowUM.EndContentUM, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - when (endContentUM) { - is TangemTokenRowUM.EndContentUM.Content -> Content( - modifier = modifier, - endContentUM = endContentUM, - isBalanceHidden = isBalanceHidden, - ) - TangemTokenRowUM.EndContentUM.Empty -> Unit - TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( - style = TangemTheme.typography2.captionSemibold12, - modifier = modifier.width(TangemTheme.dimens2.x10), - radius = TangemTheme.dimens2.x25, - ) - } -} - -@Composable -private fun Content( - endContentUM: TangemTokenRowUM.EndContentUM.Content, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( - isEnabled = endContentUM.isFlickering, - textColor = if (endContentUM.isAvailable) { - TangemTheme.colors2.text.neutral.secondary - } else { - TangemTheme.colors2.text.status.disabled - }, - ), - ) - - when (val priceChangeUM = endContentUM.priceChangeUM) { - is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = priceChangeUM, - isFlickering = endContentUM.isFlickering, - isAvailable = endContentUM.isAvailable, - ) - PriceChangeState.Unknown -> Unit - } - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun TokenRowEndBottomContent_Preview( - @PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, -) { - TangemThemePreviewRedesign { - TokenRowEndBottomContent( - endContentUM = params, - isBalanceHidden = false, - ) - } -} - -private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - TangemTokenRowPreviewData.bottomEndContentUM, - ) -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt similarity index 64% rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt index 131497424f..934b33aa8c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt @@ -8,15 +8,18 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.orMaskWithStars @@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable -internal fun TokenRowEndTopContent( +internal fun TokenRowEndContent( endContentUM: TangemTokenRowUM.EndContentUM, isBalanceHidden: Boolean, + textStyle: TextStyle, + textColor: Color, modifier: Modifier = Modifier, ) { when (endContentUM) { @@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent( modifier = modifier, endContentUM = endContentUM, isBalanceHidden = isBalanceHidden, + textStyle = textStyle, + textColor = textColor, ) TangemTokenRowUM.EndContentUM.Empty -> Unit TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( - style = TangemTheme.typography2.bodySemibold16, - modifier = modifier.width(TangemTheme.dimens2.x18), + style = textStyle, + modifier = modifier.width(TangemTheme.dimens2.x10), radius = TangemTheme.dimens2.x25, ) } @@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent( @Composable private fun Content( endContentUM: TangemTokenRowUM.EndContentUM.Content, + textStyle: TextStyle, + textColor: Color, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { @@ -56,14 +65,14 @@ private fun Content( verticalAlignment = Alignment.CenterVertically, ) { AnimatedVisibility( - visible = endContentUM.icons.isNotEmpty(), + visible = endContentUM.startIcons.isNotEmpty(), ) { Row( modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { - endContentUM.icons.fastForEach { icon -> + endContentUM.startIcons.fastForEach { icon -> Icon( modifier = Modifier.size(TangemTheme.dimens2.x3), painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), @@ -75,11 +84,11 @@ private fun Content( } Text( - modifier = Modifier, text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + color = textColor, + style = textStyle.applyBladeBrush( isEnabled = endContentUM.isFlickering, textColor = if (endContentUM.isAvailable) { TangemTheme.colors2.text.neutral.primary @@ -88,6 +97,34 @@ private fun Content( }, ), ) + + AnimatedVisibility( + visible = endContentUM.endIcons.isNotEmpty(), + ) { + Row( + modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + endContentUM.endIcons.fastForEach { icon -> + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x3), + painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), + tint = icon.tintReference(), + contentDescription = null, + ) + } + } + } + + when (val priceChangeUM = endContentUM.priceChangeUM) { + is PriceChangeState.Content -> TokenRowPriceChangeContent( + priceChangeState = priceChangeUM, + isFlickering = endContentUM.isFlickering, + isAvailable = endContentUM.isAvailable, + ) + PriceChangeState.Unknown -> Unit + } } } @@ -95,13 +132,15 @@ private fun Content( @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun TokenRowEndTopContent_Preview( +private fun TokenRowEndContent_Preview( @PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, ) { TangemThemePreviewRedesign { - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = params, isBalanceHidden = false, + textColor = TangemTheme.colors2.text.neutral.primary, + textStyle = TangemTheme.typography2.captionSemibold12, ) } } @@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview( private class TokenRowEndContentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - TangemTokenRowPreviewData.topEndContentUM, + TangemTokenRowPreviewData.bottomEndContentUM, ) } // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt index 93845dea18..6ab252c5b8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt @@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -19,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.ds.badge.* import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -40,55 +38,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C LaunchedEffect(promoBannerUM) { promoBannerUM.onPromoShown() } - val bgColor = TangemTheme.colors.control.default - Column(modifier = modifier) { + val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen + Column( + modifier = modifier, + ) { + Icon( + painter = painterResource(id = R.drawable.shape_triangular), + contentDescription = null, + tint = bgColor, + modifier = Modifier.padding(start = TangemTheme.dimens2.x5), + ) Row( modifier = Modifier .background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4)) .clickable(onClick = promoBannerUM.onPromoBannerClick) - .padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2) - .fillMaxWidth(), + .padding( + start = TangemTheme.dimens2.x2_5, + end = TangemTheme.dimens2.x0_5, + top = TangemTheme.dimens2.x0_5, + bottom = TangemTheme.dimens2.x0_5, + ), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), contentDescription = null, - tint = TangemTheme.colors.icon.accent, + tint = TangemTheme.colors2.markers.textGreen, modifier = Modifier - .padding(end = TangemTheme.dimens2.x2) - .size(TangemTheme.dimens2.x4), + .padding(vertical = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x3), ) Text( text = promoBannerUM.title.resolveReference(), - style = TangemTheme.typography2.captionSemibold12, - color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionSemibold11, + color = TangemTheme.colors2.markers.textGreen, modifier = Modifier - .weight(1f) - .padding(end = TangemTheme.dimens2.x2), + .padding(vertical = TangemTheme.dimens2.x0_5), ) - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors2.text.neutral.secondary, - modifier = Modifier - .size(TangemTheme.dimens2.x4) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = { promoBannerUM.onCloseClick() }, - ), - ) - } - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_rectangle_bottom), - contentDescription = null, - tint = bgColor, - modifier = Modifier - .size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2), + TangemBadge( + size = TangemBadgeSize.X4, + shape = TangemBadgeShape.Rounded, + color = TangemBadgeColor.Green, + type = TangemBadgeType.Tinted, + iconRes = R.drawable.ic_close_24, + iconPosition = TangemBadgeIconPosition.None, + onClick = promoBannerUM.onCloseClick, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index cae9546835..89aaa014aa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE @@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE import com.tangem.utils.extensions.isNotWhitespace import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } +open class BigDecimalCryptoFormatStyled( + val symbol: String, + val decimals: Int, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), + val shouldIgnoreSymbolPosition: Boolean = false, +) : BigDecimalFormatStyled { + + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} + // == Initializers == fun BigDecimalFormatScope.crypto( @@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto( ) } +fun BigDecimalFormatScope.cryptoStyled( + symbol: String, + decimals: Int, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = symbol, + decimals = decimals, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} + +fun BigDecimalFormatScope.cryptoStyled( + cryptoCurrency: CryptoCurrency, + spanStyleReference: SpanStyleReference, + ignoreSymbolPosition: Boolean = false, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + spanStyleReference = spanStyleReference, + shouldIgnoreSymbolPosition = ignoreSymbolPosition, + locale = locale, + ) +} + // == Formatters == fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> @@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> } } +fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = + BigDecimalFormatStyled { value -> + if (shouldIgnoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + stringReference(NON_BREAKING_SPACE + symbol), + ) + } else { + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + ) + } + } + fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> val formatter = if (value.isMoreThanThreshold()) { NumberFormat.getCurrencyInstance(locale).apply { diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 5c41281344..a1f6549b2b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -1,9 +1,11 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -15,8 +17,16 @@ open class BigDecimalFiatFormat( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } -// == Initializers == +open class BigDecimalFiatFormatStyled( + val fiatCurrencyCode: String, + val fiatCurrencySymbol: String, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormatStyled { + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} +//region == Initializers == fun BigDecimalFormatScope.fiat( fiatCurrencyCode: String, fiatCurrencySymbol: String, @@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat( ) } -// == Formatters == +fun BigDecimalFormatScope.fiat( + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalFiatFormatStyled { + return BigDecimalFiatFormatStyled( + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} +// endregion == Formatters == /** * Formats fiat amount with default precision. @@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { } } +fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + val formattingAmount = if (value.isLessThanThreshold()) { + FIAT_FORMAT_THRESHOLD + } else { + value + } + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val formattedAmount = formatter.format(formattingAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + val wholePart = formattedAmount.take(separatorIndex) + val fractionalPart = formattedAmount.drop(separatorIndex) + + combinedReference( + if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY, + stringReference(wholePart), + styledStringReference(fractionalPart, spanStyleReference), + ) +} + /** * Formats fiat amount with default precision and adds tilde sign */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt index c685db7f1d..f7a87efe6a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt @@ -1,17 +1,27 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import java.math.BigDecimal interface BigDecimalFormatScope { - companion object { val Empty = object : BigDecimalFormatScope {} } + companion object { + val Empty = object : BigDecimalFormatScope {} + } } fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope +fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope + inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String { return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference { + return BigDecimalFormatScope.Empty.block()(this) +} + inline fun BigDecimal?.format( fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, block: BigDecimalFormatScope.() -> BigDecimalFormat, @@ -20,10 +30,26 @@ inline fun BigDecimal?.format( return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal?.formatStyled( + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, + block: BigDecimalFormatScope.() -> BigDecimalFormatStyled, +): TextReference { + if (this == null) return stringReference(fallbackString) + return BigDecimalFormatScope.Empty.block()(this) +} + fun BigDecimal?.format( format: BigDecimalFormat, fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, ): String { if (this == null) return fallbackString return format(this) +} + +fun BigDecimal?.format( + format: BigDecimalFormatStyled, + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, +): TextReference { + if (this == null) return stringReference(fallbackString) + return format(this) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 04801d50d7..0906b9626d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -16,6 +16,7 @@ object TangemColorPalette { val Dark4 = Color(0xFF3B3B3B) val Dark5 = Color(0xFF303030) val Dark6 = Color(0xFF1E1E1E) + val Dark7 = Color(0xFF171717) // endregion Dark // region Dark Alpha @@ -58,20 +59,45 @@ object TangemColorPalette { val DarkGreen = Color(0xFF06311F) // endregion Green - // region Blue + // region Azure val Azure = Color(0xFF0099FF) - // endregion Blue + val Azure_50 = Color(0x800099FF) + val Azure_10 = Color(0x1A0099FF) + // endregion Azure - // region Red + // region Amaranth val Amaranth = Color(0xFFFF3333) + val Amaranth_50 = Color(0x80FF3333) + val Amaranth_20 = Color(0x33FF3333) + val Amaranth_10 = Color(0x1AFF3333) + // endregion Amaranth + + // region Flamingo val Flamingo = Color(0xFFFF5B5B) - // endregion Red + val Flamingo_50 = Color(0x80FF5B5B) + val Flamingo_20 = Color(0x33FF5B5B) + val Flamingo_10 = Color(0x1AFF5B5B) + // endregion Flamingo // region Yellow val Tangerine = Color(0xFFFFB71B) val Mustard = Color(0xFFFDDE55) // endregion Yellow + // region Emerald + val Emerald = Color(0xFF34DF12) + val Emerald_50 = Color(0x8034DF12) + val Emerald_20 = Color(0x3334DF12) + val Emerald_10 = Color(0x1A34DF12) + // endregion Emerald + + // region Eucalyptus + val Eucalyptus = Color(0xFF0C9F3D) + val Eucalyptus_50 = Color(0x800C9F3D) + val Eucalyptus_20 = Color(0x330C9F3D) + val Eucalyptus_10 = Color(0x1A0C9F3D) + // endregion Eucalyptus + // region Overlay val Overlay1 = Color(0x66000000) val Overlay2 = Color(0xB2000000) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index c3d2a23e2a..b9b0c54c51 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -448,24 +448,36 @@ class TangemColors2 internal constructor( @Stable class Markers internal constructor( - backgroundSolidGray: Color, - backgroundDisabled: Color, - backgroundSolidBlue: Color, - textGray: Color, textDisabled: Color, - iconGray: Color, iconDisabled: Color, + backgroundDisabled: Color, + textGray: Color, + iconGray: Color, borderGray: Color, - backgroundTintedBlue: Color, + backgroundSolidGray: Color, + backgroundTintedGray: Color, textBlue: Color, + iconBlue: Color, + borderTintedBlue: Color, + backgroundSolidBlue: Color, + backgroundTintedBlue: Color, + textRed: Color, + iconRed: Color, + borderTintedRed: Color, backgroundSolidRed: Color, backgroundTintedRed: Color, - iconBlue: Color, - iconRed: Color, - textRed: Color, - backgroundTintedGray: Color, - borderTintedBlue: Color, - borderTintedRed: Color, + textGreen: Color, + iconGreen: Color, + borderTintedGreen: Color, + borderSolidColor: Color, + backgroundTintedGreen: Color, + backgroundSolidGreen: Color, + textGreenAlt: Color, + iconGreenAlt: Color, + borderTintedGreenAlt: Color, + borderSolidColorAlt: Color, + backgroundTintedGreenAlt: Color, + backgroundSolidGreenAlt: Color, ) { var backgroundSolidGray by mutableStateOf(backgroundSolidGray) private set @@ -504,6 +516,32 @@ class TangemColors2 internal constructor( var borderTintedRed by mutableStateOf(borderTintedRed) private set + var textGreen by mutableStateOf(textGreen) + private set + var iconGreen by mutableStateOf(iconGreen) + private set + var borderTintedGreen by mutableStateOf(borderTintedGreen) + private set + var borderSolidColor by mutableStateOf(borderSolidColor) + private set + var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen) + private set + var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen) + private set + var textGreenAlt by mutableStateOf(textGreenAlt) + private set + var iconGreenAlt by mutableStateOf(iconGreenAlt) + private set + var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt) + private set + var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt) + private set + + var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt) + private set + var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt) + private set + fun update(other: Markers) { backgroundSolidGray = other.backgroundSolidGray backgroundDisabled = other.backgroundDisabled @@ -523,6 +561,18 @@ class TangemColors2 internal constructor( backgroundTintedGray = other.backgroundTintedGray borderTintedBlue = other.borderTintedBlue borderTintedRed = other.borderTintedRed + textGreen = other.textGreen + iconGreen = other.iconGreen + borderTintedGreen = other.borderTintedGreen + borderSolidColor = other.borderSolidColor + backgroundTintedGreen = other.backgroundTintedGreen + backgroundSolidGreen = other.backgroundSolidGreen + textGreenAlt = other.textGreenAlt + iconGreenAlt = other.iconGreenAlt + borderTintedGreenAlt = other.borderTintedGreenAlt + borderSolidColorAlt = other.borderSolidColorAlt + backgroundTintedGreenAlt = other.backgroundTintedGreenAlt + backgroundSolidGreenAlt = other.backgroundSolidGreenAlt } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index efa104abf2..93a3647d65 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -122,8 +122,8 @@ private fun lightThemeColors2(): TangemColors2 { val surface = TangemColors2.Surface( level1 = TangemColorPalette.White, level2 = TangemColorPalette.Light1V2, - level3 = TangemColorPalette.Light1V2, - level4 = TangemColorPalette.White, + level3 = TangemColorPalette.White, + level4 = TangemColorPalette.Light1V2, ) val controls = TangemColors2.Controls( backgroundChecked = TangemColorPalette.Dark6, @@ -154,16 +154,28 @@ private fun lightThemeColors2(): TangemColors2 { iconGray = TangemColorPalette.Dark1, iconDisabled = TangemColorPalette.Light2, borderGray = TangemColorPalette.Light3, - backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), + backgroundTintedBlue = TangemColorPalette.Azure_10, textBlue = text.status.accent, backgroundSolidRed = TangemColorPalette.Amaranth, - backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + backgroundTintedRed = TangemColorPalette.Amaranth_10, iconBlue = TangemColorPalette.Azure, iconRed = TangemColorPalette.Amaranth, textRed = TangemColorPalette.Amaranth, backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f), - borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), - borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + borderTintedBlue = TangemColorPalette.Azure_10, + borderTintedRed = TangemColorPalette.Amaranth_10, + textGreen = TangemColorPalette.Emerald, + iconGreen = TangemColorPalette.Emerald, + borderTintedGreen = TangemColorPalette.Emerald_10, + borderSolidColor = TangemColorPalette.Emerald_50, + backgroundTintedGreen = TangemColorPalette.Emerald_10, + backgroundSolidGreen = TangemColorPalette.Emerald, + textGreenAlt = TangemColorPalette.Eucalyptus, + iconGreenAlt = TangemColorPalette.Eucalyptus, + borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10, + borderSolidColorAlt = TangemColorPalette.Eucalyptus_50, + backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10, + backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus, ) val tabs = TangemColors2.Tabs( textPrimary = TangemColorPalette.Light2, @@ -270,8 +282,8 @@ private fun darkThemeColors2(): TangemColors2 { borderPrimary = TangemColorPalette.Light4, ) val surface = TangemColors2.Surface( - level1 = TangemColorPalette.Dark6, - level2 = TangemColorPalette.Black, + level1 = TangemColorPalette.Black, + level2 = TangemColorPalette.Dark7, level3 = TangemColorPalette.Dark6, level4 = TangemColorPalette.Dark5, ) @@ -304,7 +316,7 @@ private fun darkThemeColors2(): TangemColors2 { iconGray = TangemColorPalette.Dark2, iconDisabled = TangemColorPalette.Dark5, borderGray = TangemColorPalette.White.copy(alpha = 0.2f), - backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), + backgroundTintedBlue = TangemColorPalette.Azure_10, textBlue = text.status.accent, backgroundSolidRed = TangemColorPalette.Amaranth, backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), @@ -312,8 +324,20 @@ private fun darkThemeColors2(): TangemColors2 { iconRed = TangemColorPalette.Flamingo, textRed = TangemColorPalette.Flamingo, backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f), - borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), - borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + borderTintedBlue = TangemColorPalette.Azure_10, + borderTintedRed = TangemColorPalette.Amaranth_10, + textGreen = TangemColorPalette.Emerald, + iconGreen = TangemColorPalette.Emerald, + borderTintedGreen = TangemColorPalette.Emerald_10, + borderSolidColor = TangemColorPalette.Emerald_50, + backgroundTintedGreen = TangemColorPalette.Emerald_10, + backgroundSolidGreen = TangemColorPalette.Emerald, + textGreenAlt = TangemColorPalette.Emerald, + iconGreenAlt = TangemColorPalette.Emerald, + borderTintedGreenAlt = TangemColorPalette.Emerald_10, + borderSolidColorAlt = TangemColorPalette.Emerald_50, + backgroundTintedGreenAlt = TangemColorPalette.Emerald_10, + backgroundSolidGreenAlt = TangemColorPalette.Emerald, ) val tabs = TangemColors2.Tabs( textPrimary = TangemColorPalette.Dark4, diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index d1c1e10b32..cff7ef913b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemeRedesign /** * Interface representing a Compose screen with common theming and content composition properties. @@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView( uiDependencies = uiDependencies, overrideSystemBarColors = overrideSystemBarColors, ) { - ScreenContent(modifier = screenModifier) + TangemThemeRedesign { + ScreenContent(modifier = screenModifier) + } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt new file mode 100644 index 0000000000..600ad9af39 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.shader + +class GlossyShader : TangemShader { + override val sksl: String = + """ +// The MIT License + +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +uniform float uTime; +uniform vec3 uResolution; + +vec4 main( vec2 fragCoord ) +{ + float mr = min(uResolution.x, uResolution.y); + vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr; + + float d = -uTime * 0.5; + float a = 0.0; + for (float i = 0.0; i < 8.0; ++i) { + a += cos(i - d - a * uv.x); + d += sin(uv.y * i + a); + } + d += uTime * 0.5; + vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5); + col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5); + return vec4(col,1.0); +} + """ +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt new file mode 100644 index 0000000000..05ade74d32 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt @@ -0,0 +1,193 @@ +@file:Suppress("MagicNumber") +package com.tangem.core.ui.shader + +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.shader.runtime.RuntimeEffect + +/** + * A shader that creates a colorful, flowing "northern lights" effect. + * @param colors The colors to display. The last provided color acts like a "background" + * @param speed Adjust the speed of the movement + * @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs + * +[REDACTED_AUTHOR] + */ +class NorthernLightsMeshGradientShader( + colors: Array, + speed: Float = 1f, + scale: Float = 2f, +) : TangemShader { + + private val colorCount = colors.size + private val colorUniforms = colors.flatMap { + listOf(it.red, it.green, it.blue) + }.toTypedArray().toFloatArray() + private val ambientUniform = FloatArray(3) + + init { + recomputeAmbient() + } + + override val sksl = """ +uniform float uTime; +uniform vec3 uResolution; +uniform vec3 uAmbient; + +const int MAX_COLORS = $colorCount; +uniform vec3 uColor[MAX_COLORS]; + +// Simplex 3D Noise +// by Ian McEwan, Ashima Arts +// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83 +// +vec4 permute(vec4 x) { + return mod(((x * 34.0) + 1.0) * x, 289.0); +} +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0. + 0.0 * C + vec3 x1 = x0 - i1 + 1.0 * C.xxx; + vec3 x2 = x0 - i2 + 2.0 * C.xxx; + vec3 x3 = x0 - 1. + 3.0 * C.xxx; + + // Permutations + i = mod(i, 289.0); + vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients + // ( N*N points uniformly over a square, mapped onto an octahedron.) + float n_ = 1.0 / 7.0; // N=7 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + //Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} + +vec4 main( vec2 fragCoord ) { + float mr = min(uResolution.x, uResolution.y); + vec2 uv = (fragCoord * $scale - uResolution.xy) / mr; + + vec2 base = uv / 2; + + vec3 vColor = uColor[MAX_COLORS - 1]; + + const vec2 frequency = vec2(0.7, 0.3); + const float noiseFloor = 0.00001; + float t = uTime * 0.005; + + for(int i = 0; i < MAX_COLORS - 1; i++) { + float fi = float(i); + float flow = 5. + fi * 0.3; + float speed = 6. * $speed + fi * 0.3; + float seed = 1. + fi * 4.; + float noiseCeil = 0.6 + fi * 0.07; + + float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed))); + + vColor = mix(vColor, uColor[i], noise); + } + + vColor = max(vColor, uAmbient); + + // Elliptical falloff centred at the very top of the screen. + // Using fragCoord directly (pixels) and uResolution for screen size. + // Horizontal radius ~ 80 % of screen width → wide enough to cover corners. + // Vertical radius ~ 45 % of screen height → controls how far down the glow reaches. + vec2 topCenter = vec2(uResolution.x * 0.5, 0.0); + vec2 delta = fragCoord - topCenter; + vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65); + float normDist = length(delta / radii); + float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5); + + // Pre-multiplied alpha so the shader composites correctly over the dark background. + return vec4(vColor * alpha, alpha); +} + """ + + /** Updates the animated colors in-place without recreating the shader. */ + fun updateColors(colors: Array) { + colors.forEachIndexed { i, color -> + colorUniforms[i * 3 + 0] = color.red + colorUniforms[i * 3 + 1] = color.green + colorUniforms[i * 3 + 2] = color.blue + } + recomputeAmbient() + } + + private fun recomputeAmbient() { + val count = colorCount - 1 + var r = 0f + var g = 0f + var b = 0f + for (i in 0 until count) { + r += colorUniforms[i * 3] + g += colorUniforms[i * 3 + 1] + b += colorUniforms[i * 3 + 2] + } + val scale = 0.5f / count + ambientUniform[0] = r * scale + ambientUniform[1] = g * scale + ambientUniform[2] = b * scale + } + + override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) { + super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height) + + runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms) + runtimeEffect.setFloatUniform( + name = "uAmbient", + value1 = ambientUniform[0], + value2 = ambientUniform[1], + value3 = ambientUniform[2], + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt new file mode 100644 index 0000000000..388914a11b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.shader + +import com.tangem.core.ui.shader.runtime.RuntimeEffect + +interface TangemShader { + val speedModifier: Float + get() = 0.5f + + val sksl: String + + /** Applies the uniforms required for this shader to the effect */ + fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) { + runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height) + runtimeEffect.setFloatUniform(name = "uTime", value1 = time) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt new file mode 100644 index 0000000000..899e3c8a78 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.shader.runtime + +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +internal class FallbackRuntimeEffect : RuntimeEffect { + override val isSupported: Boolean = false + override val isReady: Boolean = false + + override fun build(): Brush { + return Brush.horizontalGradient(listOf(Color.White, Color.White)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt new file mode 100644 index 0000000000..035495aad8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt @@ -0,0 +1,35 @@ +package com.tangem.core.ui.shader.runtime + +import android.os.Build +import androidx.compose.ui.graphics.Brush +import com.tangem.core.ui.shader.TangemShader + +interface RuntimeEffect { + + val isSupported: Boolean + val isReady: Boolean + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float, value2: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, values: FloatArray) {} + + fun update(shader: TangemShader, time: Float, width: Float, height: Float) {} + + fun build(): Brush +} + +internal fun buildEffect(shader: TangemShader): RuntimeEffect { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + RuntimeShaderEffect(shader) + } else { + FallbackRuntimeEffect() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt new file mode 100644 index 0000000000..78c50e2e7c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.shader.runtime + +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ShaderBrush +import com.tangem.core.ui.shader.TangemShader + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +internal class RuntimeShaderEffect(tangemShader: TangemShader) : RuntimeEffect { + private val compositeRuntimeEffect = RuntimeShader(tangemShader.sksl) + + override val isSupported: Boolean = true + override var isReady: Boolean = false + + override fun setFloatUniform(name: String, value1: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1) + } + + override fun setFloatUniform(name: String, value1: Float, value2: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1, value2) + } + + override fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1, value2, value3) + } + + override fun setFloatUniform(name: String, values: FloatArray) { + compositeRuntimeEffect.setFloatUniform(name, values) + } + + override fun update(shader: TangemShader, time: Float, width: Float, height: Float) { + shader.applyUniforms(runtimeEffect = this, time = time, width = width, height = height) + isReady = width > 0 && height > 0 + } + + override fun build(): Brush { + return ShaderBrush(compositeRuntimeEffect) + } +} \ No newline at end of file diff --git a/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp new file mode 100644 index 0000000000..8844ed902d Binary files /dev/null and b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp differ diff --git a/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml new file mode 100644 index 0000000000..6ee9686437 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml new file mode 100644 index 0000000000..69f15ed1a6 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp new file mode 100644 index 0000000000..9d18888c54 Binary files /dev/null and b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp differ diff --git a/core/ui/src/main/res/drawable/shape_triangular.xml b/core/ui/src/main/res/drawable/shape_triangular.xml new file mode 100644 index 0000000000..c4baf11fb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/shape_triangular.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 88a6e8f021..156a72544e 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -18,7 +18,7 @@ import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -131,11 +131,11 @@ internal object OnrampDataModule { @Provides @Singleton fun provideMercuryoRepository( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, dispatchersProvider: CoroutineDispatcherProvider, ): LegacyTopUpRepository { return MercuryoTopUpRepository( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, dispatchersProvider = dispatchersProvider, ) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt index a2b80fec76..9eaee443e1 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt @@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -13,14 +12,13 @@ import kotlinx.coroutines.withContext import javax.inject.Inject internal class MercuryoTopUpRepository @Inject constructor( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val dispatchersProvider: CoroutineDispatcherProvider, ) : LegacyTopUpRepository { override suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String = withContext(dispatchersProvider.default) { val blockchain = cryptoCurrency.network.toBlockchain() - val environmentConfig = environmentConfigStorage.getConfigSync() val builder = Uri.Builder() .scheme(LegacyTopUpRepository.SCHEME) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt index 5eedc18e16..171c5c720a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt @@ -49,7 +49,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import timber.log.Timber @@ -161,10 +163,6 @@ internal class DefaultStakeKitRepository( private fun getAvailableStakeKitIntegrationsIds(): List { return StakingIntegrationID.StakeKit.entries - // load all integrations for now and filter in use cases if needed - // .filterNot { - // it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled - // } } private fun NetworkTypeDTO.extractJsonName(): String { 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 bdb663a190..b46d6ea594 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 @@ -8,7 +8,6 @@ import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability @@ -17,7 +16,6 @@ import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.lib.crypto.BlockchainUtils.isCardano import com.tangem.lib.crypto.BlockchainUtils.isSolana @@ -35,7 +33,6 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, - private val walletManagersFacade: WalletManagersFacade, ) : StakingRepository { override fun getStakingAvailability( @@ -43,7 +40,7 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, ): Flow { return channelFlow { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { send(StakingAvailability.Unavailable) return@channelFlow } @@ -79,7 +76,7 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { return StakingAvailability.Unavailable } @@ -119,31 +116,14 @@ internal class DefaultStakingRepository( } } - private suspend fun checkFeatureToggleEnabled(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean { return when (cryptoCurrency.network.id.toBlockchain()) { - Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled Blockchain.Ethereum -> { when (cryptoCurrency) { is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled is CryptoCurrency.Token -> true } } - Blockchain.Cardano -> { - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val balance = stakingBalanceStoreV2.getSyncOrNull( - userWalletId = userWalletId, - stakingId = StakingID( - integrationId = StakingIntegrationID.create(currencyId = cryptoCurrency.id)?.value - ?: return false, - address = address, - ), - ) - if ((balance as? StakingBalance.Data.StakeKit)?.balance?.items?.isNotEmpty() == true) { - return true - } else { - stakingFeatureToggles.isCardanoStakingEnabled - } - } else -> true } } 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 f1c17a770a..0aa8abf1b5 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 @@ -61,7 +61,6 @@ internal object StakingDataModule { dispatchers: CoroutineDispatcherProvider, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, - walletManagersFacade: WalletManagersFacade, ): StakingRepository { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, @@ -69,7 +68,6 @@ internal object StakingDataModule { stakingBalanceStoreV2 = stakeKitBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, - walletManagersFacade = walletManagersFacade, stakingFeatureToggles = stakingFeatureToggles, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt index e5ee6c623a..fe7e688a8f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt @@ -7,12 +7,6 @@ internal class DefaultStakingFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : StakingFeatureToggles { - override val isTonStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_TON_ENABLED") - - override val isCardanoStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("STAKING_CARDANO_ENABLED") - override val isEthStakingEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("STAKING_ETH_ENABLED") } \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 67768d9958..6443faf2f5 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -38,11 +38,6 @@ dependencies { implementation(projects.domain.common) implementation(projects.features.swap.domain) - /** Feature API - remove after removing [HotWalletFeatureToggles] */ - implementation(projects.features.hotWallet.api) - - /** Feature API - remove after removing [TangemPayFeatureToggles] */ - implementation(projects.features.tangempay.details.api) /** Project - Utils */ implementation(projects.core.utils) @@ -53,6 +48,7 @@ dependencies { implementation(projects.libs.visa) /** Libs - Other */ + implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.arrow.fx) @@ -70,6 +66,6 @@ dependencies { implementation(projects.libs.tangemSdkApi) /** DI */ - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 2d20b15a27..c3eb76035e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -104,7 +104,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( map { wallet -> async { val isCustomer = onboardingRepository - .checkCustomerWallet(wallet.walletId) + .hasTangemPayInWallet(wallet.walletId) .getOrNull() == true wallet to isCustomer } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt new file mode 100644 index 0000000000..db6fbcca4f --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.data.pay.converter + +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.converter.TwoWayConverter + +/** + * Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM]. + * + * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted + * (Loading, ExposedDevice, Unavailable, NotSynced). + * + * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. + */ +internal object PaymentAccountStatusDMConverter : + TwoWayConverter { + + override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? { + return when (value) { + is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated() + is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus) + is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard() + is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked() + is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded( + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + balance = value.balance, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + ) + is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed() + // Transient statuses are not persisted + is PaymentAccountStatus.Loading, + is PaymentAccountStatus.Error.ExposedDevice, + is PaymentAccountStatus.Error.Unavailable, + is PaymentAccountStatus.Error.NotSynced, + -> null + } + } + + override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus { + return when (value) { + is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed + is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated + is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE) + is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE) + is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview( + source = StatusSource.CACHE, + kycStatus = value.kycStatus, + ) + is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded( + source = StatusSource.CACHE, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + balance = value.balance, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + ) + null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 5868e4c0c6..565fdbe700 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -1,13 +1,28 @@ package com.tangem.data.pay.di +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager +import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher +import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* +import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusProducer +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -16,11 +31,15 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -75,7 +94,51 @@ internal interface TangemPayDataModule { @Singleton fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager + @Binds + @Singleton + fun bindPaymentAccountStatusProducerFactory( + impl: DefaultPaymentAccountStatusProducer.Factory, + ): PaymentAccountStatusProducer.Factory + + @Binds + @Singleton + fun bindPaymentAccountStatusFetcher(impl: DefaultPaymentAccountStatusFetcher): PaymentAccountStatusFetcher + companion object { + + @Provides + @Singleton + fun providePaymentAccountStatusesStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): PaymentAccountStatusesStore { + return PaymentAccountStatusesStore( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun providePaymentAccountStatusSupplier( + factory: PaymentAccountStatusProducer.Factory, + ): PaymentAccountStatusSupplier { + return object : PaymentAccountStatusSupplier( + factory = factory, + keyCreator = { "payment_account_status_${it.userWalletId.stringValue}" }, + ) {} + } + @Provides @Singleton fun provideTangemPayMainScreenCustomerInfoUseCase( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt new file mode 100644 index 0000000000..cdfbf0d9b4 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -0,0 +1,178 @@ +package com.tangem.data.pay.flow + +import arrow.core.Either +import com.tangem.data.pay.store.PaymentAccountStatusesStore +import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import timber.log.Timber +import javax.inject.Inject + +private const val TAG = "PaymentAccountStatusFetcher" + +internal class DefaultPaymentAccountStatusFetcher @Inject constructor( + private val paymentAccountStatusesStore: PaymentAccountStatusesStore, + private val onboardingRepository: OnboardingRepository, + private val customerOrderRepository: CustomerOrderRepository, + private val deviceSecurity: DeviceSecurityInfoProvider, + private val dispatchers: CoroutineDispatcherProvider, +) : PaymentAccountStatusFetcher { + + override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = + eitherOn(dispatchers.default) { + Timber.tag(TAG).i("fetch: ${params.userWalletId.stringValue}") + + if (deviceSecurity.isSecurityExposed()) { + Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + + return@eitherOn paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = PaymentAccountStatus.Error.ExposedDevice, + ) + } + + val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) + .fold( + ifLeft = { error -> + Timber.tag(TAG).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + when (error) { + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated + else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + }, + ifRight = { hasTangemPay -> + proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) + }, + ) + Timber.tag(TAG).i("invoke status ${params.userWalletId}: $status") + paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) + } + + private suspend fun proceedHasTangemPayResult( + userWalletId: UserWalletId, + hasTangemPay: Boolean, + ): PaymentAccountStatus { + Timber.tag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + return if (hasTangemPay) { + fetchTangemPayAccountStatus(userWalletId = userWalletId) + } else { + PaymentAccountStatus.NotCreated + } + } + + private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { + val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) + if (prevResult == null || prevResult is PaymentAccountStatus.Error) { + paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) + } + + return proceedWithOrderId(userWalletId = userWalletId) + } + + private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { + return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { + PaymentAccountStatus.Error.NotSynced + } else { + val orderId = onboardingRepository.getOrderId(userWalletId) + if (orderId != null) { + proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + } else { + proceedWithoutOrder(userWalletId = userWalletId) + } + } + } + + private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { + return onboardingRepository.getCustomerInfo(userWalletId).fold( + ifLeft = { error -> + Timber.tag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + error.mapToPaymentAccountStatus() + }, + ifRight = { customerInfo -> + Timber.tag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + val status = customerInfo.mapToPaymentAccountStatus() + if (status is PaymentAccountStatus.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { + // If order id wasn't saved -> start order creation and get customer info + onboardingRepository.createOrder(userWalletId) + } + status + }, + ) + } + + private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { + return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( + ifLeft = { error -> + Timber.tag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + error.mapToPaymentAccountStatus() + }, + ifRight = { orderData -> + Timber.tag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + when (orderData.status) { + // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.NEW, + OrderStatus.PROCESSING, + -> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + + OrderStatus.CANCELED -> { + // If order was cancelled -> clear previous order from local storage and start order creation + onboardingRepository.clearOrderId(userWalletId) + onboardingRepository.createOrder(userWalletId) + PaymentAccountStatus.Error.CardIssueFailed + } + OrderStatus.COMPLETED -> { + // Order was completed -> clear order id and get customer info + onboardingRepository.clearOrderId(userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + .fold( + ifLeft = { it.mapToPaymentAccountStatus() }, + ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, + ) + } + OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + }, + ) + } + + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { + val cardInfo = this.cardInfo + val productInstance = this.productInstance + return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { + PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) + } else if (cardInfo != null && productInstance != null) { + PaymentAccountStatus.Loaded( + source = StatusSource.ACTUAL, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + balance = cardInfo.balance, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + ) + } else { + PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + } + } + + private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { + return when (this) { + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated + else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt new file mode 100644 index 0000000000..cb021c1ee8 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -0,0 +1,37 @@ +package com.tangem.data.pay.flow + +import arrow.core.Option +import arrow.core.some +import com.tangem.data.pay.store.PaymentAccountStatusesStore +import com.tangem.domain.core.flow.FlowProducerTools +import com.tangem.domain.models.StatusSource +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.pay.flow.PaymentAccountStatusProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty + +internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( + @Assisted private val params: PaymentAccountStatusProducer.Params, + override val flowProducerTools: FlowProducerTools, + private val paymentAccountStatusesStore: PaymentAccountStatusesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : PaymentAccountStatusProducer { + override val fallback: Option + get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some() + + override fun produce(): Flow { + return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) + .onEmpty { emit(value = PaymentAccountStatus.NotCreated) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : PaymentAccountStatusProducer.Factory { + override fun create(params: PaymentAccountStatusProducer.Params): DefaultPaymentAccountStatusProducer + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt index f479794766..508627ca37 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.response.OrderResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus @@ -19,11 +20,11 @@ internal class DefaultCustomerOrderRepository @Inject constructor( tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) }.map { response -> val status = when (response.result?.status) { - null -> OrderStatus.UNKNOWN - OrderStatus.NEW.apiName -> OrderStatus.NEW - OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING - OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED - else -> OrderStatus.CANCELED + null -> OrderStatus.PROCESSING + OrderResponse.Result.Status.NEW -> OrderStatus.NEW + OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING + OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED + OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED } OrderData( status = status, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 2fdcfabcb6..042922e80a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.datasource.TangemPayAuthDataSource @@ -27,9 +28,6 @@ import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject private const val VALID_STATUS = "valid" -private const val APPROVED_KYC_STATUS = "approved" -private const val IN_PROGRESS_KYC_STATUS = "in_progress" -private const val DECLINED_KYC_STATUS = "declined" private const val TAG = "TangemPay: OnboardingRepository" @Suppress("LongParameterList") @@ -145,7 +143,6 @@ internal class DefaultOnboardingRepository @Inject constructor( lastFourDigits = card.cardNumberEnd, balance = fiatBalance.availableBalance, currencyCode = fiatBalance.currency, - customerWalletAddress = paymentAccount.customerWalletAddress, depositAddress = response.depositAddress, isPinSet = response.card?.isPinSet == true, ) @@ -159,19 +156,19 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId, cardFrozenState = cardFrozenState) + ProductInstance(id = instance.id, cardId = instance.cardId) } return CustomerInfo( customerId = response?.id, productInstance = productInstance, - kycStatus = getKycStatus(status = response?.kyc?.status), + kycStatus = KycStatus.fromString(status = response?.kyc?.status), cardInfo = cardInfo, ).also { lastFetchedCustomerInfoMap[userWalletId] = it } } - override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either { + override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either { val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId) if (hasTangemPay != null) { return Either.Right(hasTangemPay) @@ -228,13 +225,4 @@ internal class DefaultOnboardingRepository @Inject constructor( setHideMainOnboardingBanner(userWalletId) } } - - private fun getKycStatus(status: String?): CustomerInfo.KycStatus { - return when (status?.lowercase()) { - IN_PROGRESS_KYC_STATUS -> CustomerInfo.KycStatus.PENDING - DECLINED_KYC_STATUS -> CustomerInfo.KycStatus.REJECTED - APPROVED_KYC_STATUS -> CustomerInfo.KycStatus.APPROVED - else -> CustomerInfo.KycStatus.INIT - } - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 3252c49c4e..61f222a421 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -16,10 +16,10 @@ import com.tangem.datasource.api.pay.models.request.CardDetailsRequest import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest import com.tangem.datasource.api.pay.models.request.SetPinRequest import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse +import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails @@ -279,15 +279,15 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( orderStatus.onRight { response -> val status = response.result?.status - if (status == OrderStatus.COMPLETED.apiName || status == OrderStatus.CANCELED.apiName) { + if (status == Status.COMPLETED || status == Status.CANCELED) { // Remove from jobs pollingJobs.remove(key = orderId) // Final card state val finalState = when { - status == OrderStatus.COMPLETED.apiName && isFreeze + status == Status.COMPLETED && isFreeze -> TangemPayCardFrozenState.Frozen - status == OrderStatus.COMPLETED.apiName && !isFreeze + status == Status.COMPLETED && !isFreeze -> TangemPayCardFrozenState.Unfrozen else -> return@launch } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index 021605d89a..eab58caa05 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -77,34 +77,22 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( limit: Int, ): List { cacheRegistry.invokeOnExpire( - key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor), + key = getCacheKey(userWalletId = userWalletId, cursor = cursor), skipCache = config.shouldRefresh, - block = { - fetch( - userWalletId = userWalletId, - customerWalletAddress = config.customerWalletAddress, - cursor = cursor, - pageSize = limit, - ) - }, + block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) }, ) return txHistoryItemsStore.getSyncOrNull( - key = config.customerWalletAddress, + key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, ).orEmpty() } - private fun getCacheKey(customerWalletAddress: String, cursor: String?): String { - return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}" + private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String { + return "tangem_pay_tx_history_${userWalletId.stringValue}_${cursor ?: INITIAL_CURSOR}" } - private suspend fun fetch( - userWalletId: UserWalletId, - customerWalletAddress: String, - cursor: String?, - pageSize: Int, - ) { + private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) { requestPerformer.performRequest(userWalletId = userWalletId) { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) }.onLeft { @@ -112,7 +100,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( }.onRight { response -> val result = response.result val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull() - txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) + txHistoryItemsStore.store(key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, value = items) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt new file mode 100644 index 0000000000..5b8866a09a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -0,0 +1,86 @@ +package com.tangem.data.pay.store + +import androidx.datastore.core.DataStore +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import timber.log.Timber + +internal typealias WalletIdWithPaymentStatus = Map +internal typealias WalletIdWithPaymentStatusDM = Map + +/** + * Store for payment account statuses with dual storage (runtime + persistence). + * + * @property runtimeStore runtime store for fast in-memory access + * @property persistenceDataStore persistence store for caching across app restarts + */ +internal class PaymentAccountStatusesStore( + private val runtimeStore: RuntimeSharedStore, + private val persistenceDataStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + try { + val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch + runtimeStore.store( + value = cachedStatuses.mapValues { (_, statusDM) -> + PaymentAccountStatusDMConverter.convertBack(statusDM) + }, + ) + } catch (e: Exception) { + Timber.e(e, "Error while loading cached payment account statuses") + } + } + } + + fun get(userWalletId: UserWalletId): Flow { + return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + } + + suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) + } + + suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status) } + } + } + + suspend fun contains(userWalletId: UserWalletId): Boolean { + return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + put(key = userWalletId.stringValue, value = status) + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { + val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return + persistenceDataStore.updateData { storedStatuses -> + storedStatuses.toMutableMap().apply { + put(key = userWalletId.stringValue, value = statusDM) + } + } + } +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt new file mode 100644 index 0000000000..56cfcb4a13 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt @@ -0,0 +1,101 @@ +package com.tangem.data.wallets.derivations + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation +import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.ColdCurvesConfig +import com.tangem.domain.wallets.config.CurvesConfig +import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +/** + * Source of derivations data + */ +internal sealed interface DerivationsSource { + + val isHDWalletAllowed: Boolean + val hasOldStyleDerivation: Boolean + val curvesConfig: CurvesConfig + val derivationStyleProvider: DerivationStyleProvider + + fun getWalletPublicKey(curve: EllipticCurve): ByteArray? + fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap + + data class FromUserWallet(val userWallet: UserWallet) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.settings.isHDWalletAllowed + is UserWallet.Hot -> true + } + + override val hasOldStyleDerivation: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.hasOldStyleDerivation + is UserWallet.Hot -> false + } + + override val curvesConfig: CurvesConfig + get() = userWallet.curvesConfig + + override val derivationStyleProvider: DerivationStyleProvider + get() = userWallet.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getWalletPublicKey(curve) + is UserWallet.Hot -> userWallet.wallets + ?.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey + } + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getDerivedKeys(publicKey) + is UserWallet.Hot -> { + val derivedKeys = userWallet.wallets + ?.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) } + ?.derivedKeys + .orEmpty() + + ExtendedPublicKeysMap(derivedKeys) + } + } + } + } + + data class FromScanResponse(val scanResponse: ScanResponse) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = scanResponse.card.settings.isHDWalletAllowed + + override val hasOldStyleDerivation: Boolean + get() = scanResponse.card.hasOldStyleDerivation + + override val curvesConfig: CurvesConfig + get() = ColdCurvesConfig(scanResponse.card) + + override val derivationStyleProvider: DerivationStyleProvider + get() = scanResponse.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return scanResponse.getWalletPublicKey(curve) + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return scanResponse.getDerivedKeys(publicKey) + } + } +} + +private fun ScanResponse.getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return card.wallets.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey +} + +private fun ScanResponse.getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index ce12ab7516..3ea84acd9e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -3,45 +3,80 @@ package com.tangem.data.wallets.derivations import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.config.curvesConfig -import com.tangem.domain.wallets.derivations.derivationStyleProvider -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import kotlin.collections.forEach private typealias DerivationData = Pair> internal typealias Derivations = Map> +/** + * Data class representing a blockchain with its derivation path + */ +data class BlockchainToDerive( + val blockchain: Blockchain, + val derivationPath: DerivationPath, +) + /** * Finder of missed derivations * - * @property userWallet User wallet to find derivations for + * @property source Source of derivations data (UserWallet or ScanResponse) * [REDACTED_AUTHOR] */ -internal class MissedDerivationsFinder(private val userWallet: UserWallet) { +class MissedDerivationsFinder private constructor(private val source: DerivationsSource) { + + /** + * Secondary constructor for backward compatibility with UserWallet + */ + constructor(userWallet: UserWallet) : this(DerivationsSource.FromUserWallet(userWallet)) + + /** + * Secondary constructor for ScanResponse + */ + constructor(scanResponse: ScanResponse) : this(DerivationsSource.FromScanResponse(scanResponse)) /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { return currencies.map { it.network }.let(::findByNetworks) } + /** Find missed derivations for given [Network] list */ fun findByNetworks(networks: List): Derivations { + val blockchainsToDerive = networks.mapNotNull { network -> + val blockchain = network.toBlockchain() + val derivationPath = network.derivationPath.value?.let(::DerivationPath) + ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + return findByBlockchainsToDerive(blockchainsToDerive) + } + + /** Find missed derivations for given [BlockchainToDerive] list */ + fun findByBlockchainsToDerive(blockchainsToDerive: Collection): Derivations { + val enrichedBlockchains = blockchainsToDerive.enrichBlockchains() + return findDerivationsInternal(enrichedBlockchains) + } + + /** + * Common implementation for finding derivations + */ + private fun findDerivationsInternal(items: Collection): Derivations { return buildMap> { - networks - .mapToNewDerivations() + items + .mapNotNull(::mapToNewDerivation) .forEach { data -> val current = this[data.first] if (current != null) { current.addAll(data.second) - current.distinct() + this[data.first] = current.distinct().toMutableList() } else { this[data.first] = data.second.toMutableList() } @@ -49,31 +84,17 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } } - private fun List.mapToNewDerivations(): List { - return mapNotNull { network -> - val blockchain = network.toBlockchain() - val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null + /** + * Maps a single BlockchainToDerive to derivation data (public key -> derivation paths) + */ + private fun mapToNewDerivation(input: BlockchainToDerive): DerivationData? { + val curve = source.curvesConfig.primaryCurve(input.blockchain) ?: return null + if (!input.blockchain.getSupportedCurves().contains(curve)) return null - val walletPublicKey = when (userWallet) { - is UserWallet.Cold -> { - val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve } - wallet?.publicKey - } - is UserWallet.Hot -> { - val wallet = userWallet.wallets?.firstOrNull { it.curve == curve } - wallet?.publicKey - } - } + val publicKey = source.getWalletPublicKey(curve) ?: return null - walletPublicKey?.let { - findNewDerivations(curve = curve, publicKey = it, network = network) - } - } - } - - private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? { - val derivationCandidates = network - .getDerivationCandidates(curve) + val derivationCandidates = input.blockchain + .getDerivationCandidates(input.derivationPath) .ifEmpty { return null } .filterAlreadyDerivedKeys(publicKey.toMapKey()) .ifEmpty { return null } @@ -81,59 +102,63 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { return publicKey.toMapKey() to derivationCandidates } - private fun Network.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = this.toBlockchain() - + /** + * Gets all possible derivation paths for a blockchain + */ + private fun Blockchain.getDerivationCandidates(derivationPath: DerivationPath): List { return buildList { - add(blockchain.getDerivationPath(curve = curve)) - add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) - add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates)) + // Default derivation path for blockchain + add(getDerivationPath()) + + // The specified derivation path (can be either default or custom) + add(derivationPath) + + // Extended Cardano derivation path if needed + add(getCardanoExtendedDerivationPath(derivationPath)) } .filterNotNull() .distinct() } - private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle()) - } else { - null - } + private fun Blockchain.getDerivationPath(): DerivationPath? { + return derivationPath(style = source.derivationStyleProvider.getDerivationStyle()) } - private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - network.derivationPath.value?.let(::DerivationPath) - } else { - null - } - } - - private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? { - return if (this == Blockchain.Cardano) { - network.derivationPath.value?.let { - CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) - } - } else { - null - } + private fun Blockchain.getCardanoExtendedDerivationPath(customDerivationPath: DerivationPath): DerivationPath? { + if (this != Blockchain.Cardano) return null + return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath) } private fun List.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey) + val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList() return filterNot(alreadyDerivedPaths::contains) } - private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val extendedPublicKeysMap = when (userWallet) { - is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) - is UserWallet.Hot -> { - val wallets = userWallet.wallets ?: return emptyList() - wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys - ?: ExtendedPublicKeysMap(emptyMap()) - } + // region Blockchain enrichment logic + + /** + * Enriches blockchains collection: + * - Adds Ethereum if HD wallet is allowed + * - Removes unnecessary blockchains that share derivation path with Ethereum (for cards without old style derivation) + */ + private fun Collection.enrichBlockchains(): Collection { + if (!source.isHDWalletAllowed) return this + + val derivationStyle = source.derivationStyleProvider.getDerivationStyle() + val ethereumDerivationPath = Blockchain.Ethereum.derivationPath(derivationStyle) ?: return this + + val withEthereum = this + BlockchainToDerive(Blockchain.Ethereum, ethereumDerivationPath) + + // For cards with old style derivation, keep all blockchains + if (source.hasOldStyleDerivation) { + return withEthereum.distinct() } - return extendedPublicKeysMap.keys.toList() + // For new cards: filter out blockchains with same derivation path as Ethereum (except Ethereum itself) + return withEthereum + .filter { it.derivationPath != ethereumDerivationPath || it.blockchain == Blockchain.Ethereum } + .distinct() } + + // endregion } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt index 8a77f2f70a..d3887a70c3 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -47,18 +47,11 @@ class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( hotWalletId = hotWalletId, auth = true, ) - val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( - hotWalletId = hotWalletId, - auth = false, - ) - appPreferencesStore.editData { - it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + appPreferencesStore.editData { data -> + data.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + data.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + data.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) } } @@ -119,13 +112,21 @@ class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( } else -> { val remaining = remainingSeconds(deadlineElapsed, bootStored) - Attempts.WithDelay(count, remaining) + val newCount = if (id.auth) { + count + } else { + MAX_FAST_FORWARD_ATTEMPTS + } + Attempts.WithDelay(newCount, remaining) } } } private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { - return "${hotWalletId.value}_$auth" + // Regarding [REDACTED_TASK_KEY], the attempts counter must be shared between modes (auth vs signing). + // To provide backward compatibility, we use the same keys but read attempts in auth mode for security reasons. + val isAuthMode = true + return "${hotWalletId.value}_$isAuthMode" } private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt index 426a1bc144..cd22c4c1f8 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt @@ -14,11 +14,13 @@ import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class MissedDerivationsFinderTest { @Test @@ -97,9 +99,8 @@ internal class MissedDerivationsFinderTest { val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) - Truth.assertThat(actual).containsExactly( - ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()), - listOf( + val expected = mapOf( + ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()) to listOf( DerivationConfigV2.derivations(Blockchain.Cardano).values.first(), CardanoUtils.extendedDerivationPath( derivationPath = DerivationPath( @@ -108,7 +109,12 @@ internal class MissedDerivationsFinderTest { ), ), ), + ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()) to listOf( + DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(), + ), ) + + Truth.assertThat(actual).containsExactlyEntriesIn(expected) } @Test diff --git a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt index 2a87c8b14e..a8a0e5cdbb 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt @@ -22,4 +22,8 @@ abstract class SingleAccountSupplier( fun filterPaymentAccount(accountId: AccountId): Flow { return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() } + + fun filterCryptoPortfolioAccount(accountId: AccountId): Flow { + return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() + } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index a228aceb2e..1f7bea813c 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -67,8 +67,8 @@ internal object AccountStatusUseCaseModule { fun provideApplyTokenListSortingUseCaseV2( accountsCRUDRepository: AccountsCRUDRepository, dispatchers: CoroutineDispatcherProvider, - ): ApplyTokenListSortingUseCaseV2 { - return ApplyTokenListSortingUseCaseV2( + ): ApplyTokenListSortingUseCase { + return ApplyTokenListSortingUseCase( accountsCRUDRepository = accountsCRUDRepository, dispatchers = dispatchers, ) @@ -141,8 +141,8 @@ internal object AccountStatusUseCaseModule { @Singleton fun provideToggleTokenListSortingUseCaseV2( dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListSortingUseCaseV2 { - return ToggleTokenListSortingUseCaseV2( + ): ToggleTokenListSortingUseCase { + return ToggleTokenListSortingUseCase( dispatchers = dispatchers, ) } @@ -151,8 +151,8 @@ internal object AccountStatusUseCaseModule { @Singleton fun provideToggleTokenListGroupingUseCaseV2( dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListGroupingUseCaseV2 { - return ToggleTokenListGroupingUseCaseV2( + ): ToggleTokenListGroupingUseCase { + return ToggleTokenListGroupingUseCase( dispatchers = dispatchers, ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt index 5acaea2b98..152ab3ca77 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt @@ -4,7 +4,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.serialization.Serializable -typealias AccountCryptoCurrencies = Map> +typealias AccountCryptoCurrencies = Map> /** * Combines an [Account] with its corresponding [CryptoCurrency]. diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt index 6dc53cca47..55e21d936c 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt @@ -25,7 +25,7 @@ private typealias SortingErrorByAccountId = MutableMap + errors[account.accountId] = error return@map account } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt index 32e1b28366..648daca038 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * * @property dispatchers Provides coroutine dispatchers for executing tasks. */ -class ToggleTokenListGroupingUseCaseV2( +class ToggleTokenListGroupingUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt index ccf2416368..18bfbc35a1 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * * @property dispatchers Provides coroutine dispatchers for executing tasks. */ -class ToggleTokenListSortingUseCaseV2( +class ToggleTokenListSortingUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt index af71ec2c02..bd340dc150 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -30,7 +30,7 @@ internal class ApplyTokenListSortingUseCaseTest { private val accountsCRUDRepository = mockk(relaxUnitFun = true) - private val useCase = ApplyTokenListSortingUseCaseV2( + private val useCase = ApplyTokenListSortingUseCase( accountsCRUDRepository = accountsCRUDRepository, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt similarity index 98% rename from domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt index 9dadb85e4f..9e3b423a62 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt @@ -28,9 +28,9 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ToggleTokenListGroupingUseCaseV2Test { +class ToggleTokenListGroupingUseCaseTest { - private val useCase = ToggleTokenListGroupingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider()) + private val useCase = ToggleTokenListGroupingUseCase(dispatchers = TestingCoroutineDispatcherProvider()) private val userWalletId = UserWalletId("011") private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt similarity index 97% rename from domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt index 344dc8641e..c7808be2d4 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt @@ -28,9 +28,9 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ToggleTokenListSortingUseCaseV2Test { +class ToggleTokenListSortingUseCaseTest { - private val useCase = ToggleTokenListSortingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider()) + private val useCase = ToggleTokenListSortingUseCase(dispatchers = TestingCoroutineDispatcherProvider()) private val userWalletId = UserWalletId("011") private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/domain/earn/build.gradle.kts b/domain/earn/build.gradle.kts index f72d726fea..380db663f4 100644 --- a/domain/earn/build.gradle.kts +++ b/domain/earn/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { api(projects.domain.core) api(projects.domain.models) api(projects.core.pagination) + implementation(projects.domain.account) implementation(projects.domain.common) - implementation(projects.domain.networks) implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt index 1988d0e166..e6a93c5605 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -1,26 +1,29 @@ package com.tangem.domain.earn.usecase import arrow.core.Either +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.models.earn.EarnNetwork import com.tangem.domain.models.earn.EarnNetworks import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.networks.multi.MultiNetworkStatusProducer -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged /** - * Observes earn networks with [EarnNetwork.isAdded] enriched from user's wallets - * via [multiNetworkStatusSupplier]. Single entry point for all/mine filtering. + * Observes earn networks with [EarnNetwork.isAdded] enriched from user's active (non-archived) + * accounts via [multiAccountListSupplier]. Single entry point for all/mine filtering. + * + * Uses [MultiAccountListSupplier] so that only networks from active accounts are considered; + * archived accounts are not included in [AccountList.accounts]. */ class GetEarnNetworksUseCase( private val earnRepository: EarnRepository, + private val multiAccountListSupplier: MultiAccountListSupplier, private val userWalletsListRepository: UserWalletsListRepository, - private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, ) { operator fun invoke(): Flow { @@ -36,24 +39,24 @@ class GetEarnNetworksUseCase( }.distinctUntilChanged() } - @OptIn(ExperimentalCoroutinesApi::class) private fun observeMyNetworkIds(): Flow> { - return userWalletsListRepository.userWallets - .map { it.orEmpty() } - .flatMapLatest { wallets -> - val activeWallets = wallets - .filterNot(UserWallet::isLocked) - .filter(UserWallet::isMultiCurrency) - if (activeWallets.isEmpty()) { - flowOf(emptySet()) - } else { - val flows = activeWallets.map { wallet -> - multiNetworkStatusSupplier( - MultiNetworkStatusProducer.Params(userWalletId = wallet.walletId), - ).map { statuses -> statuses.map { it.network.backendId }.toSet() } - } - combine(flows) { arrays -> arrays.flatMap { it }.toSet() } - } + return combine( + multiAccountListSupplier(), + userWalletsListRepository.userWallets, + ) { accountLists, wallets -> + val unlockedWalletsId = wallets + .orEmpty() + .filterNot(UserWallet::isLocked) + .mapTo(HashSet()) { it.walletId } + + if (unlockedWalletsId.isEmpty()) { + return@combine emptySet() } + + accountLists + .filter { it.userWalletId in unlockedWalletsId } + .flatMap(AccountList::flattenCurrencies) + .mapTo(HashSet()) { it.network.backendId } + } } } \ No newline at end of file diff --git a/domain/kyc/models/.gitignore b/domain/kyc/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/kyc/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/kyc/models/build.gradle.kts b/domain/kyc/models/build.gradle.kts new file mode 100644 index 0000000000..6b18f3f83f --- /dev/null +++ b/domain/kyc/models/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt new file mode 100644 index 0000000000..7297bc7c87 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.models.kyc + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +private const val APPROVED_KYC_STATUS = "approved" +private const val IN_PROGRESS_KYC_STATUS = "in_progress" +private const val DECLINED_KYC_STATUS = "declined" + +@JsonClass(generateAdapter = false) +enum class KycStatus { + /** Initial state */ + @Json(name = "init") + INIT, + + /** Performing the check */ + @Json(name = "in_progress") + PENDING, + + /** SumSub approved */ + @Json(name = "approved") + APPROVED, + + /** The check failed, documents rejected */ + @Json(name = "declined") + REJECTED, + + ; + + companion object { + + fun fromString(status: String?, default: KycStatus = INIT): KycStatus { + return when (status?.lowercase()) { + IN_PROGRESS_KYC_STATUS -> PENDING + DECLINED_KYC_STATUS -> REJECTED + APPROVED_KYC_STATUS -> APPROVED + else -> default + } + } + } +} \ No newline at end of file diff --git a/domain/offramp/.gitignore b/domain/offramp/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/offramp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/offramp/build.gradle.kts b/domain/offramp/build.gradle.kts new file mode 100644 index 0000000000..c2d05ca8fe --- /dev/null +++ b/domain/offramp/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + /** Domain modules */ + api(projects.domain.core) + api(projects.domain.models) + + /** Test libraries */ + testImplementation(projects.test.core) + testRuntimeOnly(deps.test.junit5.engine) +} diff --git a/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt b/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt new file mode 100644 index 0000000000..75b764ac2d --- /dev/null +++ b/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.offramp + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.offramp.repository.OfframpRepository + +/** + * Use case for getting offramp (sell crypto) URL + * + * @property offrampRepository repository for offramp operations + */ +class GetOfframpUrlUseCase( + private val offrampRepository: OfframpRepository, +) { + + operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrencyCode: String): Either = + either { + val walletAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ensure(walletAddress != null) { Error.WalletAddressNotFound } + + val url = offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrencyStatus.currency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + ensure(url != null) { Error.UrlNotAvailable } + + url + } + + /** Offramp use case errors */ + sealed class Error { + /** Wallet address not found in currency status */ + data object WalletAddressNotFound : Error() + + /** Offramp URL is not available for this currency */ + data object UrlNotAvailable : Error() + } +} \ No newline at end of file diff --git a/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt b/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt new file mode 100644 index 0000000000..0fdfca218b --- /dev/null +++ b/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.offramp.repository + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * Repository for offramp (sell crypto) operations + */ +interface OfframpRepository { + + /** + * Get offramp (sell) URL for the given cryptocurrency + * + * @param cryptoCurrency crypto currency to sell + * @param fiatCurrencyCode fiat currency code (e.g., "USD", "EUR") + * @param walletAddress wallet address for the refund + * @return URL for offramp service or null if not available + */ + fun getOfframpUrl(cryptoCurrency: CryptoCurrency, fiatCurrencyCode: String, walletAddress: String): String? +} \ No newline at end of file diff --git a/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt b/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt new file mode 100644 index 0000000000..2ae52dafbe --- /dev/null +++ b/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt @@ -0,0 +1,127 @@ +package com.tangem.domain.offramp + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.offramp.repository.OfframpRepository +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetOfframpUrlUseCaseTest { + + private val offrampRepository: OfframpRepository = mockk() + private val useCase = GetOfframpUrlUseCase(offrampRepository) + + private val cryptoCurrency: CryptoCurrency = mockk() + private val appCurrencyCode = "USD" + private val walletAddress = "0x1234567890abcdef" + private val expectedUrl = "https://moonpay.com/sell?address=$walletAddress" + + @BeforeEach + fun resetMocks() { + clearMocks(offrampRepository) + } + + @Test + fun `invoke should return url when wallet address and url are available`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress) + every { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } returns expectedUrl + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isRight()).isTrue() + assertThat(result.getOrNull()).isEqualTo(expectedUrl) + + verify(exactly = 1) { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } + } + + @Test + fun `invoke should return WalletAddressNotFound error when network address is null`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(networkAddress = null) + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.WalletAddressNotFound) + + verify(exactly = 0) { + offrampRepository.getOfframpUrl(any(), any(), any()) + } + } + + @Test + fun `invoke should return UrlNotAvailable error when repository returns null`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress) + every { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } returns null + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.UrlNotAvailable) + + verify(exactly = 1) { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } + } + + private fun createCryptoCurrencyStatus( + walletAddress: String? = null, + networkAddress: NetworkAddress? = null, + ): CryptoCurrencyStatus { + val resolvedNetworkAddress = networkAddress ?: walletAddress?.let { address -> + mockk { + every { defaultAddress } returns mockk { + every { value } returns address + } + } + } + + val statusValue: CryptoCurrencyStatus.Value = mockk { + every { this@mockk.networkAddress } returns resolvedNetworkAddress + } + + return mockk { + every { currency } returns cryptoCurrency + every { value } returns statusValue + } + } +} + diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt index fa08534edf..3553692065 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt @@ -1,7 +1,5 @@ package com.tangem.domain.staking.toggles interface StakingFeatureToggles { - val isTonStakingEnabled: Boolean - val isCardanoStakingEnabled: Boolean val isEthStakingEnabled: Boolean } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c2a83d7894..53e21909a4 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.card) implementation(projects.domain.staking) + implementation(projects.domain.visa) implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt deleted file mode 100644 index 11bdea2a34..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.operations.TokenListFactory -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class ToggleTokenListGroupingUseCase( - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke(tokenList: TokenList): Either { - return withContext(dispatchers.default) { - either { - when (tokenList) { - is TokenList.GroupedByNetwork -> ungroupTokens(tokenList) - is TokenList.Ungrouped -> groupTokens(tokenList) - is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty) - } - } - } - } - - private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { - validate(tokenList) - - return TokenListFactory.createGroupedByNetwork(tokenList) - } - - private fun Raise.ungroupTokens(tokenList: TokenList.GroupedByNetwork): TokenList.Ungrouped { - validate(tokenList) - - return TokenListFactory.createUngrouped(tokenList) - } - - private fun Raise.validate(tokenList: TokenList) { - ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { - TokenListSortingError.TokenListIsLoading - } - - ensure(tokenList.flattenCurrencies().isNotEmpty()) { - TokenListSortingError.TokenListIsEmpty - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt deleted file mode 100644 index 2bc37f5bb1..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.operations.TokenListFactory -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class ToggleTokenListSortingUseCase( - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke(tokenList: TokenList): Either { - return withContext(dispatchers.default) { - either { - ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { - TokenListSortingError.TokenListIsLoading - } - - TokenListFactory.create( - statuses = tokenList.flattenCurrencies(), - groupType = when (tokenList) { - is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK - is TokenList.Ungrouped -> TokensGroupType.NONE - is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty) - }, - sortType = TokensSortType.BALANCE, - ) - } - } - } -} \ No newline at end of file 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 a2eeb1e0a6..c281c5edd4 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 @@ -5,4 +5,6 @@ package com.tangem.domain.tokens * [REDACTED_AUTHOR] */ -interface TokensFeatureToggles \ No newline at end of file +interface TokensFeatureToggles { + val isMultiAddressUtxoEnabled: Boolean +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt deleted file mode 100644 index 1cf1651c9d..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.tokens.legacy - -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import org.rekotlin.Action - -sealed class TradeCryptoAction : Action { - - data class FinishSelling(val transactionId: String) : TradeCryptoAction() - - data class Sell( - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - ) : TradeCryptoAction() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt index a30ede50c9..56c8bf8ef3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt @@ -9,4 +9,5 @@ enum class FetchingSource { NETWORK, QUOTE, STAKING, + TANGEM_PAY, } \ 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 06b3f5f4ee..17b46927d6 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 @@ -2,11 +2,13 @@ package com.tangem.domain.tokens.wallet import arrow.core.Either import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher @@ -45,6 +47,7 @@ class WalletBalanceFetcher internal constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -57,6 +60,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( @@ -72,6 +76,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -91,10 +96,18 @@ class WalletBalanceFetcher internal constructor( error("UserWallet doesn't contain crypto-currencies: $userWalletId") } - fetcher.fetch(userWalletId = userWalletId, currencies = currencies) + fetcher.fetch( + userWalletId = userWalletId, + currencies = currencies, + paymentAccountRefactorEnabled = params.isPaymentAccountRefactorEnabled, + ) } - private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set) { + private suspend fun BaseWalletBalanceFetcher.fetch( + userWalletId: UserWalletId, + currencies: Set, + paymentAccountRefactorEnabled: Boolean, + ) { coroutineScope { val results = fetchingSources.map { source -> async { @@ -102,6 +115,10 @@ class WalletBalanceFetcher internal constructor( FetchingSource.NETWORK -> fetchNetworks(userWalletId = userWalletId, currencies = currencies) FetchingSource.QUOTE -> fetchQuotes(currencies = currencies) FetchingSource.STAKING -> fetchStaking(userWalletId = userWalletId, currencies = currencies) + FetchingSource.TANGEM_PAY -> fetchPaymentAccount( + userWalletId = userWalletId, + paymentAccountRefactorEnabled = paymentAccountRefactorEnabled, + ) } source to maybeResult @@ -173,10 +190,19 @@ class WalletBalanceFetcher internal constructor( } } + private suspend fun fetchPaymentAccount( + userWalletId: UserWalletId, + paymentAccountRefactorEnabled: Boolean, + ): Either { + if (!paymentAccountRefactorEnabled) return Unit.right() + + return paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) + } + /** * Params of [WalletBalanceFetcher] * * @property userWalletId user wallet id */ - data class Params(val userWalletId: UserWalletId) + data class Params(val userWalletId: UserWalletId, val isPaymentAccountRefactorEnabled: Boolean) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt index 8bd9d8a36c..eaa142f3fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt @@ -27,6 +27,7 @@ internal class MultiWalletBalanceFetcher( FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING, + FetchingSource.TANGEM_PAY, ) override suspend fun getCryptoCurrencies(userWalletId: UserWalletId): Set { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt deleted file mode 100644 index 8630063505..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ToggleTokenListGroupingTest { - - private val useCase = ToggleTokenListGroupingUseCase( - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `when list is empty then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsEmpty.left() - - // When - val actual = useCase(MockTokenLists.emptyUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.sortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.unsortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.sortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.unsortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt deleted file mode 100644 index 787d55393c..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ToggleTokenListSortingUseCaseTest { - - private val useCase = ToggleTokenListSortingUseCase( - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `when list is empty then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsEmpty.left() - - // When - val actual = useCase(MockTokenLists.emptyTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index 2af6690bc8..937aa0d32d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID @@ -42,6 +43,7 @@ internal class WalletBalanceFetcherTest { private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( @@ -52,6 +54,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -76,7 +79,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } throws exception // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = exception.left() @@ -107,7 +115,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException("Unknown type of wallet: $userWalletId").left() @@ -139,7 +152,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } throws exception // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = exception.left() @@ -171,7 +189,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns emptySet() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException("UserWallet doesn't contain crypto-currencies: $userWalletId").left() @@ -213,7 +236,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -259,7 +287,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -311,7 +344,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -354,7 +392,12 @@ internal class WalletBalanceFetcherTest { } returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency) // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -396,7 +439,12 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -444,7 +492,12 @@ internal class WalletBalanceFetcherTest { } returns stellarStakingId // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -506,7 +559,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -572,7 +630,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() @@ -624,7 +687,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() @@ -674,7 +742,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt index 55bc9e7b8b..100c5964c1 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt @@ -47,7 +47,12 @@ class MultiWalletBalanceFetcherTest { val actual = fetcher.fetchingSources // Assert - val expected = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING) + val expected = setOf( + FetchingSource.NETWORK, + FetchingSource.QUOTE, + FetchingSource.STAKING, + FetchingSource.TANGEM_PAY, + ) Truth.assertThat(actual).isEqualTo(expected) } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 559774d3e2..3c13ebe644 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,10 +24,6 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) - implementation(projects.features.swap.domain) - - /** Feature API - remove after removing [TangemPayFeatureToggles] */ - implementation(projects.features.tangempay.details.api) /** Security */ implementation(deps.spongecastle.core) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt new file mode 100644 index 0000000000..bdc604087f --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.pay + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +@Serializable +sealed class PaymentAccountStatus { + + abstract val source: StatusSource + + @Serializable + data object Loading : PaymentAccountStatus() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data object NotCreated : PaymentAccountStatus() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data class UnderReview( + override val source: StatusSource, + val kycStatus: KycStatus, + ) : PaymentAccountStatus() + + @Serializable + data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus() + + @Serializable + data class Locked(override val source: StatusSource) : PaymentAccountStatus() + + @Serializable + data class Loaded( + override val source: StatusSource, + val cardId: String, + val lastFourDigits: String, + val balance: SerializedBigDecimal, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + ) : PaymentAccountStatus() + + @Serializable + sealed class Error : PaymentAccountStatus() { + @Serializable + data object ExposedDevice : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data class Unavailable(override val source: StatusSource) : Error() + + @Serializable + data object NotSynced : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data object CardIssueFailed : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index 14edae3667..e2424a061a 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -9,7 +9,6 @@ data class TangemPayDetailsConfig( val cardId: String, val isPinSet: Boolean, val cardFrozenState: TangemPayCardFrozenState, - val customerWalletAddress: String, val cardNumberEnd: String, val chainId: Int, ) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt new file mode 100644 index 0000000000..740d9d0824 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.models.wallet.UserWalletId + +interface PaymentAccountStatusFetcher : FlowFetcher { + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt new file mode 100644 index 0000000000..c49a25f45d --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus + +interface PaymentAccountStatusProducer : FlowProducer { + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt new file mode 100644 index 0000000000..94580d26e1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.pay.PaymentAccountStatus + +@Suppress("UnnecessaryAbstractClass") +abstract class PaymentAccountStatusSupplier( + override val factory: PaymentAccountStatusProducer.Factory, + override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 501df2011b..2fd02e7a45 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -1,6 +1,6 @@ package com.tangem.domain.pay.model -import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.domain.models.kyc.KycStatus import java.math.BigDecimal sealed class MainCustomerInfoContentState { @@ -22,31 +22,15 @@ data class CustomerInfo( val cardInfo: CardInfo?, ) { - enum class KycStatus { - /** Initial state */ - INIT, - - /** Performing the check */ - PENDING, - - /** SumSub approved */ - APPROVED, - - /** The check failed, documents rejected */ - REJECTED, - } - data class ProductInstance( val id: String, val cardId: String, - val cardFrozenState: TangemPayCardFrozenState, ) data class CardInfo( val lastFourDigits: String, val balance: BigDecimal, val currencyCode: String, - val customerWalletAddress: String, val depositAddress: String?, val isPinSet: Boolean, ) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt index 6ae706a0e1..327d8fd61f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt @@ -1,9 +1,9 @@ package com.tangem.domain.pay.model -enum class OrderStatus(val apiName: String) { - UNKNOWN(""), - NEW("NEW"), - PROCESSING("PROCESSING"), - COMPLETED("COMPLETED"), - CANCELED("CANCELED"), +enum class OrderStatus { + UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + NEW, + PROCESSING, + COMPLETED, + CANCELED, } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 568cf85006..52197ec817 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -23,7 +23,7 @@ interface OnboardingRepository { suspend fun getOrderId(userWalletId: UserWalletId): String? - suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either suspend fun checkCustomerEligibility(): Boolean suspend fun getCustomerEligibility(): Boolean diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 647e6ad412..4116e2e4f4 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.pay.usecase import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.* @@ -38,7 +39,7 @@ class TangemPayMainScreenCustomerInfoUseCase( return // fast exit } - onboardingRepository.checkCustomerWallet(userWalletId) + onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") @@ -125,7 +126,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } .map { customerInfo -> Timber.tag(TAG).i("customerInfo") - if (customerInfo.cardInfo == null && customerInfo.kycStatus == CustomerInfo.KycStatus.APPROVED) { + if (customerInfo.cardInfo == null && customerInfo.kycStatus == KycStatus.APPROVED) { // If order id wasn't saved -> start order creation and get customer info onboardingRepository.createOrder(userWalletId) } @@ -151,7 +152,7 @@ class TangemPayMainScreenCustomerInfoUseCase( info = CustomerInfo( customerId = null, productInstance = null, - kycStatus = CustomerInfo.KycStatus.APPROVED, + kycStatus = KycStatus.APPROVED, cardInfo = null, ), orderStatus = orderData.status, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt index fb61bc2965..5b68240025 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt @@ -1,3 +1,3 @@ package com.tangem.domain.tangempay.model -data class TangemPayTxHistoryListConfig(val customerWalletAddress: String, val shouldRefresh: Boolean) \ No newline at end of file +data class TangemPayTxHistoryListConfig(val shouldRefresh: Boolean) \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt index b18a7db430..2a24783240 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent { ) : Params data class Edit( - val account: Account, + val account: Account.CryptoPortfolio, ) : Params } } \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt index d1c47280ea..99450a8474 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt @@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account interface AccountDetailsComponent : ComposableContentComponent { interface Factory : ComponentFactory - data class Params(val account: Account) + data class Params(val account: Account.CryptoPortfolio) } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index d100998aca..4b69fc806d 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -25,7 +25,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents @@ -73,7 +72,7 @@ internal class AccountCreateEditModel @Inject constructor( when (params) { is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId) is AccountCreateEditComponent.Params.Edit -> { - val derivationIndex = params.account.derivationIndex?.value + val derivationIndex = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex) analyticsEventHandler.send(event) } @@ -170,7 +169,7 @@ internal class AccountCreateEditModel @Inject constructor( val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon) val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon - val derivationIndex = params.account.derivationIndex?.value + val derivationIndex = params.account.derivationIndex.value analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex)) uiState.value = uiState.value.toggleProgress(showProgress = true) @@ -182,7 +181,7 @@ internal class AccountCreateEditModel @Inject constructor( uiState.value = uiState.value.toggleProgress(showProgress = false) result - .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) } + .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) } .onRight { showMessage(R.string.account_edit_success_message) router.pop() diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index 47b06df25c..ee77d31aa2 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -14,12 +14,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent @@ -54,29 +52,29 @@ internal class AccountDetailsModel @Inject constructor( init { analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened()) - singleAccountSupplier(SingleAccountProducer.Params(accountId)) + singleAccountSupplier.filterCryptoPortfolioAccount(accountId) .onEach { account -> uiState.update { buildUI(account) } } .launchIn(modelScope) } - private fun onEditAccountClick(account: Account) { + private fun onEditAccountClick(account: Account.CryptoPortfolio) { analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit()) router.push(AppRoute.EditAccount(account)) } - private fun onManageTokensClick(account: Account) { + private fun onManageTokensClick(account: Account.CryptoPortfolio) { val route = AppRoute.ManageTokens( source = AppRoute.ManageTokens.Source.ACCOUNT, portfolioId = PortfolioId(account.accountId), ) analyticsEventHandler.send( - AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value), + AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value), ) router.push(route) } private fun onArchiveAccountClick() { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation) analyticsEventHandler.send(event) confirmArchiveDialog() @@ -86,7 +84,7 @@ internal class AccountDetailsModel @Inject constructor( val secondAction = EventMessageAction( title = resourceReference(R.string.common_cancel), onClick = { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation) analyticsEventHandler.send(event) }, @@ -107,7 +105,7 @@ internal class AccountDetailsModel @Inject constructor( } private fun archiveCryptoPortfolio() = modelScope.launch { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation) analyticsEventHandler.send(event) uiState.update { it.toggleProgress(true) } @@ -128,7 +126,7 @@ internal class AccountDetailsModel @Inject constructor( val event = AccountSettingsAnalyticEvents.AccountError( source = AccountSettingsAnalyticEvents.Source.ARCHIVE, error = error.tag, - accountDerivation = params.account.derivationIndex?.value, + accountDerivation = params.account.derivationIndex.value, ) analyticsEventHandler.send(event) val titleRes: Int @@ -155,16 +153,13 @@ internal class AccountDetailsModel @Inject constructor( messageSender.send(dialogMessage) } - private fun buildUI(account: Account): AccountDetailsUM { - val archiveMode = when (account) { - is Account.CryptoPortfolio -> when (account.isMainAccount) { - true -> ArchiveMode.None - false -> ArchiveMode.Available( - onArchiveAccountClick = ::onArchiveAccountClick, - isLoading = false, - ) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") + private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM { + val archiveMode = when (account.isMainAccount) { + true -> ArchiveMode.None + false -> ArchiveMode.Available( + onArchiveAccountClick = ::onArchiveAccountClick, + isLoading = false, + ) } val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull() ?.isMultiCurrency == true diff --git a/features/approval/api/build.gradle.kts b/features/approval/api/build.gradle.kts new file mode 100644 index 0000000000..0401b93110 --- /dev/null +++ b/features/approval/api/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.approval.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + + /** Common */ + implementation(projects.common.ui) + + /** Other */ + implementation(deps.kotlin.immutable.collections) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt new file mode 100644 index 0000000000..b680b9470b --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -0,0 +1,30 @@ +package com.tangem.features.approval.api + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet + +interface GiveApprovalComponent : ComposableBottomSheetComponent { + + data class Params( + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val amount: String, + val spenderAddress: String, + val subtitle: TextReference, + val callback: Callback, + ) + + interface Callback { + fun onApproveDone() + fun onApproveFailed() + fun onCancelClick() + } + + interface Factory { + fun create(context: AppComponentContext, params: Params): GiveApprovalComponent + } +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..46410d3fbf --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.approval.api + +interface GiveApprovalFeatureToggles { + + val isGaslessApprovalEnabled: Boolean +} \ No newline at end of file diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts new file mode 100644 index 0000000000..01c43ad67f --- /dev/null +++ b/features/approval/impl/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.approval.impl" +} + +dependencies { + + /** Feature */ + implementation(projects.features.approval.api) + implementation(projects.features.sendV2.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Common */ + implementation(projects.common.ui) + + /** SDK */ + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.runtime) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other */ + implementation(deps.decompose) + implementation(deps.decompose.ext.compose) + implementation(deps.timber) + implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt new file mode 100644 index 0000000000..51c2fe3986 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -0,0 +1,105 @@ +package com.tangem.features.approval.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.impl.model.GiveApprovalModel +import com.tangem.features.approval.impl.ui.GiveApprovalContent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import com.tangem.common.ui.R as CommonUiR + +internal class DefaultGiveApprovalComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: GiveApprovalComponent.Params, + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : GiveApprovalComponent, AppComponentContext by appComponentContext { + + private val model: GiveApprovalModel = getOrCreateModel(params = params) + + private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = child("giveApprovalFeeSelector"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = FeeSelectorUM.Loading, + onLoadFee = { model.loadFee() }, + onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) }, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY, + analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve, + userWalletId = params.userWallet.walletId, + ), + onResult = model::onFeeResult, + ) + + private val currency: String = params.cryptoCurrencyStatus.currency.symbol + + override fun dismiss() { + params.callback.onCancelClick() + } + + @Composable + override fun BottomSheet() { + val uiState by model.uiState.collectAsStateWithLifecycle() + + val config = remember { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.secondary, + titleText = resourceReference(CommonUiR.string.give_permission_title), + titleAction = TopAppBarButtonUM.Icon( + iconRes = CommonUiR.drawable.ic_information_24, + onClicked = model::showPermissionInfoDialog, + ), + ) { + GiveApprovalContent( + currency = currency, + subtitle = params.subtitle, + approveType = uiState.approveType, + approveItems = uiState.approveItems, + onChangeApproveType = model::onChangeApproveType, + walletInteractionIcon = walletInterationIcon(params.userWallet), + isApproveEnabled = uiState.isApproveButtonEnabled, + isApproveLoading = uiState.isApproveLoading, + onApproveClick = model::onApproveClick, + onCancelClick = model::onCancelClick, + onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) + } + } + + @AssistedFactory + interface Factory : GiveApprovalComponent.Factory { + override fun create( + context: AppComponentContext, + params: GiveApprovalComponent.Params, + ): DefaultGiveApprovalComponent + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..07867cc6a6 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt @@ -0,0 +1,13 @@ +package com.tangem.features.approval.impl + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.approval.api.GiveApprovalFeatureToggles + +internal class DefaultGiveApprovalFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : GiveApprovalFeatureToggles { + + // Remove GiveTxPermissionBottomSheet and all dependencies with this toggle + override val isGaslessApprovalEnabled: Boolean + get() = featureToggles.isFeatureEnabled("GASLESS_APPROVAL_ENABLED") +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt new file mode 100644 index 0000000000..94d432e9b9 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.approval.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.impl.DefaultGiveApprovalComponent +import com.tangem.features.approval.impl.model.GiveApprovalModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal interface GiveApprovalFeatureModule { + + @Binds + @Singleton + fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface GiveApprovalModelModule { + + @Binds + @IntoMap + @ClassKey(GiveApprovalModel::class) + fun bindModel(model: GiveApprovalModel): Model +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt new file mode 100644 index 0000000000..2b3fb3fa8f --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -0,0 +1,203 @@ +package com.tangem.features.approval.impl.model + +import androidx.compose.runtime.Stable +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.left +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +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.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP +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 java.math.BigDecimal +import javax.inject.Inject + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class GiveApprovalModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase, + private val getFeeForTokenUseCase: GetFeeForTokenUseCase, + private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, + private val uiMessageSender: UiMessageSender, + private val urlOpener: UrlOpener, +) : Model(), FeeSelectorModelCallback { + + private val params: GiveApprovalComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow( + GiveApprovalUM( + approveType = ApproveType.LIMITED, + isApproveButtonEnabled = false, + isApproveLoading = false, + ), + ) + + private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + this.feeSelectorUM = feeSelectorUM + uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) } + } + + fun onApproveClick() { + uiState.update { it.copy(isApproveLoading = true) } + modelScope.launch(dispatchers.main) { + val isSuccess = sendApprovalTransaction() + uiState.update { it.copy(isApproveLoading = false) } + if (isSuccess) { + params.callback.onApproveDone() + } else { + params.callback.onApproveFailed() + } + } + } + + fun onCancelClick() { + params.callback.onCancelClick() + } + + fun onChangeApproveType(approveType: ApproveType) { + uiState.update { it.copy(approveType = approveType) } + } + + fun onOpenLearnMoreAboutApproveClick() { + urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP) + } + + fun showPermissionInfoDialog() { + uiMessageSender.send( + DialogMessage( + message = resourceReference(com.tangem.common.ui.R.string.give_permission_staking_footer), + title = resourceReference(com.tangem.common.ui.R.string.common_approve), + ), + ) + } + + suspend fun prepareApprovalTransaction(): Either { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: return Either.Left(IllegalStateException("Currency is not a token")) + + return createApprovalTransactionUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, + amount = getApprovalAmount(), + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ) + } + + suspend fun loadFee(): Either { + val approvalTransaction = prepareApprovalTransaction() + .getOrElse { return GetFeeError.DataError(it).left() } + + return getFeeUseCase( + transactionData = approvalTransaction, + userWallet = params.userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + } + + suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either { + val approvalTransaction = prepareApprovalTransaction() + .getOrElse { return GetFeeError.DataError(it).left() } + + return if (maybeToken == null) { + getFeeForGaslessUseCase( + transactionData = approvalTransaction, + userWallet = params.userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + } else { + getFeeForTokenUseCase( + transactionData = approvalTransaction, + userWallet = params.userWallet, + token = maybeToken.currency, + ) + } + } + + private suspend fun sendApprovalTransaction(): Boolean { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false + + val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false + val selectedFee = feeContent.selectedFeeItem.fee + val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended + + val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency + + val transactionData = createApprovalTransactionUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, + amount = getApprovalAmount(), + fee = selectedFee, + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ).getOrElse { error -> + Timber.e(error, "Failed to create approval transaction") + return false + } + + return if (isFeeInTokenCurrency) { + createAndSendGaslessTransactionUseCase( + userWallet = params.userWallet, + transactionData = transactionData, + fee = feeExtended, + ) + } else { + sendTransactionUseCase( + txData = transactionData, + userWallet = params.userWallet, + network = tokenCurrency.network, + ) + }.fold( + ifLeft = { error -> + Timber.e("Failed to send approval transaction: $error") + false + }, + ifRight = { true }, + ) + } + + private fun getApprovalAmount(): BigDecimal? { + return if (uiState.value.approveType == ApproveType.LIMITED) { + params.amount.toBigDecimalOrNull() + } else { + null + } + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt new file mode 100644 index 0000000000..83bf60054d --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.approval.impl.model + +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal data class GiveApprovalUM( + val approveType: ApproveType, + val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), + val isApproveButtonEnabled: Boolean, + val isApproveLoading: Boolean, +) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt new file mode 100644 index 0000000000..2d3386f430 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -0,0 +1,353 @@ +package com.tangem.features.approval.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +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.DpOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.PopupProperties +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import com.tangem.common.ui.R as CommonUiR + +@Composable +@Suppress("LongParameterList") +internal fun GiveApprovalContent( + currency: String, + subtitle: TextReference, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + walletInteractionIcon: Int?, + isApproveEnabled: Boolean, + isApproveLoading: Boolean, + onApproveClick: () -> Unit, + onCancelClick: () -> Unit, + onOpenLearnMoreAboutApproveClick: () -> Unit, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = subtitle.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24), + ) + + SpacerH16() + + ApprovalInfo( + currency = currency, + approveType = approveType, + approveItems = approveItems, + onChangeApproveType = onChangeApproveType, + onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) + + SpacerH(height = TangemTheme.dimens.spacing20) + + PrimaryButtonIconEnd( + text = stringResourceSafe(id = CommonUiR.string.common_approve), + iconResId = walletInteractionIcon, + showProgress = isApproveLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onApproveClick, + enabled = isApproveEnabled, + ) + + SpacerH12() + + SecondaryButton( + text = stringResourceSafe(id = CommonUiR.string.common_cancel), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onCancelClick, + ) + + SpacerH16() + } +} + +@Suppress("LongParameterList") +@Composable +private fun ApprovalInfo( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + onOpenLearnMoreAboutApproveClick: () -> Unit, + feeSelectorBlockComponent: FeeSelectorBlockComponent, +) { + FooterContainer( + footer = annotatedReference { + append(stringResourceSafe(CommonUiR.string.swap_approve_description)) + append(" ") + withLink( + link = LinkAnnotation.Clickable( + tag = "APPROVE_TAG", + linkInteractionListener = { onOpenLearnMoreAboutApproveClick() }, + ), + block = { + appendColored( + text = stringResourceSafe(CommonUiR.string.common_learn_more), + color = TangemTheme.colors.text.accent, + ) + }, + ) + }, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + AmountItem( + currency = currency, + approveType = approveType, + onChangeApproveType = onChangeApproveType, + approveItems = approveItems, + ) + } + SpacerH16() + FooterContainer( + footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + } +} + +@Composable +private fun AmountItem( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, +) { + var isExpandSelector by remember { mutableStateOf(false) } + var amountSize by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = { isExpandSelector = true }, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { amountSize = it } + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + SpacerWMax() + Text( + text = approveType.text.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + } + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { type -> + isExpandSelector = false + onChangeApproveType(type) + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun DropdownSelector( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, + selectedType: ApproveType, + amountSize: IntSize, +) { + var dropDownWidth by remember { mutableStateOf(IntSize.Zero) } + val offsetY = amountSize.height.times(-1) + val offsetX = amountSize.width - dropDownWidth.width + + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action), + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)), + ) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + properties = PopupProperties(clippingEnabled = false), + offset = with(LocalDensity.current) { + DpOffset(x = offsetX.toDp(), y = offsetY.toDp()) + }, + modifier = Modifier + .wrapContentSize() + .background(TangemTheme.colors.background.action) + .onSizeChanged { dropDownWidth = it }, + ) { + items.forEach { item -> + val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent + + DropdownMenuItem( + modifier = Modifier.fillMaxWidth(), + text = { + Row { + Text( + text = when (item) { + ApproveType.LIMITED -> stringResourceSafe( + id = CommonUiR.string.give_permission_current_transaction, + ) + ApproveType.UNLIMITED -> stringResourceSafe( + id = CommonUiR.string.give_permission_unlimited, + ) + }, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + SpacerWMax() + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24), + ), + tint = color, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.size20), + ) + } + }, + onClick = { + onItemClick.invoke(item) + }, + ) + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun GiveApprovalContentPreview( + @PreviewParameter(GiveApprovalContentPreviewProvider::class) params: GiveApprovalPreviewParams, +) { + TangemThemePreview { + GiveApprovalContent( + currency = params.currency, + subtitle = params.subtitle, + approveType = params.approveType, + approveItems = params.approveItems, + onChangeApproveType = {}, + walletInteractionIcon = params.walletInteractionIcon, + isApproveEnabled = params.isApproveEnabled, + isApproveLoading = params.isApproveLoading, + onApproveClick = {}, + onCancelClick = {}, + onOpenLearnMoreAboutApproveClick = {}, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + ) + } +} + +private data class GiveApprovalPreviewParams( + val currency: String, + val subtitle: TextReference, + val approveType: ApproveType, + val approveItems: ImmutableList, + val walletInteractionIcon: Int?, + val isApproveEnabled: Boolean, + val isApproveLoading: Boolean, +) + +private class GiveApprovalContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + GiveApprovalPreviewParams( + currency = "USDT", + subtitle = stringReference("Allow this app to access your USDT"), + approveType = ApproveType.LIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + walletInteractionIcon = CommonUiR.drawable.ic_tangem_24, + isApproveEnabled = true, + isApproveLoading = false, + ), + GiveApprovalPreviewParams( + currency = "USDC", + subtitle = stringReference("Allow this app to access your USDC"), + approveType = ApproveType.UNLIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + walletInteractionIcon = CommonUiR.drawable.ic_tangem_24, + isApproveEnabled = false, + isApproveLoading = true, + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..a08d796627 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt @@ -0,0 +1,15 @@ +package com.tangem.features.approval.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM + +internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { + override fun updateState(feeSelectorUM: FeeSelectorUM) { + } + + @Composable + override fun Content(modifier: Modifier) { + } +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt index 72d5656bad..1513ec7932 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt @@ -1,6 +1,5 @@ package com.tangem.features.feed.entry.featuretoggle interface FeedFeatureToggle { - val isFeedEnabled: Boolean val isEarnBlockEnabled: Boolean } \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index beb8db8937..df759d55a1 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.domain.feedback.models) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) + implementation(projects.domain.offramp) implementation(projects.domain.onramp.models) implementation(projects.domain.staking.models) implementation(projects.domain.tokens) @@ -56,12 +57,6 @@ dependencies { implementation(projects.domain.yieldSupply) implementation(projects.domain.earn) - // FIXME [REDACTED_TASK_KEY] - // Remove the "Buy" and "Sell" actions from the redux middleware. - // Instead, create some kind of interface for such cases. - /* Redux -_- */ - implementation(projects.domain.legacy) - implementation(deps.reKotlin) /* Compose */ implementation(deps.compose.coil) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt index 7bfe5544bd..3f6071b417 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt @@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage @@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor( private val router: Router, private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, @Assisted private val currentAppCurrency: Provider, @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor( } private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt index 38b3458572..3e15b106b0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt @@ -7,9 +7,6 @@ internal class DefaultFeedFeatureToggle( private val featureTogglesManager: FeatureTogglesManager, ) : FeedFeatureToggle { - override val isFeedEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED") - override val isEarnBlockEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED") } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt index 245f6b84c7..6a6b8580cc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.converter -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index bb91b6a704..8622c63f4b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -32,12 +32,10 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter import com.tangem.features.feed.model.earn.state.EarnStateController -import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.* import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM @@ -69,8 +67,8 @@ internal class EarnModel @Inject constructor( private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) private val earnListConfigProvider = Provider { createEarnTokensListConfig( - selectedTypeFilter = stateController.value.selectedTypeFilter, - selectedNetworkFilter = stateController.value.selectedNetworkFilter, + selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter, + selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter, earnNetworks = earnNetworks.value, ) } @@ -104,7 +102,6 @@ internal class EarnModel @Inject constructor( init { updateInitialState() fetchEarnNetworks() - fetchTopEarnTokens() subscribeOnStoredFilters() subscribeOnNetworks() subscribeOnBatchFlow() @@ -117,19 +114,22 @@ internal class EarnModel @Inject constructor( batchFlowManager.initialLoadingError, batchFlowManager.paginationStatus, ) { items, error, paginationStatus -> - val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || - state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks - error?.let(::handleBestOpportunitiesErrorAnalytics) + val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All || + state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks EarnListStateManager.calculateState( items = items, error = error, paginationStatus = paginationStatus, hasActiveFilters = hasActiveFilters, - onRetryClick = { batchFlowManager.reload() }, + onRetryClick = { + batchFlowManager.reload() + reloadEarnNetworks() + }, onLoadMore = { batchFlowManager.loadMore() }, onClearFiltersClick = ::onClearFiltersClick, - ) - }.onEach { bestOpportunitiesState -> + ) to error + }.onEach { (bestOpportunitiesState, error) -> + error?.let(::handleBestOpportunitiesErrorAnalytics) stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) }.launchIn(modelScope) } @@ -156,23 +156,27 @@ internal class EarnModel @Inject constructor( private fun subscribeOnStoredFilters() { modelScope.launch(dispatchers.default) { - getEarnFilterUseCase() - .collect { filter -> - val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) - val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) - stateController.update( - EarnFilterSelectedStateTransformer( - filterType = typeFilterUM, - filterNetwork = networkFilterUM, - ), - ) - batchFlowManager.reload() - } + combine( + getEarnFilterUseCase(), + earnNetworks, + ) { filter, networks -> + val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) + val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) + stateController.update( + EarnFilterSelectedStateTransformer( + filterType = typeFilterUM, + filterNetwork = networkFilterUM, + earnNetworks = networks, + ), + ) + batchFlowManager.reload() + }.collect() } } private fun fetchTopEarnTokens() { modelScope.launch(dispatchers.default) { + stateController.update(UpdateMostlyUsedStateLoadingTransformer()) fetchTopEarnTokensUseCase() } } @@ -183,13 +187,21 @@ internal class EarnModel @Inject constructor( } } + private fun reloadEarnNetworks() { + modelScope.launch(dispatchers.default) { + if (earnNetworks.value.isLeft()) { + fetchEarnNetworks() + } + } + } + /* start of clicks area */ private fun onTypeFilterClick() { val currentState = state.value bottomSheetNavigation.activate( FeedBottomSheetRoute.TypeFilter( params = EarnTypeFilterComponent.Params( - selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter), + selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter), onFilterSelected = ::onTypeFilterOptionSelected, onDismiss = { bottomSheetNavigation.dismiss() }, ), @@ -210,7 +222,7 @@ internal class EarnModel @Inject constructor( } private fun createNetworkFilters(): List { - val selectedFilter = state.value.selectedNetworkFilter + val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter return buildList { add( EarnFilterNetwork.AllNetworks( @@ -277,11 +289,14 @@ internal class EarnModel @Inject constructor( modelScope.launch(dispatchers.default) { setEarnFilterUseCase( EarnFilter( - earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter), + earnFilterNetwork = EarnFilterNetworkUMConverter().convert( + value = state.value.earnFilterUM.selectedNetworkFilter, + ), earnFilterType = type, ), ) bottomSheetNavigation.dismiss() + reloadEarnNetworks() } } @@ -290,7 +305,7 @@ internal class EarnModel @Inject constructor( setEarnFilterUseCase( EarnFilter( earnFilterNetwork = filter, - earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter), + earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter), ), ) } @@ -319,11 +334,13 @@ internal class EarnModel @Inject constructor( is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty() else -> null to "" } - analyticsEventHandler.send( - EarnAnalyticsEvent.BestOpportunitiesLoadError( - code = code, - message = message, - ), - ) + if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) { + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesLoadError( + code = code, + message = message, + ), + ) + } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt index 08845ff277..68a5a99bfe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt @@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig( earnNetworks.fold( ifLeft = { null }, ifRight = { networks -> - networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId) + networks.filter(EarnNetwork::isAdded) + .map(EarnNetwork::networkId) + .ifEmpty { listOf(NO_ONE_NETWORK) } }, ) } @@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig( networks = networks, isForEarn = isForEarn, ) -} \ No newline at end of file +} + +/** + * This id means that backend has to return empty result + */ +private const val NO_ONE_NETWORK = "-1" \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt index c608bc31c7..3ff24b4467 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt @@ -26,7 +26,7 @@ internal sealed class EarnAnalyticsEvent( event = "Best Opportunities Filter Network Applied", params = mapOf( "Network Filter Type" to filterType.value, - "NetworkId" to networkId.orEmpty(), + "Network Id" to networkId.orEmpty(), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index 6cec5cd393..f074cbdc66 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() { return EarnUM( mostlyUsed = EarnListUM.Loading, bestOpportunities = EarnBestOpportunitiesUM.Loading, - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + earnFilterUM = EarnFilterUM( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + ), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt index 9bdb205b49..3258d8f251 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt @@ -1,18 +1,24 @@ package com.tangem.features.feed.model.earn.state.transformers +import com.tangem.domain.models.earn.EarnNetworks import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM internal class EarnFilterSelectedStateTransformer( + private val earnNetworks: EarnNetworks, private val filterType: EarnFilterTypeUM, private val filterNetwork: EarnFilterNetworkUM, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { return prevState.copy( - selectedTypeFilter = filterType, - selectedNetworkFilter = filterNetwork, + earnFilterUM = prevState.earnFilterUM.copy( + selectedTypeFilter = filterType, + selectedNetworkFilter = filterNetwork, + isNetworkFilterEnabled = earnNetworks.isRight(), + isTypeFilterEnabled = true, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt deleted file mode 100644 index a4ae7833a3..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.feed.model.earn.state.transformers - -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnUM - -internal class EarnNetworkFilterSelectedStateTransformer( - private val filter: EarnFilterNetworkUM, -) : EarnUMTransformer { - - override fun transform(prevState: EarnUM): EarnUM { - return prevState.copy(selectedNetworkFilter = filter) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt new file mode 100644 index 0000000000..e1fcb79a35 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(mostlyUsed = EarnListUM.Loading) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt index 6b9886c03f..eb6341a0c7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.news.list.statemanager -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.news.model.NewsListConfig diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt index e16a20e9f2..9c244106fa 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.news.list.statemanager -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 0b7845919d..fb0a94723b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -3,7 +3,6 @@ package com.tangem.features.feed.ui.earn import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material3.Icon @@ -14,21 +13,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.layout.onFirstVisible import androidx.compose.ui.platform.LocalDensity 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.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -39,6 +38,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder import com.tangem.features.feed.ui.earn.components.EarnListItem +import com.tangem.features.feed.ui.earn.components.MostlyUsedCard import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf @@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { SpacerH(12.dp) BestOpportunitiesFilters( state = state.bestOpportunities, - selectedNetworkFilterText = when (state.selectedNetworkFilter) { - is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) - is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) - is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text) - }, - selectedTypeFilterText = state.selectedTypeFilterText, + earnFilterUM = state.earnFilterUM, onNetworkFilterClick = state.onNetworkFilterClick, onTypeFilterClick = state.onTypeFilterClick, ) @@ -151,10 +146,12 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 12.dp, - ), + .padding(horizontal = 16.dp, vertical = 12.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(vertical = 32.dp, horizontal = 12.dp), contentAlignment = Alignment.Center, ) { UnableToLoadData(onRetryClick = st.onRetryClicked) @@ -164,71 +161,17 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { } } -@Composable -private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .width(148.dp) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .clickable(onClick = onClick) - .padding(12.dp), - ) { - CurrencyIcon( - modifier = Modifier.size(32.dp), - state = item.currencyIconState, - shouldDisplayNetwork = true, - networkBadgeSize = 12.dp, - networkBadgeBackground = TangemTheme.colors.background.action, - ) - - SpacerH(8.dp) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(weight = 1f, fill = false), - text = item.tokenName.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW(4.dp) - Text( - text = item.symbol.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - ) - } - - SpacerH(2.dp) - - Text( - text = item.earnValue.resolveReference(), - color = TangemTheme.colors.text.accent, - style = TangemTheme.typography.caption1, - maxLines = 1, - ) - } -} - @Composable private fun BestOpportunitiesFilters( state: EarnBestOpportunitiesUM, - selectedNetworkFilterText: TextReference, - selectedTypeFilterText: TextReference, + earnFilterUM: EarnFilterUM, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, ) { when (state) { is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer() else -> FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered, + earnFilterUM = earnFilterUM, onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, ) @@ -307,9 +250,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) @Composable private fun FilterButtons( - selectedNetworkFilterText: TextReference, - selectedTypeFilterText: TextReference, - isEnabled: Boolean, + earnFilterUM: EarnFilterUM, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, modifier: Modifier = Modifier, @@ -319,10 +260,14 @@ private fun FilterButtons( ) { SecondarySmallButton( config = SmallButtonConfig( - text = selectedNetworkFilterText, + text = when (earnFilterUM.selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) + is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) + is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text) + }, onClick = onNetworkFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - isEnabled = isEnabled, + isEnabled = earnFilterUM.isNetworkFilterEnabled, ), ) @@ -330,10 +275,10 @@ private fun FilterButtons( SecondarySmallButton( config = SmallButtonConfig( - text = selectedTypeFilterText, + text = earnFilterUM.selectedTypeFilter.text, onClick = onTypeFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - isEnabled = isEnabled, + isEnabled = earnFilterUM.isTypeFilterEnabled, ), ) } @@ -496,7 +441,7 @@ private fun EarnContentLoadingPreview() { ) { EarnContent( state = previewEarnUM( - mostlyUsed = EarnListUM.Loading, + mostlyUsed = EarnListUM.Error(onRetryClicked = {}), bestOpportunities = EarnBestOpportunitiesUM.Loading, ), ) @@ -588,8 +533,12 @@ private fun previewEarnUM( ): EarnUM = EarnUM( mostlyUsed = mostlyUsed, bestOpportunities = bestOpportunities, - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + earnFilterUM = EarnFilterUM( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + isTypeFilterEnabled = true, + isNetworkFilterEnabled = true, + ), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt new file mode 100644 index 0000000000..21fdbcc0a4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt @@ -0,0 +1,198 @@ +package com.tangem.features.feed.ui.earn.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.ds.opportunities.OpportunitiesBG +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.* +import com.tangem.features.feed.ui.earn.state.EarnListItemUM + +@Composable +internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + val isRedesignEnabled = LocalRedesignEnabled.current + + if (isRedesignEnabled) { + MostlyUsedCardV2( + modifier = modifier, + item = item, + onClick = onClick, + ) + } else { + MostlyUsedCardV1( + modifier = modifier, + item = item, + onClick = onClick, + ) + } +} + +@Composable +private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + OpportunitiesBG( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = onClick), + icon = TangemIconUM.Currency(item.currencyIconState), + ) { + Column(modifier = Modifier.padding(12.dp)) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(22.dp) + + Row( + verticalAlignment = Alignment.Bottom, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography2.bodySemibold16, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors2.text.status.positive, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + } +} + +@Composable +private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(onClick = onClick) + .padding(12.dp), + ) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(8.dp) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV1() { + TangemThemePreview { + MostlyUsedCardV1( + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + onClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV2() { + TangemThemePreviewRedesign { + MostlyUsedCardV2( + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + onClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt new file mode 100644 index 0000000000..3cf3b0e582 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class EarnFilterUM( + val selectedTypeFilter: EarnFilterTypeUM, + val selectedNetworkFilter: EarnFilterNetworkUM, + val isTypeFilterEnabled: Boolean = true, + val isNetworkFilterEnabled: Boolean = true, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index a11f66f48e..974b5a71b2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -1,20 +1,14 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference @Immutable internal data class EarnUM( val mostlyUsed: EarnListUM, val bestOpportunities: EarnBestOpportunitiesUM, - val selectedTypeFilter: EarnFilterTypeUM, - val selectedNetworkFilter: EarnFilterNetworkUM, + val earnFilterUM: EarnFilterUM, val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, val onSliderScroll: () -> Unit, -) { - - val selectedTypeFilterText: TextReference - get() = selectedTypeFilter.text -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt index c43c6b6a12..9fd9997c5b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.feed.components import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -8,12 +9,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.extensions.TextReference @Composable -internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) { +internal fun Header( + onSeeAllClick: () -> Unit, + isLoading: Boolean, + shouldShowSeeAll: Boolean, + title: @Composable () -> Unit, +) { AnimatedContent(isLoading) { animatedState -> Row( modifier = Modifier @@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title if (animatedState) { RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp)) } else { - title() - SecondarySmallButton( - config = SmallButtonConfig( - text = TextReference.Res(R.string.common_see_all), - onClick = onSeeAllClick, - ), - ) + Box(modifier = Modifier.weight(1f)) { + title() + } + SpacerW(8.dp) + AnimatedVisibility(shouldShowSeeAll) { + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.common_see_all), + onClick = onSeeAllClick, + ), + ) + } } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt index fb70686941..08e6849b20 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R @@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif text = stringResourceSafe(R.string.markets_earn_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = onSeeAllClick, isLoading = earnListUM is EarnListUM.Loading, + shouldShowSeeAll = earnListUM is EarnListUM.Content, ) SpacerH(12.dp) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt index 1c2798cbf8..73294fe9f8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt @@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.common.ui.markets.MarketsListItemPlaceholder -import com.tangem.common.ui.news.DefaultLoadingArticle -import com.tangem.common.ui.news.TrendingLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt index 1aa00e6976..11360bacd7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -4,12 +4,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.common.ui.markets.MarketsListItem @@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis text = stringResourceSafe(R.string.markets_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, + shouldShowSeeAll = currentChart is MarketChartUM.Content, + isLoading = currentChart is MarketChartUM.Loading, ) SpacerH(12.dp) @@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall text = stringResourceSafe(R.string.markets_pulse_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = { onSeeAllClick() }, + shouldShowSeeAll = true, + isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading, ) LazyRow( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt index e2fa4ecbf6..4601283941 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -2,28 +2,26 @@ package com.tangem.features.feed.ui.feed.components import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.ShowMoreArticlesCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW @@ -31,16 +29,18 @@ import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.feed.ui.feed.state.FeedListCallbacks -import com.tangem.features.feed.ui.feed.state.NewsUM -import com.tangem.features.feed.ui.feed.state.NewsUMState +import com.tangem.features.feed.ui.feed.state.* -private const val FOURTH_ITEM_INDEX = 3 +internal const val FOURTH_ITEM_INDEX = 3 private const val GRADIENT_START = 0f private const val GRADIENT_END = 0.5f -private val LinearGradientFirstPart = Color(0xFF635EEC) -private val LinearGradientSecondPart = Color(0xFFE05AED) +private const val LINEAR_GRADIENT_FIRST_PART_V2 = 0xFFA3A0FF +private const val LINEAR_GRADIENT_SECOND_PART_V2 = 0xFFF79DFF + +private const val LINEAR_GRADIENT_FIRST_PART_V1 = 0xFF635EEC +private const val LINEAR_GRADIENT_SECOND_PART_V1 = 0xFFE05AED @Composable internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { @@ -64,13 +64,23 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend @Suppress("LongMethod") @Composable private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { - val listState = rememberLazyListState() - val articlesReadStatus = remember(news.content) { - news.content.map { it.isViewed } + val isRedesignEnabled = LocalRedesignEnabled.current + val gradientStart = remember(isRedesignEnabled) { + if (isRedesignEnabled) { + Color(LINEAR_GRADIENT_FIRST_PART_V2) + } else { + Color(LINEAR_GRADIENT_FIRST_PART_V1) + } } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) + + val gradientEnd = remember(isRedesignEnabled) { + if (isRedesignEnabled) { + Color(LINEAR_GRADIENT_SECOND_PART_V2) + } else { + Color(LINEAR_GRADIENT_SECOND_PART_V1) + } } + Column { Header( title = { @@ -95,8 +105,8 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, withStyle( SpanStyle().copy( brush = Brush.linearGradient( - GRADIENT_START to LinearGradientFirstPart, - GRADIENT_END to LinearGradientSecondPart, + GRADIENT_START to gradientStart, + GRADIENT_END to gradientEnd, ), ), ) { @@ -104,10 +114,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, } }, style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } }, onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) }, + isLoading = news.newsUMState == NewsUMState.LOADING, + shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT, ) SpacerH(12.dp) @@ -125,48 +139,19 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, } } - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = listState, - ) { - itemsIndexed( - items = news.content, - key = { _, article -> article.id }, - contentType = { _, _ -> "article" }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = feedListCallbacks.onSliderScroll, - ) - } else { - Modifier - } - ArticleCard( - articleConfigUM = article, - onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } + NewsSlider( + NewsSliderConfig( + callbacks = NewsSliderCallbacks( + onOpenAllNews = { feedListCallbacks.onOpenAllNews(true) }, + onSliderScroll = feedListCallbacks.onSliderScroll, + onSliderEndReached = feedListCallbacks.onSliderEndReached, + onArticleClick = feedListCallbacks.onArticleClick, + ), + content = news.content, + shouldShowSeeAllNewsItem = true, + ), + ) - item(contentType = "show_more") { - ShowMoreArticlesCard( - modifier = Modifier - .width(216.dp) - .heightIn(min = 164.dp) - .onFirstVisible( - minFractionVisible = 0.5f, - callback = feedListCallbacks.onSliderEndReached, - ), - onClick = { feedListCallbacks.onOpenAllNews(true) }, - ) - } - } SpacerH(32.dp) } } @@ -181,10 +166,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) { text = stringResourceSafe(R.string.common_news), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } }, onSeeAllClick = {}, + shouldShowSeeAll = false, + isLoading = false, ) SpacerH(12.dp) BlockCard( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt new file mode 100644 index 0000000000..74d1b42fd9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +@Composable +internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) { + val isRedesignEnabled = LocalRedesignEnabled.current + if (isRedesignEnabled) { + NewsSliderV2(newsSliderConfig) + } else { + NewsSliderV1(newsSliderConfig) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt new file mode 100644 index 0000000000..e035dcc7a4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt @@ -0,0 +1,67 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.unit.dp +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +@Composable +internal fun NewsSliderV1(newsSliderConfig: NewsSliderConfig) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + itemsIndexed( + items = newsSliderConfig.content, + key = { index, _ -> index }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = if (index == FOURTH_ITEM_INDEX) { + Modifier.onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderScroll, + ) + } else { + Modifier + } + ArticleCard( + articleConfigUM = article, + onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, + modifier = articleModifier + .heightIn(min = 164.dp) + .width(216.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) + } + + if (newsSliderConfig.shouldShowSeeAllNewsItem) { + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .width(216.dp) + .heightIn(min = 164.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderEndReached, + ), + onClick = newsSliderConfig.callbacks.onOpenAllNews, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt new file mode 100644 index 0000000000..f27b0a8b1c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt @@ -0,0 +1,112 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +private val dividerSpacerWidth = 20.dp + +@Suppress("MagicNumber", "LongMethod") +@Composable +internal fun NewsSliderV2(newsSliderConfig: NewsSliderConfig) { + val density = LocalDensity.current + val dividerWidthPx = with(density) { 1.dp.roundToPx() } + val spacerWidthPx = with(density) { dividerSpacerWidth.roundToPx() } + + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + state = rememberLazyListState(), + ) { + itemsIndexed( + items = newsSliderConfig.content, + key = { index, _ -> index }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = Modifier.conditional( + condition = index == FOURTH_ITEM_INDEX, + modifier = { + onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderScroll, + ) + }, + ) + + val shouldShowDivider = newsSliderConfig.shouldShowSeeAllNewsItem || + index < newsSliderConfig.content.size - 1 + + // have to use layout cause LazyRow has not fixed height and divider can not be measured + Layout( + modifier = Modifier, + content = { + ArticleCard( + articleConfigUM = article, + onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, + modifier = articleModifier + .fillMaxHeight() + .width(220.dp), + ) + Spacer(modifier = Modifier.width(dividerSpacerWidth)) + Box( + modifier = Modifier + .width(1.dp) + .background(TangemTheme.colors2.border.neutral.secondary), + ) + Spacer(modifier = Modifier.width(dividerSpacerWidth)) + }, + ) { measurables, constraints -> + val cardPlaceable = measurables[0].measure(constraints) + val height = cardPlaceable.height + + if (shouldShowDivider) { + val leftSpacer = measurables[1].measure(Constraints.fixed(spacerWidthPx, height)) + val divider = measurables[2].measure(Constraints.fixed(dividerWidthPx, height)) + val rightSpacer = measurables[3].measure(Constraints.fixed(spacerWidthPx, height)) + val totalWidth = cardPlaceable.width + leftSpacer.width + divider.width + rightSpacer.width + + layout(totalWidth, height) { + cardPlaceable.place(0, 0) + leftSpacer.place(cardPlaceable.width, 0) + divider.place(cardPlaceable.width + leftSpacer.width, 0) + rightSpacer.place(cardPlaceable.width + leftSpacer.width + divider.width, 0) + } + } else { + layout(cardPlaceable.width, height) { + cardPlaceable.place(0, 0) + } + } + } + } + + if (newsSliderConfig.shouldShowSeeAllNewsItem) { + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .fillMaxHeight() + .width(216.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderEndReached, + ), + onClick = newsSliderConfig.callbacks.onOpenAllNews, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt new file mode 100644 index 0000000000..53e0457e5a --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt @@ -0,0 +1,32 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.material3.CardColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.LocalRedesignEnabled + +@Composable +fun ArticleCard( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, + colors: CardColors = TangemBlockCardColors, +) { + val isRedesignEnabled = LocalRedesignEnabled.current + + if (isRedesignEnabled) { + ArticleCardV2( + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + modifier = modifier, + ) + } else { + ArticleCardV1( + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + modifier = modifier, + colors = colors, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt index a6a7a2a078..97ede1fbdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import android.content.res.Configuration import androidx.compose.foundation.* @@ -37,7 +37,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @Composable -fun ArticleCard( +internal fun ArticleCardV1( articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier, @@ -126,7 +126,7 @@ private fun TrendingArticle( } @Composable -fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) { +internal fun ShowMoreArticlesCardV1(modifier: Modifier = Modifier, onClick: () -> Unit) { BlockCard( modifier = modifier, onClick = onClick, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt new file mode 100644 index 0000000000..a47843225f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt @@ -0,0 +1,382 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import android.content.res.Configuration +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_NO +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet + +@Composable +internal fun ArticleCardV2( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (articleConfigUM.isTrending) { + TrendingArticle( + modifier = modifier, + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + ) + } else { + DefaultArticle( + modifier = modifier, + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + ) + } +} + +@Composable +private fun TrendingArticle( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TrendingArticleBackground( + modifier = modifier, + onClick = onArticleClick, + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.Start, + ) { + DayAndRatingInfo(rating = stringReference("${articleConfigUM.score}")) + + SpacerH(8.dp) + + Text( + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + }, + style = TangemTheme.typography2.headingSemibold20, + textAlign = TextAlign.Start, + ) + + SpacerH(18.dp) + + Text( + text = articleConfigUM.createdAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + SpacerH(18.dp) + + Tags(tags = articleConfigUM.tags.toImmutableList()) + } + } +} + +@Composable +internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(20.dp)) + .background(color = TangemTheme.colors2.surface.level3) + .clickable(onClick = onClick) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.primary, + shape = RoundedCornerShape(20.dp), + ) + .padding(vertical = 41.dp, horizontal = 16.dp), + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48), + contentDescription = stringResourceSafe(R.string.common_show_more), + ) + + SpacerH(10.dp) + + Text( + text = stringResourceSafe(R.string.news_all_news), + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, + ) + + Text( + text = stringResourceSafe(R.string.news_stay_in_the_loop), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + ) + } +} + +@Composable +private fun DefaultArticle( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors2.surface.level2) + .clickable { onArticleClick() } + .padding(vertical = 16.dp, horizontal = 10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + RatingInfo(rating = stringReference("${articleConfigUM.score}")) + } + + SpacerH(8.dp) + + Text( + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + }, + style = TangemTheme.typography2.bodyRegular16, + minLines = 3, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + + Text( + modifier = Modifier.padding(vertical = 20.dp), + text = articleConfigUM.createdAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Tags(tags = articleConfigUM.tags.toImmutableList()) + } +} + +@Suppress("MagicNumber") +@Composable +private fun TrendingArticleBackground( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val isDarkTheme = LocalIsInDarkTheme.current + + val bgColor = remember(isDarkTheme) { + if (isDarkTheme) { + Color(TRENDING_NIGHT_BG) + } else { + Color(TRENDING_LIGHT_BG) + } + } + + Box( + modifier = modifier + .clip(RoundedCornerShape(20.dp)) + .drawBehind { + drawRect(bgColor) + + val w = size.width + val h = size.height + val radiusScale = (w + h) / 2f + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF7C16F1).copy(alpha = .8f), + Color.Transparent, + ), + center = Offset(w / 2f, 2.4f * h), + radius = radiusScale * 1.57f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF3360FF).copy(alpha = .7f), + Color.Transparent, + ), + center = Offset(w / 2f, 2.95f * h), + radius = radiusScale * 1.9f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFF9408).copy(alpha = .45f), + Color.Transparent, + ), + center = Offset(-0.38f * w, 2.1f * h), + radius = radiusScale * 1.41f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFC2424).copy(alpha = .5f), + Color.Transparent, + ), + center = Offset(1.188f * w, 2.37f * h), + radius = radiusScale * 1.41f, + ), + ) + } + .border(width = 1.dp, color = bgColor.copy(.1f)) + .clickable(onClick = onClick), + content = content, + ) +} + +@Composable +private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + RatingInfo(rating) + + SpacerW(8.dp) + + Text( + text = stringResourceSafe(R.string.feed_trending_now), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.primary, + ) + } +} + +@Composable +private fun RatingInfo(rating: TextReference) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16), + tint = TangemTheme.colors2.fill.status.attention, + contentDescription = null, + ) + + SpacerW(2.dp) + + Text( + text = rating.resolveReference(), + color = TangemTheme.colors2.text.status.attention, + style = TangemTheme.typography2.captionSemibold12, + ) +} + +private const val TRENDING_NIGHT_BG = 0xFF1F1F1F +private const val TRENDING_LIGHT_BG = 0xFFFFFFFF + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TagsPreview() { + TangemThemePreviewRedesign { + Tags( + tags = persistentListOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), + ), + ) + } +} + +@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_YES) +@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_NO) +@Composable +private fun ArticleCardsPreview() { + val tags = listOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Breaking news")), + ).toImmutableSet() + + val config = ArticleConfigUM( + id = 1, + title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", + score = 9.5f, + createdAt = TextReference.Str("1h ago"), + isTrending = true, + tags = tags, + isViewed = false, + ) + + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + ) { + ArticleCardV2( + articleConfigUM = config, + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isViewed = true), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isTrending = false), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isTrending = false, isViewed = true), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ShowMoreArticlesCardV2(onClick = {}) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt similarity index 76% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt index 46714a0ba8..fde217b1de 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt @@ -1,9 +1,11 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableSet +@Immutable data class ArticleConfigUM( val id: Int, val title: String, diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt similarity index 96% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt index 15bac87bdc..fcb608cf65 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt index a2834e2afb..656b128605 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt similarity index 98% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt index 300d41f104..1d5a646dda 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt new file mode 100644 index 0000000000..fba589144d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.LocalRedesignEnabled + +@Composable +fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) { + val isRedesignEnabled: Boolean = LocalRedesignEnabled.current + if (isRedesignEnabled) { + ShowMoreArticlesCardV2(modifier = modifier, onClick = onClick) + } else { + ShowMoreArticlesCardV1(modifier = modifier, onClick = onClick) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt similarity index 98% rename from common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt index a163c8ee15..8508ef89a9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index ceaf16a400..59635e4fe6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 8d8a014175..956cfde84f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.extensions.TextReference import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.earn.state.EarnListUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt new file mode 100644 index 0000000000..f61adb2d56 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.ui.feed.state + +import androidx.compose.runtime.Immutable +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class NewsSliderConfig( + val shouldShowSeeAllNewsItem: Boolean, + val content: ImmutableList, + val callbacks: NewsSliderCallbacks, +) + +@Immutable +internal data class NewsSliderCallbacks( + val onOpenAllNews: () -> Unit, + val onSliderScroll: () -> Unit, + val onSliderEndReached: () -> Unit, + val onArticleClick: (id: Int) -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index ff3047cac8..01d5c21f9d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -2,30 +2,24 @@ package com.tangem.features.feed.ui.market.detailed.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.items.DescriptionPlaceholder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.feed.components.NewsSlider +import com.tangem.features.feed.ui.feed.state.NewsSliderCallbacks +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews -private const val FOURTH_ITEM_INDEX = 3 - @Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA] internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, @@ -207,13 +201,6 @@ private fun LazyListScope.loadingInfoBlocks() { private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { item("related-news") { - val listState = rememberLazyListState() - val articlesReadStatus = remember(relatedNews.articles) { - relatedNews.articles.map { it.isViewed } - } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) - } Column( modifier = Modifier .fillMaxWidth() @@ -231,35 +218,18 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { color = TangemTheme.colors.text.primary1, ) - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = listState, - ) { - itemsIndexed( - items = relatedNews.articles, - key = { index, article -> article.id }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = relatedNews.onScroll, - ) - } else { - Modifier - } - - ArticleCard( - articleConfigUM = article, - onArticleClick = { relatedNews.onArticledClicked(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } - } + NewsSlider( + NewsSliderConfig( + callbacks = NewsSliderCallbacks( + onOpenAllNews = {}, // not applicable here + onSliderScroll = relatedNews.onScroll, + onSliderEndReached = {}, // not applicable here + onArticleClick = relatedNews.onArticledClicked, + ), + content = relatedNews.articles, + shouldShowSeeAllNewsItem = false, + ), + ) } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index d0c4775d82..5f21370028 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.market.detailed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index 41d152a94b..fd67175055 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.CachePolicy import coil.request.ImageRequest -import com.tangem.common.ui.news.ArticleHeader +import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt index 5dd1413007..61ae51398f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.chip.Chip import com.tangem.core.ui.components.chip.entity.ChipUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt index 465c76d0f9..8d41f60ea3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt @@ -16,9 +16,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.DefaultLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.block.TangemBlockCardColors diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt index 12fe9861f3..9d8c0e32b1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.feed.ui.news.list.state import androidx.compose.runtime.Immutable -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.chip.entity.ChipUM import kotlinx.collections.immutable.ImmutableList diff --git a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt index b6965fc720..a841936911 100644 --- a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt +++ b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt @@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject /** * Mocking it for release/external builds to exclude SumSub dependency - * This will never be called if the FT [isTangemPayEnabled] is off */ @Suppress("UnusedPrivateProperty") internal class MockKycComponent @AssistedInject constructor( diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt deleted file mode 100644 index 0136e2532f..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.features.markets.details - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import kotlinx.serialization.Serializable - -@Stable -interface MarketsTokenDetailsComponent : ComposableContentComponent { - - @Serializable - data class Params( - val token: TokenMarketParams, - val appCurrency: AppCurrency, - val shouldShowPortfolio: Boolean, - val analyticsParams: AnalyticsParams?, - ) - - @Serializable - data class AnalyticsParams( - val blockchain: String?, - val source: String, - ) - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt deleted file mode 100644 index 05a7e68670..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.markets.entry - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState - -@Stable -interface MarketsEntryComponent { - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory { - fun create(context: AppComponentContext): MarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 1928a996ed..86304b9fd5 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.domain.feedback.models) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) + implementation(projects.domain.offramp) implementation(projects.domain.onramp.models) implementation(projects.domain.staking.models) implementation(projects.domain.staking) @@ -49,12 +50,6 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply) - // FIXME [REDACTED_TASK_KEY] - // Remove the "Buy" and "Sell" actions from the redux middleware. - // Instead, create some kind of interface for such cases. - /* Redux -_- */ - implementation(projects.domain.legacy) - implementation(deps.reKotlin) /* Compose */ implementation(deps.compose.coil) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt deleted file mode 100644 index e6f564100f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.features.markets.details.impl - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.lifecycle.compose.LifecycleStartEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.child -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params -import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent -import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel -import com.tangem.features.markets.details.impl.model.state.TokenNetworksState -import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -@Stable -internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted params: Params, - analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, - portfolioComponentFactory: MarketsPortfolioComponent.Factory, -) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { - - // applying l2 compatibility - private val updatedParams = params.copy( - token = params.token.copy( - id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)), - ), - ) - private val analyticsParams = params.analyticsParams - - private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) - - private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) { - portfolioComponentFactory.create( - context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params( - updatedParams.token, - analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) }, - ), - ) - } else { - null - } - - init { - componentScope.launch { - model.networksState.collectLatest { networksState -> - when (networksState) { - is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks( - networksState.networks, - ) - TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable() - else -> {} - } - } - } - - // === Analytics === - if (analyticsParams != null) { - analyticsEventHandler.send( - MarketDetailsAnalyticsEvent.EventBuilder( - token = params.token, - ).screenOpened( - blockchain = analyticsParams.blockchain, - source = analyticsParams.source, - ), - ) - } - } - - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - LifecycleStartEffect(Unit) { - model.isVisibleOnScreen.value = true - onStopOrDispose { - model.isVisibleOnScreen.value = false - } - } - - val state by model.state.collectAsStateWithLifecycle() - val bsState by bottomSheetState - - LaunchedEffect(bsState) { - model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED - } - - BackHandler(enabled = bsState == BottomSheetState.EXPANDED) { - navigateBack() - } - - MarketsTokenDetailsContent( - modifier = modifier, - backgroundColor = LocalMainBottomSheetColor.current.value, - addTopBarStatusBarPadding = false, - state = state, - onBackClick = ::navigateBack, - backButtonEnabled = bsState == BottomSheetState.EXPANDED, - onHeaderSizeChange = onHeaderSizeChange, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, - portfolioBlock = portfolioComponent?.let { component -> - { blockModifier -> - component.Content(blockModifier) - } - }, - ) - } - - @Composable - override fun Content(modifier: Modifier) { - LifecycleStartEffect(Unit) { - model.isVisibleOnScreen.value = true - onStopOrDispose { - model.isVisibleOnScreen.value = false - } - } - - val state by model.state.collectAsStateWithLifecycle() - - MarketsTokenDetailsContent( - modifier = modifier, - backgroundColor = TangemTheme.colors.background.tertiary, - addTopBarStatusBarPadding = true, - state = state, - onBackClick = ::navigateBack, - backButtonEnabled = true, - onHeaderSizeChange = {}, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, - portfolioBlock = portfolioComponent?.let { component -> - { blockModifier -> - component.Content(blockModifier) - } - }, - ) - } - - private fun navigateBack() = router.pop() - - @AssistedFactory - interface Factory : MarketsTokenDetailsComponent.Factory { - override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt deleted file mode 100644 index c3674eb4cc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.markets.details.impl.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketParams - -internal class MarketDetailsAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { - - data class EventBuilder( - val token: TokenMarketParams, - ) { - fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent( - event = "Token Chart Screen Opened", - params = buildMap { - put("Token", token.symbol) - blockchain?.let { put("blockchain", it) } - put("Source", source) - }, - ) - - fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent( - event = "Button - Period", - params = mapOf( - "Token" to token.symbol, - "Period" to interval.toAnalyticsString(), - "Source" to intervalType.source, - ), - ) - - fun readMoreClicked() = MarketDetailsAnalyticsEvent( - event = "Button - Read More", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent( - event = "Button - Links", - params = mapOf( - "Token" to token.symbol, - "Link" to linkTitle, - ), - ) - - fun exchangesScreenOpened() = MarketDetailsAnalyticsEvent( - event = "Exchanges Screen Opened", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun securityScoreOpened() = MarketDetailsAnalyticsEvent( - event = "Security Score Info", - params = mapOf("Token" to token.symbol), - ) - - fun securityScoreProviderClicked(provider: String) = MarketDetailsAnalyticsEvent( - event = "Security Score Provider Clicked", - params = mapOf( - "Token" to token.symbol, - "Provider" to provider, - ), - ) - } - - enum class IntervalType(val source: String) { - Chart("Chart"), - PricePerformance("Price"), - Insights("Insights"), - } -} - -private fun PriceChangeInterval.toAnalyticsString() = when (this) { - PriceChangeInterval.H24 -> "24h" - PriceChangeInterval.WEEK -> "7d" - PriceChangeInterval.MONTH -> "1m" - PriceChangeInterval.MONTH3 -> "3m" - PriceChangeInterval.MONTH6 -> "6m" - PriceChangeInterval.YEAR -> "1y" - PriceChangeInterval.ALL_TIME -> "All" -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt deleted file mode 100644 index 8ef6de84d7..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.details.impl.di - -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent -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 ComponentModule { - - @Binds - @Singleton - fun bindMarketsTokenDetailsComponent( - factory: DefaultMarketsTokenDetailsComponent.Factory, - ): MarketsTokenDetailsComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt deleted file mode 100644 index 2fda5dee58..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.details.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface ModelModule { - - @Binds - @IntoMap - @ClassKey(MarketsTokenDetailsModel::class) - fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt deleted file mode 100644 index f2a44120b9..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ /dev/null @@ -1,645 +0,0 @@ -package com.tangem.features.markets.details.impl.model - -import androidx.compose.runtime.Stable -import arrow.core.Either -import arrow.core.getOrElse -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.charts.state.MarketChartData -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.sorted -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 -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains -import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.markets.* -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent -import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter -import com.tangem.features.markets.details.impl.model.converters.ExchangeItemStateConverter -import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter -import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater -import com.tangem.features.markets.details.impl.model.state.TokenNetworksState -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import org.joda.time.DateTime -import java.math.BigDecimal -import java.util.Locale -import javax.inject.Inject - -@Suppress("LargeClass", "LongParameterList") -@Stable -@ModelScoped -internal class MarketsTokenDetailsModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, - private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, - private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, - private val getTokenExchangesUseCase: GetTokenExchangesUseCase, - private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val urlOpener: UrlOpener, - private val analyticsEventHandler: AnalyticsEventHandler, - private val excludedBlockchains: ExcludedBlockchains, - private val getUserCountryUseCase: GetUserCountryUseCase, - private val getUserWalletsUseCase: GetWalletsUseCase, -) : Model() { - - private val quotesJob = JobHolder() - private var userCountry: UserCountry? = null - private val params = paramsContainer.require() - private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token) - - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = params.appCurrency, - ) - - private val infoConverter = TokenMarketInfoConverter( - appCurrency = Provider { currentAppCurrency.value }, - onInfoClick = { showBottomSheet(it) }, - onListedOnClick = ::onListedOnClick, - onLinkClick = { link -> - urlOpener.openUrl(link.url) - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title)) - }, - onSecurityScoreInfoClick = { content -> - showBottomSheet(content) - - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.securityScoreOpened()) - }, - onSecurityScoreProviderLinkClick = { provider -> - provider.urlData?.fullUrl?.let { url -> - urlOpener.openUrl(url) - } - - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.securityScoreProviderClicked(provider.name)) - }, - // === Analytics === - onPricePerformanceIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, - interval = interval, - ), - ) - }, - onInsightsIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights, - interval = interval, - ), - ) - }, - needApplyFCARestrictions = Provider { - userCountry.needApplyFCARestrictions() - }, - // ================== - ) - - private val descriptionConverter = DescriptionConverter( - onReadModeClicked = { content -> - showBottomSheet(content) - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked()) - }, - needApplyFCARestrictions = Provider { - userCountry.needApplyFCARestrictions() - }, - onGeneratedAINotificationClick = { - modelScope.launch { - sendFeedbackEmailUseCase( - type = FeedbackEmailType.CurrencyDescriptionError( - currencyId = params.token.id.value, - currencyName = params.token.name, - ), - ) - } - }, - ) - - private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { - chartData = MarketChartData.NoData.Loading - - updateLook { currentLook -> - val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType() - - currentLook.copy( - type = percentChangeType.toChartType(), - xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), - yAxisFormatter = { value -> - value.format { - fiat( - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ).price() - } - }, - ) - } - } - - private val currentQuotes = MutableStateFlow( - TokenQuotes( - currentPrice = params.token.tokenQuotes.currentPrice, - h24ChangePercent = params.token.tokenQuotes.h24Percent, - weekChangePercent = params.token.tokenQuotes.weekPercent, - monthChangePercent = params.token.tokenQuotes.monthPercent, - m3ChangePercent = null, - m6ChangePercent = null, - yearChangePercent = null, - allTimeChangePercent = null, - ), - ) - - private val currentTokenInfo = MutableStateFlow(null) - private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis) - - val isVisibleOnScreen = MutableStateFlow(false) - val networksState = MutableStateFlow(TokenNetworksState.Loading) - - val state = MutableStateFlow( - MarketsTokenDetailsUM( - tokenName = params.token.name, - priceText = params.token.tokenQuotes.currentPrice.format { - fiat( - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ).price() - }, - dateTimeText = resourceReference(R.string.common_today), - priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, - priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), - iconUrl = params.token.imageUrl, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = chartDataProducer, - onLoadRetryClick = ::onLoadRetryClicked, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = ::onMarkerPointSelected, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = ::onSelectedIntervalChange, - isMarkerSet = false, - body = MarketsTokenDetailsUM.Body.Loading, - triggerPriceChange = consumedEvent(), - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - ), - ) - - private val quotesStateUpdater = QuotesStateUpdater( - currentAppCurrency = Provider { currentAppCurrency.value }, - state = state, - currentQuotes = currentQuotes, - lastUpdatedTimestamp = lastUpdatedTimestamp, - currentTokenInfo = currentTokenInfo, - onPricePerformanceIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, - interval = interval, - ), - ) - }, - ) - - private val loadChartJobHolder = JobHolder() - - init { - userCountry = getUserCountryUseCase.invokeSync().getOrNull() - ?: UserCountry.Other(Locale.getDefault().country) - // reload screen if currency changed - modelScope.launch { - currentAppCurrency - .filter { it != params.appCurrency } - .collectLatest { _ -> - initialLoad() - } - } - - initialLoad() - } - - private fun initialLoad() { - loadInfo() - loadChart(state.value.selectedInterval) - modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) - } - - private fun loadQuotes() { - modelScope.launch { - val result = getTokenFullQuotesUseCase( - tokenId = params.token.id, - appCurrency = currentAppCurrency.value, - tokenSymbol = params.token.symbol, - ) - - result.onRight { res -> - updateQuotes(res) - } - } - } - - private fun loadChart(interval: PriceChangeInterval) { - modelScope.launch { - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - ), - ) - } - - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.NoData.Loading - } - - val chart = getTokenPriceChartUseCase.invoke( - appCurrency = currentAppCurrency.value, - interval = interval, - tokenId = params.token.id, - tokenSymbol = params.token.symbol, - preview = false, - ) - - state.update { currentState -> - currentState.copy( - selectedInterval = interval, - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - ), - ) - } - - chart - .onRight { updateTokenChart(it) } - .onLeft { - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.ERROR, - ), - body = if (currentState.body is MarketsTokenDetailsUM.Body.Error) { - MarketsTokenDetailsUM.Body.Nothing - } else { - currentState.body - }, - ) - } - } - }.saveIn(loadChartJobHolder) - } - - private suspend fun updateTokenChart(tokenChart: TokenChart) { - val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval) - - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.Data( - y = tokenChart.priceY.toImmutableList(), - x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(), - ).sorted() - - updateLook { currentLook -> - currentLook.copy( - xAxisFormatter = xAxisFormatter, - type = state.value.priceChangeType.toChartType(), - ) - } - } - - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.DATA, - ), - body = if (currentState.body is MarketsTokenDetailsUM.Body.Nothing) { - MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked) - } else { - currentState.body - }, - ) - } - } - - private fun loadInfo() { - state.update { currentState -> - currentState.copy( - body = MarketsTokenDetailsUM.Body.Loading, - ) - } - - modelScope.launch { - val tokenMarketInfo = getTokenMarketInfoUseCase( - appCurrency = currentAppCurrency.value, - tokenId = params.token.id, - tokenSymbol = params.token.symbol, - ) - - tokenMarketInfo.fold( - ifRight = { result -> updateInfo(result) }, - ifLeft = { - state.update { currentState -> - if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { - currentState.copy( - body = MarketsTokenDetailsUM.Body.Error( - onLoadRetryClick = ::onLoadRetryClicked, - ), - ) - } else { - currentState.copy( - body = MarketsTokenDetailsUM.Body.Nothing, - ) - } - } - }, - ) - } - } - - private fun updateInfo(newInfo: TokenMarketInfo) { - lastUpdatedTimestamp.value = DateTime.now().millis - - currentTokenInfo.value = newInfo - currentQuotes.value = newInfo.quotes - - val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) - - state.update { currentState -> - currentState.copy( - priceText = newInfo.quotes.currentPrice.format { - fiat( - fiatCurrencySymbol = currentAppCurrency.value.symbol, - fiatCurrencyCode = currentAppCurrency.value.code, - ).price() - }, - priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( - interval = currentState.selectedInterval, - ), - priceChangeType = percent.percentChangeType(), - body = MarketsTokenDetailsUM.Body.Content( - description = descriptionConverter.convert(newInfo), - infoBlocks = infoConverter.convert(newInfo), - ), - ) - } - - val areAllWalletsHot = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - - val networks = newInfo.networks?.filter { network -> - BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, - excludedBlockchains = excludedBlockchains, - hotExcludedBlockchains = hotWalletExcludedBlockchains, - hasOnlyHotWallets = areAllWalletsHot, - ) - } - - networksState.value = if (networks.isNullOrEmpty()) { - TokenNetworksState.NoNetworksAvailable - } else { - TokenNetworksState.NetworksAvailable(networks) - } - - chartDataProducer.runTransaction { - updateLook { currentLook -> - currentLook.copy(type = percent.percentChangeType().toChartType()) - } - } - } - - private suspend fun updateQuotes(newQuotes: TokenQuotes) { - val populatedNewQuotes = currentQuotes.value.populateWith(newQuotes) - - quotesStateUpdater.updateQuotes(newQuotes = populatedNewQuotes) - - val percent = populatedNewQuotes - .getPercentByInterval(interval = state.value.selectedInterval) - - chartDataProducer.runTransaction { - updateLook { - it.copy(type = percent.percentChangeType().toChartType()) - } - } - } - - private fun onSelectedIntervalChange(interval: PriceChangeInterval) { - if (state.value.selectedInterval == interval) return - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart, - interval = interval, - ), - ) - // ================== - - val quotes = currentQuotes.value - val priceChangePercent = quotes.getFormattedPercentByInterval(interval) - - state.update { currentState -> - currentState.copy( - priceChangePercentText = priceChangePercent, - selectedInterval = interval, - priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType() - ?: PriceChangeType.NEUTRAL, - dateTimeText = getDefaultDateTimeString(interval), - ) - } - - loadChart(interval) - - if (priceChangePercent.isEmpty()) { - loadQuotes() - } - } - - @Suppress("MagicNumber") - private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) { - val currentState = state.value - - val dateTimeText = markerTimestamp?.let { timestamp -> - MarketsDateTimeFormatters.formatDateByIntervalWithMarker( - interval = currentState.selectedInterval, - markerTimestamp = timestamp, - ) - } ?: getDefaultDateTimeString(currentState.selectedInterval) - - val priceText = (price ?: currentQuotes.value.currentPrice).format { - fiat( - fiatCurrencySymbol = currentAppCurrency.value.symbol, - fiatCurrencyCode = currentAppCurrency.value.code, - ).price() - } - - val percent = price?.let { selectedPrice -> - getChangePercentBetween( - previousPrice = selectedPrice, - currentPrice = currentQuotes.value.currentPrice, - ) - } ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval) - - val percentText = percent?.format { percent() }.orEmpty() - - state.update { stateToUpdate -> - stateToUpdate.copy( - isMarkerSet = markerTimestamp != null, - dateTimeText = dateTimeText, - priceText = priceText, - priceChangePercentText = percentText, - priceChangeType = percent.percentChangeType(), - ) - } - - chartDataProducer.runTransaction { - updateLook { currentLook -> - currentLook.copy( - type = percent.percentChangeType().toChartType(), - ) - } - } - } - - private fun showBottomSheet(content: TangemBottomSheetConfigContent) { - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy( - isShown = true, - onDismissRequest = ::hideBottomSheet, - content = content, - ), - ) - } - } - - private fun hideBottomSheet() { - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShown = false), - ) - } - } - - private fun onLoadRetryClicked() { - val currentState = state.value - - if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) { - loadChart(currentState.selectedInterval) - } - - if (currentState.body is MarketsTokenDetailsUM.Body.Error || - currentState.body is MarketsTokenDetailsUM.Body.Nothing - ) { - loadInfo() - modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) - } - } - - private fun onListedOnClick(exchangesCount: Int) { - modelScope.launch { - analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened()) - - showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount)) - - val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id) - - // Delay to show the bottom sheet - delay(timeMillis = 400L) - - updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount) - } - } - - private fun updateExchangeBSContent( - maybeExchanges: Either>, - exchangesCount: Int, - ) { - val content = maybeExchanges - .fold( - ifLeft = { - ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) }) - }, - ifRight = { exchanges -> - ExchangesBottomSheetContent.Content( - exchangeItems = ExchangeItemStateConverter.convertList(exchanges).toImmutableList(), - ) - }, - ) - - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(content = content), - ) - } - } - - private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { - launch { - while (true) { - delay(timeMillis) - // Update quotes only when content is visible on the screen - isVisibleOnScreen.first { it } - - loadQuotes() - } - }.saveIn(quotesJob) - } - - private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference { - return MarketsDateTimeFormatters.formatDateByInterval( - interval = interval, - startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( - interval = interval, - currentTimestamp = lastUpdatedTimestamp.value, - ), - ) - } - - private companion object { - const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt deleted file mode 100644 index ae495c8db0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -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.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -@Stable -internal class DescriptionConverter( - private val onReadModeClicked: (InfoBottomSheetContent) -> Unit, - private val onGeneratedAINotificationClick: () -> Unit, - private val needApplyFCARestrictions: Provider, -) : Converter { - - override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? { - if (needApplyFCARestrictions()) return null - val shortDesc = value.shortDescription ?: return null - return MarketsTokenDetailsUM.Description( - shortDescription = stringReference(shortDesc), - fullDescription = value.fullDescription?.let(::stringReference), - onReadMoreClick = { - onReadModeClicked( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_about_token_title, - wrappedList( - value.name, - ), - ), - body = stringReference(value.fullDescription.orEmpty()), - generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM( - onClick = onGeneratedAINotificationClick, - ), - ), - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt deleted file mode 100644 index 096f0d0430..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.markets.TokenMarketExchange -import com.tangem.domain.markets.TokenMarketExchange.TrustScore -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter - -/** - * Converter from [TokenMarketExchange] to [TokenItemState] - * -[REDACTED_AUTHOR] - */ -internal object ExchangeItemStateConverter : Converter { - - override fun convert(value: TokenMarketExchange): TokenItemState { - return TokenItemState.Content( - id = value.id, - iconState = CurrencyIconState.CoinIcon( - url = value.imageUrl, - fallbackResId = R.drawable.ic_alert_24, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)), - fiatAmountState = TokenItemState.FiatAmountState.Content( - text = value.volumeInUsd.format { - fiat( - fiatCurrencyCode = "USD", - fiatCurrencySymbol = "$", - ).price() - }, - ), - subtitleState = TokenItemState.SubtitleState.TextContent( - value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"), - ), - subtitle2State = TokenItemState.Subtitle2State.LabelContent( - auditLabelUM = value.trustScore.toAuditLabelUM(), - ), - onItemClick = null, - onItemLongClick = null, - ) - } - - private fun TrustScore.toAuditLabelUM(): AuditLabelUM { - return when (this) { - TrustScore.Risky -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky), - type = AuditLabelUM.Type.Prohibition, - ) - TrustScore.Caution -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution), - type = AuditLabelUM.Type.Warning, - ) - TrustScore.Trusted -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted), - type = AuditLabelUM.Type.Permit, - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt deleted file mode 100644 index 65273ee0bb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.rawCompact -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.InsightsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal - -@Stable -internal class InsightsConverter( - private val appCurrency: Provider, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, - private val onIntervalChanged: (PriceChangeInterval) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.Insights): InsightsUM { - return with(value) { - InsightsUM( - h24Info = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.day, - holdersChange = holdersChange?.day, - liquidityChange = liquidityChange?.day, - buyPressureChange = buyPressureChange?.day, - ), - weekInfo = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.week, - holdersChange = holdersChange?.week, - liquidityChange = liquidityChange?.week, - buyPressureChange = buyPressureChange?.week, - ), - monthInfo = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.month, - holdersChange = holdersChange?.month, - liquidityChange = liquidityChange?.month, - buyPressureChange = buyPressureChange?.month, - ), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_insights), - body = resourceReference( - R.string.markets_insights_info_description_message, - wrappedList(value.sourceNetworks.joinToString { it.name }), - ), - ), - ) - }, - onIntervalChanged = onIntervalChanged, - ) - } - } - - private fun createInfoPointList( - experiencedBuyerChange: BigDecimal?, - holdersChange: BigDecimal?, - liquidityChange: BigDecimal?, - buyPressureChange: BigDecimal?, - ): ImmutableList { - return listOfNotNull( - experiencedBuyerChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = experiencedBuyerChange.convertChange(), - change = experiencedBuyerChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_experienced_buyers_full), - body = resourceReference(R.string.markets_token_details_experienced_buyers_description), - ), - ) - }, - ) - }, - buyPressureChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = buyPressureChange.convertChange(isFiatValue = true), - change = buyPressureChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_buy_pressure_full), - body = resourceReference(R.string.markets_token_details_buy_pressure_description), - ), - ) - }, - ) - }, - holdersChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = holdersChange.convertChange(), - change = holdersChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_holders_full), - body = resourceReference(R.string.markets_token_details_holders_description), - ), - ) - }, - ) - }, - liquidityChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = liquidityChange.convertChange(), - change = liquidityChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_liquidity_full), - body = resourceReference(R.string.markets_token_details_liquidity_description), - ), - ) - }, - ) - }, - ).toImmutableList() - } - - private fun BigDecimal.changeType(): InfoPointUM.ChangeType? { - return when { - this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP - this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN - else -> null - } - } - - private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { - val value = if (isFiatValue) { - this.abs().format { - val currency = appCurrency() - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).compact() - } - } else { - this.abs().format { - rawCompact() - } - } - - return when { - this > BigDecimal.ZERO -> StringsSigns.PLUS + value - this < BigDecimal.ZERO -> StringsSigns.MINUS + value - this == BigDecimal.ZERO -> value - else -> StringsSigns.DASH_SIGN - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt deleted file mode 100644 index ca676ee2c5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -@Stable -internal class LinksConverter( - private val onLinkClick: (LinksUM.Link) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.Links): LinksUM { - return LinksUM( - officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(), - social = value.social?.map { it.convert() }.orEmpty().toImmutableList(), - repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(), - blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(), - onLinkClick = onLinkClick, - ) - } - - private fun TokenMarketInfo.Link.convert(): LinksUM.Link { - return LinksUM.Link( - title = title, - iconRes = getIconById(id), - url = link, - ) - } - - private fun getIconById(id: String?): Int { - return when (id) { - "linkedin" -> R.drawable.ic_linkedin_24 - "discord" -> R.drawable.ic_discord_24 - "youtube" -> R.drawable.ic_youtube_24 - "telegram" -> R.drawable.ic_telegram_24 - "github" -> R.drawable.ic_github_24 - "twitter" -> R.drawable.ic_twitter_24 - "facebook" -> R.drawable.ic_facebook_24 - "reddit" -> R.drawable.ic_reddit_24 - "instagram" -> R.drawable.ic_instagram_24 - "whitepaper" -> R.drawable.ic_doc_24 - else -> R.drawable.ic_arrow_top_right_24 - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt deleted file mode 100644 index c91039839b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.crypto -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.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.MetricsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal - -@Stable -internal class MetricsConverter( - private val appCurrency: Provider, - private val tokenSymbol: String, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, -) : Converter { - - @Suppress("LongMethod") - override fun convert(value: TokenMarketInfo.Metrics): MetricsUM { - return with(value) { - MetricsUM( - metrics = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_capitalization), - value = marketCap.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_market_capitalization_full, - ), - body = resourceReference( - R.string.markets_token_details_market_capitalization_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_rating), - value = marketRating?.toString() ?: StringsSigns.DASH_SIGN, - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_market_rating_full), - body = resourceReference(R.string.markets_token_details_market_rating_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_trading_volume), - value = volume24h.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_trading_volume_full), - body = resourceReference( - R.string.markets_token_details_trading_volume_24h_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = fullyDilutedValuation.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_fully_diluted_valuation_full, - ), - body = resourceReference( - R.string.markets_token_details_fully_diluted_valuation_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_circulating_supply), - value = circulatingSupply.formatAmount(crypto = true), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_circulating_supply_full), - body = resourceReference( - R.string.markets_token_details_circulating_supply_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_max_supply), - value = maxSupply.formatMaxSupply(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_max_supply_full), - body = resourceReference(R.string.markets_token_details_total_supply_description), - ), - ) - }, - ), - ), - ) - } - } - - private fun BigDecimal?.formatMaxSupply(): String { - when (this) { - null -> return StringsSigns.DASH_SIGN - BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN - } - - return this.formatAmount(crypto = true) - } - - private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN - - return if (crypto) { - format { - crypto( - symbol = tokenSymbol, - decimals = 2, - ).compact() - } - } else { - val currency = appCurrency() - - format { - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).compact() - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt deleted file mode 100644 index c3aa321cb1..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import java.math.BigDecimal -import java.math.RoundingMode - -@Stable -internal class PricePerformanceConverter( - private val appCurrency: Provider, - private val onIntervalChanged: (PriceChangeInterval) -> Unit, -) { - - fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { - return PricePerformanceUM( - h24 = value.day.convert(currentPrice), - month = value.month.convert(currentPrice), - all = value.allTime.convert(currentPrice), - onIntervalChanged = onIntervalChanged, - ) - } - - private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { - if (this == null || this.low == null || this.high == null) { - return PricePerformanceUM.Value( - low = StringsSigns.DASH_SIGN, - high = StringsSigns.DASH_SIGN, - indicatorFraction = 0f, - ) - } - - return PricePerformanceUM.Value( - low = low.convert(), - high = high.convert(), - indicatorFraction = calculateFraction(currentPrice), - ) - } - - private fun BigDecimal?.convert(): String { - val currency = appCurrency() - - return format { - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).price() - } - } - - private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float { - val lowValue = low - val highValue = high - return when { - lowValue == null || highValue == null || highValue == BigDecimal.ZERO || currentPrice < lowValue -> 0f - currentPrice > highValue || lowValue == highValue -> 1f - else -> { - (currentPrice - lowValue).divide(highValue - lowValue, RoundingMode.HALF_UP) - .setScale(2, RoundingMode.HALF_UP) - .toFloat().coerceAtMost(1f) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt deleted file mode 100644 index d3bec1f927..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.model.formatter.MarketsDateTimeFormatters -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter - -@Stable -internal class SecurityScoreConverter( - private val onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, - private val onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM { - val ratingsCount = value.securityScoreProviderData.size - return SecurityScoreUM( - score = value.totalSecurityScore, - description = pluralReference( - id = R.plurals.markets_token_details_based_on_ratings, - count = ratingsCount, - formatArgs = wrappedList(ratingsCount), - ), - onInfoClick = { - onSecurityScoreInfoClick( - SecurityScoreBottomSheetContent( - title = resourceReference(R.string.markets_token_details_security_score), - description = resourceReference(R.string.markets_token_details_security_score_description), - providers = value.securityScoreProviderData.map { provider -> - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = provider.providerName, - lastAuditDate = provider.lastAuditDate?.let { date -> - MarketsDateTimeFormatters.formatAsDate(date.millis) - }, - score = provider.securityScore, - urlData = provider.urlData?.let { urlData -> - SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = urlData.fullUrl, - rootHost = urlData.rootHost, - ) - }, - iconUrl = provider.iconUrl, - ) - }, - onProviderLinkClick = onSecurityScoreProviderLinkClick, - ), - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt deleted file mode 100644 index e7aa05eeb5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.details.impl.ui.state.ListedOnUM -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -@Stable -@Suppress("LongParameterList") -internal class TokenMarketInfoConverter( - private val appCurrency: Provider, - private val needApplyFCARestrictions: Provider, - private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit, - private val onListedOnClick: (Int) -> Unit, - onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, - onLinkClick: (LinksUM.Link) -> Unit, - onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, - onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, - onInsightsIntervalChanged: (PriceChangeInterval) -> Unit, -) : Converter { - - private val insightsConverter = InsightsConverter( - appCurrency = appCurrency, - onInfoClick = onInfoClick, - onIntervalChanged = onInsightsIntervalChanged, - ) - - private val securityScoreConverter = SecurityScoreConverter( - onSecurityScoreInfoClick = onSecurityScoreInfoClick, - onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick, - ) - private val pricePerformanceConverter = PricePerformanceConverter( - appCurrency = appCurrency, - onIntervalChanged = onPricePerformanceIntervalChanged, - ) - private val linksConverter = LinksConverter(onLinkClick = onLinkClick) - - override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { - val metricsConverter = MetricsConverter( - tokenSymbol = value.symbol, - appCurrency = appCurrency, - onInfoClick = onInfoClick, - ) - - val exchangesAmount = value.exchangesAmount - val insights = if (needApplyFCARestrictions()) { - null - } else { - value.insights?.let { insightsConverter.convert(it) } - } - val securityScore = if (needApplyFCARestrictions()) { - null - } else { - value.securityData?.let { securityScoreConverter.convert(it) } - } - return MarketsTokenDetailsUM.InformationBlocks( - insights = insights, - securityScore = securityScore, - metrics = value.metrics?.let { metricsConverter.convert(it) }, - pricePerformance = value.pricePerformance?.let { performance -> - pricePerformanceConverter.convert( - value = performance, - currentPrice = value.quotes.currentPrice, - ) - }, - listedOn = if (exchangesAmount != null && exchangesAmount > 0) { - ListedOnUM.Content(onClick = { onListedOnClick(exchangesAmount) }, amount = exchangesAmount) - } else { - ListedOnUM.Empty - }, - links = value.links?.let { linksConverter.convert(it) }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt deleted file mode 100644 index bc1aeb77e3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.markets.details.impl.model.formatter - -import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenQuotes -import java.math.BigDecimal -import java.math.RoundingMode - -internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String { - val percent = when (interval) { - PriceChangeInterval.H24 -> h24ChangePercent - PriceChangeInterval.WEEK -> weekChangePercent - PriceChangeInterval.MONTH -> monthChangePercent - PriceChangeInterval.MONTH3 -> m3ChangePercent - PriceChangeInterval.MONTH6 -> m6ChangePercent - PriceChangeInterval.YEAR -> yearChangePercent - PriceChangeInterval.ALL_TIME -> allTimeChangePercent - } - - return percent?.format { percent() }.orEmpty() -} - -internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? { - return when (interval) { - PriceChangeInterval.H24 -> h24ChangePercent - PriceChangeInterval.WEEK -> weekChangePercent - PriceChangeInterval.MONTH -> monthChangePercent - PriceChangeInterval.MONTH3 -> m3ChangePercent - PriceChangeInterval.MONTH6 -> m6ChangePercent - PriceChangeInterval.YEAR -> yearChangePercent - PriceChangeInterval.ALL_TIME -> allTimeChangePercent - } -} - -@Suppress("MagicNumber") -internal fun BigDecimal?.percentChangeType(): PriceChangeType { - val scaled = this?.setScale(4, RoundingMode.HALF_UP) - return when { - scaled == null -> PriceChangeType.NEUTRAL - scaled > BigDecimal.ZERO -> PriceChangeType.UP - scaled < BigDecimal.ZERO -> PriceChangeType.DOWN - else -> PriceChangeType.NEUTRAL - } -} - -@Suppress("MagicNumber") -internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal { - return if (previousPrice == BigDecimal.ZERO) { - BigDecimal.ZERO - } else { - currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP) - } -} - -internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = getFiatPriceAmountWithScale(value = currentPrice).first - val updated = getFiatPriceAmountWithScale(value = updatedPrice).first - - return when { - updated > current -> PriceChangeType.UP - updated < current -> PriceChangeType.DOWN - else -> PriceChangeType.NEUTRAL - } -} - -internal fun PriceChangeType.toChartType(): MarketChartLook.Type { - return when (this) { - PriceChangeType.UP -> MarketChartLook.Type.Growing - PriceChangeType.DOWN -> MarketChartLook.Type.Falling - PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt deleted file mode 100644 index f36474bbee..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.features.markets.details.impl.model.formatter - -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.utils.DateTimeFormatters -import com.tangem.core.ui.utils.formatAsDateTime -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.impl.R -import com.tangem.utils.H24_MILLIS -import com.tangem.utils.WEEK_MILLIS -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import java.math.BigDecimal - -internal object MarketsDateTimeFormatters { - - private val dateTimeMMMFormatter by lazy { - DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm") - } - - private val dateFormatter = DateTimeFormatters.dateDDMMYYYY - - fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { - return when (interval) { - PriceChangeInterval.H24 -> { value: BigDecimal -> - value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) - } - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) - } - PriceChangeInterval.YEAR -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) - } - PriceChangeInterval.ALL_TIME -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY) - } - } - } - - fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { - return when (interval) { - PriceChangeInterval.H24 -> resourceReference(R.string.common_today) - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - startTimestamp.formatAsDateTime(dateTimeMMMFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - startTimestamp.formatAsDateTime(dateFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all) - } - } - - fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference { - return when (interval) { - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - PriceChangeInterval.ALL_TIME, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - markerTimestamp.toLong().formatAsDateTime(dateFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - } - } - - @Suppress("MagicNumber") - fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long { - return when (interval) { - PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS - PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS - PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis - PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis - PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis - PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis - PriceChangeInterval.ALL_TIME -> 0 - } - } - - fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { - return formatDateByInterval( - interval = interval, - startTimestamp = getStartTimestampByInterval( - interval = interval, - currentTimestamp = currentTimestamp, - ), - ) - } - - fun formatAsDate(timestamp: Long): String { - return timestamp.formatAsDateTime(dateFormatter) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt deleted file mode 100644 index 116fb390b4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.features.markets.details.impl.model.state - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenQuotes -import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter -import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.utils.Provider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update -import org.joda.time.DateTime -import java.math.BigDecimal - -internal class QuotesStateUpdater( - private val currentAppCurrency: Provider, - private val state: MutableStateFlow, - private val currentQuotes: MutableStateFlow, - private val lastUpdatedTimestamp: MutableStateFlow, - private val currentTokenInfo: MutableStateFlow, - private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, -) { - private val pricePerformanceConverter = PricePerformanceConverter( - currentAppCurrency, - onIntervalChanged = onPricePerformanceIntervalChanged, - ) - - suspend fun updateQuotes(newQuotes: TokenQuotes) { - val triggerPriceChangeType = getFormattedPriceChange( - currentPrice = currentQuotes.value.currentPrice, - updatedPrice = newQuotes.currentPrice, - ) - val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { - triggeredEvent( - data = triggerPriceChangeType, - onConsume = { - state.update { it.copy(triggerPriceChange = consumedEvent()) } - }, - ) - } else { - consumedEvent() - } - - val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) - val priceChangeType = percent.percentChangeType() - - // wait until marker is removed - state.first { it.isMarkerSet.not() } - - currentQuotes.value = newQuotes - lastUpdatedTimestamp.value = DateTime.now().millis - - state.update { stateToUpdate -> - stateToUpdate.copy( - priceText = newQuotes.currentPrice.format { - fiat( - fiatCurrencySymbol = currentAppCurrency().symbol, - fiatCurrencyCode = currentAppCurrency().code, - ).price() - }, - priceChangePercentText = newQuotes.getFormattedPercentByInterval( - interval = stateToUpdate.selectedInterval, - ), - priceChangeType = priceChangeType, - triggerPriceChange = trigger, - dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString( - stateToUpdate.selectedInterval, - currentTimestamp = lastUpdatedTimestamp.value, - ), - body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice), - ) - } - } - - private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body { - val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this - - return if (this is MarketsTokenDetailsUM.Body.Content) { - copy( - infoBlocks = infoBlocks.copy( - pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price), - ), - ) - } else { - this - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt deleted file mode 100644 index dbe01ddcdc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets.details.impl.model.state - -import com.tangem.domain.markets.TokenMarketInfo - -internal sealed class TokenNetworksState { - - data object Loading : TokenNetworksState() - - data object NoNetworksAvailable : TokenNetworksState() - - data class NetworksAvailable(val networks: List) : TokenNetworksState() -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt deleted file mode 100644 index a732ce35a6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.tangem.features.markets.details.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.Dp -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.marketprice.PriceChangeInPercent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -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.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.components.* -import com.tangem.features.markets.details.impl.ui.preview.MarketsTokenDetailsPreview -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Suppress("LongParameterList") -@Composable -internal fun MarketsTokenDetailsContent( - state: MarketsTokenDetailsUM, - backgroundColor: Color, - addTopBarStatusBarPadding: Boolean, - onBackClick: () -> Unit, - onHeaderSizeChange: (Dp) -> Unit, - backButtonEnabled: Boolean, - isAccountEnabled: Boolean, - modifier: Modifier = Modifier, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - Content( - modifier = modifier, - backgroundColor = backgroundColor, - state = state, - onBackClick = onBackClick, - onHeaderSizeChange = onHeaderSizeChange, - backButtonEnabled = backButtonEnabled, - portfolioBlock = portfolioBlock, - isAccountEnabled = isAccountEnabled, - addTopBarStatusBarInsets = addTopBarStatusBarPadding, - ) - - when (state.bottomSheetConfig.content) { - is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig) - is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig) - is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig) - } -} - -@Suppress("LongParameterList") -@Composable -private fun Content( - state: MarketsTokenDetailsUM, - backgroundColor: Color, - addTopBarStatusBarInsets: Boolean, - onBackClick: () -> Unit, - onHeaderSizeChange: (Dp) -> Unit, - backButtonEnabled: Boolean, - isAccountEnabled: Boolean, - modifier: Modifier = Modifier, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - val density = LocalDensity.current - val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } - val lazyListState = rememberLazyListState() - - Column( - modifier = modifier - .drawBehind { drawRect(backgroundColor) } - .let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it } - .fillMaxSize(), - ) { - TopBar( - modifier = Modifier.onGloballyPositioned { coordinates -> - if (coordinates.size.height > 0) { - with(density) { - onHeaderSizeChange(coordinates.size.height.toDp()) - } - } - }, - lazyListState = lazyListState, - tokenName = state.tokenName, - tokenPrice = state.priceText, - isBackButtonEnabled = backButtonEnabled, - onBackClick = onBackClick, - ) - - SpacerH4() - - LazyColumn( - state = lazyListState, - contentPadding = PaddingValues(bottom = bottomBarHeight), - ) { - item("header") { - Header( - state = state, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - item { SpacerH16() } - item("intervalSelector") { - IntervalSelector( - trendInterval = state.selectedInterval, - onIntervalClick = state.onSelectedIntervalChange, - isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - item { SpacerH32() } - item("chart") { - MarketTokenDetailsChart( - modifier = Modifier.fillMaxWidth(), - backgroundColor = backgroundColor, - state = state.chartState, - ) - } - item { SpacerH16() } - - tokenMarketDetailsBody( - state = state.body, - isAccountEnabled = isAccountEnabled, - portfolioBlock = portfolioBlock, - ) - } - } -} - -@Composable -private fun TopBar( - lazyListState: LazyListState, - tokenName: String, - tokenPrice: String, - isBackButtonEnabled: Boolean, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val shouldShowPriceSubtitle by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex > 1 - } - } - - TangemTopAppBar( - modifier = modifier, - title = tokenName, - subtitle = if (shouldShowPriceSubtitle) tokenPrice else null, - startButton = TopAppBarButtonUM.Back( - onBackClicked = onBackClick, - enabled = isBackButtonEnabled, - ), - ) -} - -@Composable -private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - TokenPriceText( - price = state.priceText, - triggerPriceChange = state.triggerPriceChange, - ) - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { - Text( - text = state.dateTimeText.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - if (state.priceChangePercentText != null) { - PriceChangeInPercent( - valueInPercent = state.priceChangePercentText, - type = state.priceChangeType, - textStyle = TangemTheme.typography.caption2, - ) - } - } - } - SpacerW4() - CoinIcon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size48), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - } -} - -@Composable -private fun TokenPriceText( - price: String, - triggerPriceChange: StateEvent, - modifier: Modifier = Modifier, -) { - val growColor = TangemTheme.colors.text.accent - val fallColor = TangemTheme.colors.text.warning - val generalColor = TangemTheme.colors.text.primary1 - - val color = remember(generalColor) { Animatable(generalColor) } - - EventEffect(triggerPriceChange) { changeType -> - val nextColor = when (changeType) { - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - PriceChangeType.NEUTRAL -> return@EventEffect - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) - } - - Text( - text = price, - modifier = modifier, - color = color.value, - autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.head.fontSize), - maxLines = 1, - style = TangemTheme.typography.head, - ) -} - -@Composable -private fun IntervalSelector( - trendInterval: PriceChangeInterval, - isEnabled: Boolean, - onIntervalClick: (PriceChangeInterval) -> Unit, - modifier: Modifier = Modifier, -) { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - PriceChangeInterval.ALL_TIME, - ), - color = TangemTheme.colors.button.secondary, - initialSelectedItem = trendInterval, - onClick = onIntervalClick, - isEnabled = isEnabled, - modifier = modifier, - ) { - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = it.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = if (isEnabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.disabled - }, - ) - } - } -} - -@Composable -fun PriceChangeInterval.getText(): TextReference { - return when (this) { - PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title) - PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title) - PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title) - PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title) - PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title) - PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title) - PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title) - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun MarketsTokenDetailsContent_Preview( - @PreviewParameter(MarketsTokenDetailsContentPreviewProvider::class) params: MarketsTokenDetailsUM, -) { - TangemThemePreview { - MarketsTokenDetailsContent( - state = params, - onHeaderSizeChange = {}, - onBackClick = {}, - backgroundColor = TangemTheme.colors.background.tertiary, - portfolioBlock = {}, - backButtonEnabled = true, - isAccountEnabled = true, - addTopBarStatusBarPadding = false, - ) - } -} - -private class MarketsTokenDetailsContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - MarketsTokenDetailsPreview.loadingState, - MarketsTokenDetailsPreview.contentState, - ) -} -// endregion \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt deleted file mode 100644 index fdbf284dbc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt +++ /dev/null @@ -1,191 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.toImmutableList - -/** - * Exchanges bottom sheet - * - * @param config bottom sheet config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { Title(textResId = it.titleResId, onBackClick = config.onDismissRequest) }, - content = { content -> - Box { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = bottomBarHeight), - ) { - item(key = "subtitle") { - Subtitle( - subtitleRes = content.subtitleResId, - volumeReference = content.volumeReference, - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing8, - ), - ) - } - - items( - items = content.exchangeItems, - key = TokenItemState::id, - itemContent = { TokenItem(state = it, isBalanceHidden = false) }, - ) - } - - if (content is ExchangesBottomSheetContent.Error) { - Error( - content = content, - modifier = Modifier.align(Alignment.Center), - ) - } - } - }, - ) -} - -@Composable -private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) { - TangemTopAppBar( - title = stringResourceSafe(id = textResId), - startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick), - ) -} - -@Composable -private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference, modifier: Modifier = Modifier) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - SubtitleText(textReference = resourceReference(id = subtitleRes)) - - SubtitleText(textReference = volumeReference) - } -} - -@Composable -private fun SubtitleText(textReference: TextReference) { - Text( - text = textReference.resolveReference(), - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResourceSafe(id = content.message), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption1, - ) - - SpacerH12() - - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(id = R.string.alert_button_try_again), - onClick = content.onRetryClick, - ), - ) - } -} - -@Preview -@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ExchangesBottomSheet( - @PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent, -) { - TangemThemePreview { - ExchangesBottomSheet( - config = TangemBottomSheetConfig( - onDismissRequest = {}, - content = content, - isShown = true, - ), - ) - } -} - -private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider( - listOf( - ExchangesBottomSheetContent.Loading(exchangesCount = 13), - ExchangesBottomSheetContent.Error(onRetryClick = {}), - ExchangesBottomSheetContent.Content( - exchangeItems = List(size = 13) { index -> - TokenItemState.Content( - id = index.toString(), - iconState = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.ic_facebook_24, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"), - subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")), - subtitle2State = TokenItemState.Subtitle2State.LabelContent( - auditLabelUM = AuditLabelUM( - text = stringReference("Caution"), - type = AuditLabelUM.Type.Warning, - ), - ), - onItemClick = {}, - onItemLongClick = {}, - ) - } - .toImmutableList(), - ), - ), -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt deleted file mode 100644 index 234468b262..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.impl.R -import dev.jeziellago.compose.markdowntext.MarkdownText - -@Composable -internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { TangemBottomSheetTitle(title = it.title) }, - content = { content -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - MarkdownText( - markdown = content.body.resolveReference(), - disableLinkMovementMethod = true, - linkifyMask = 0, - syntaxHighlightColor = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.secondary, - ), - ) - - if (content.generatedAINotificationUM != null) { - AdditionalInfoNotification( - onClick = content.generatedAINotificationUM.onClick, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - - SpacerH(bottomBarHeight) - } - }, - ) -} - -@Composable -private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) { - Notification( - config = NotificationConfig( - subtitle = TextReference.Res(id = R.string.information_generated_with_ai), - iconResId = R.drawable.ic_magic_28, - onClick = onClick, - shouldShowArrowIcon = false, - ), - modifier = modifier, - subtitleColor = TangemTheme.colors.text.primary1, - containerColor = TangemTheme.colors.button.disabled, - iconTint = TangemTheme.colors.icon.accent, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt deleted file mode 100644 index 3ca286968f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM - -@Composable -internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.Start, - ) { - if (infoPointUM.onInfoClick != null) { - TooltipText( - text = infoPointUM.title, - onInfoClick = infoPointUM.onInfoClick, - textStyle = TangemTheme.typography.caption2, - ) - } else { - Text( - text = infoPointUM.title.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - Row { - Text( - text = infoPointUM.value, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - if (infoPointUM.change != null) { - SpacerW4() - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size8) - .align(Alignment.CenterVertically), - imageVector = ImageVector.vectorResource( - id = when (infoPointUM.change) { - InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8 - InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8 - }, - ), - tint = when (infoPointUM.change) { - InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent - InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning - }, - contentDescription = null, - ) - } - } - } -} - -@Composable -internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) { - Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.Start, - ) { - if (withTooltip) { - Box( - modifier = Modifier - .requiredHeight(TangemTheme.dimens.size16) - .fillMaxWidth(), - contentAlignment = Alignment.CenterStart, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.caption2, - textSizeHeight = false, - ) - } - } else { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.caption2, - textSizeHeight = true, - ) - } - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - style = TangemTheme.typography.body1, - textSizeHeight = true, - ) - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - Column( - modifier = Modifier - .width(150.dp) - .background(TangemTheme.colors.background.tertiary), - ) { - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000,000", - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000,000", - onInfoClick = { }, - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000", - change = InfoPointUM.ChangeType.UP, - onInfoClick = { }, - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000", - change = InfoPointUM.ChangeType.DOWN, - onInfoClick = { }, - ), - ) - } - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewShimmer() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - Column( - modifier = Modifier - .width(150.dp) - .background(TangemTheme.colors.background.tertiary), - ) { - InfoPointShimmer(modifier = Modifier.fillMaxWidth()) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - } - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt deleted file mode 100644 index d40e37b970..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.block.information.GridItems -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.InsightsUM -import com.tangem.features.markets.details.impl.ui.getText -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -@Composable -internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { - var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } - - InformationBlock( - modifier = modifier, - title = { - TooltipText( - text = resourceReference(R.string.markets_token_details_insights), - textStyle = TangemTheme.typography.subtitle2, - onInfoClick = state.onInfoClick, - ) - }, - action = { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - ), - initialSelectedItem = PriceChangeInterval.H24, - onClick = { interval -> - currentInterval = interval - state.onIntervalChanged(interval) - }, - modifier = Modifier.width(IntrinsicSize.Min), - ) { interval -> - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - horizontal = 14.dp, - vertical = 4.dp, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = interval.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - }, - content = { - val infoPoints = when (currentInterval) { - PriceChangeInterval.H24 -> state.h24Info - PriceChangeInterval.WEEK -> state.weekInfo - PriceChangeInterval.MONTH -> state.monthInfo - else -> state.h24Info - } - - GridItems( - items = infoPoints, - itemContent = { infoPoint -> - InfoPoint( - modifier = Modifier.align(Alignment.CenterStart), - infoPointUM = infoPoint, - ) - }, - ) - }, - ) -} - -@Composable -internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) { - val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } - val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } - val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 - - InformationBlock( - modifier = modifier, - title = { - RectangleShimmer( - modifier = Modifier - .height(headerHeight) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - }, - content = { - GridItems( - items = List(size = 4) { it }.toImmutableList(), - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - itemContent = { - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - ) - }, - ) - }, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - InsightsBlock( - state = InsightsUM( - h24Info = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000 000 000", - ), - ), - weekInfo = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000 000", - ), - ), - monthInfo = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000", - ), - ), - onInfoClick = {}, - onIntervalChanged = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - actualContent = { ContentPreview() }, - shimmerContent = { InsightsBlockPlaceholder() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt deleted file mode 100644 index 6d10ec8957..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.ChipShimmer -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.chip.Chip -import com.tangem.core.ui.components.inputrow.inner.DividerContainer -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_links), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - content = { - Column { - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_official_links), - links = state.officialLinks, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_social), - links = state.social, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_repository), - links = state.repository, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site), - links = state.blockchainSite, - onLinkClick = state.onLinkClick, - lastBlock = true, - ) - } - }, - ) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun SubBlock( - links: ImmutableList, - onLinkClick: (LinksUM.Link) -> Unit, - modifier: Modifier = Modifier, - lastBlock: Boolean = false, - title: String = "Official links", -) { - if (links.isEmpty()) return - - DividerContainer( - modifier = modifier, - showDivider = !lastBlock, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Text( - text = title, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - links.fastForEach { link -> - Chip( - text = stringReference(link.title), - iconResId = link.iconRes, - onClick = { onLinkClick(link) }, - ) - } - } - } - } -} - -@Composable -fun LinksBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - ) - }, - content = { - Column { - SubBlockPlaceholder() - SubBlockPlaceholder() - SubBlockPlaceholder(lastBlock = true) - } - }, - ) -} - -@Composable -private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) { - DividerContainer( - modifier = modifier, - showDivider = !lastBlock, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - TextShimmer( - modifier = Modifier.width(78.dp), - style = TangemTheme.typography.caption2, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - repeat(times = 3) { - ChipShimmer( - modifier = Modifier.weight(1f), - ) - } - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - LinksBlock( - state = LinksUM( - officialLinks = persistentListOf( - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - social = persistentListOf( - LinksUM.Link( - title = "Twitter", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Facebook", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - repository = persistentListOf( - LinksUM.Link( - title = "Github", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - blockchainSite = persistentListOf(), - onLinkClick = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PlaceholderPreview() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { LinksBlockPlaceholder() }, - actualContent = { ContentPreview() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt deleted file mode 100644 index 3d5714135e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.common.ui.R -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.state.ListedOnUM -import kotlinx.coroutines.delay - -/** - * "Listed on" block - * - * @param state block state - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) { - Box(modifier = modifier) { - InformationBlock( - title = { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - modifier = Modifier - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .clickable(enabled = state is ListedOnUM.Content) { - (state as? ListedOnUM.Content)?.onClick?.invoke() - }, - ) { - Description( - state = state, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - - if (state is ListedOnUM.Content) { - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - ) - } - } -} - -@Composable -internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - title = { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - ) - }, - modifier = modifier, - ) { - TextShimmer( - style = TangemTheme.typography.body2, - modifier = Modifier - .fillMaxWidth(fraction = 0.3f) - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } -} - -@Composable -private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) { - Text( - text = state.description.resolveReference(), - modifier = modifier, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) -} - -@Preview(widthDp = 328, heightDp = 68) -@Preview(name = "Dark Theme", widthDp = 328, heightDp = 68, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ListedOnBlock(@PreviewParameter(ListenOnUMProvider::class) state: ListedOnUM?) { - TangemThemePreview { - if (state == null) { - ListedOnBlockPlaceholder() - } else { - ListedOnBlock(state = state) - } - } -} - -@Preview -@Composable -private fun Preview_ListedOnBlock_StateChanging() { - var state by remember { mutableStateOf(value = null) } - - Preview_ListedOnBlock(state = state) - - LaunchedEffect(key1 = null) { - delay(timeMillis = 3000) - - state = ListedOnUM.Empty - } -} - -private class ListenOnUMProvider : CollectionPreviewParameterProvider( - collection = listOf( - ListedOnUM.Empty, - ListedOnUM.Content(onClick = {}, amount = 5), - null, - ), -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt deleted file mode 100644 index 0ad6b22ee0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -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.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import com.tangem.common.ui.charts.MarketChart -import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight -import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.common.ui.charts.state.rememberMarketChartState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.core.ui.components.UnableToLoadData - -@Composable -internal fun MarketTokenDetailsChart( - state: MarketsTokenDetailsUM.ChartState, - backgroundColor: Color, - modifier: Modifier = Modifier, -) { - val growingColor = TangemTheme.colors.icon.accent - val fallingColor = TangemTheme.colors.icon.warning - val neutralColor = TangemTheme.colors.icon.informative - - val chartState = rememberMarketChartState( - dataProducer = state.dataProducer, - colorMapper = { chartType -> - when (chartType) { - MarketChartLook.Type.Growing -> growingColor - MarketChartLook.Type.Falling -> fallingColor - MarketChartLook.Type.Neutral -> neutralColor - } - }, - onMarkerShown = state.onMarkerPointSelected, - ) - - val bottomChartAxisHeight = getMarketChartBottomAxisHeight() - - Box(modifier) { - MarketChart( - modifier = Modifier.fillMaxWidth(), - state = chartState, - ) - - if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) { - Box( - Modifier - .drawBehind { drawRect(backgroundColor) } - .matchParentSize() - .padding(bottom = bottomChartAxisHeight), - ) { - when (state.status) { - MarketsTokenDetailsUM.ChartState.Status.LOADING -> { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .align(Alignment.Center), - color = TangemTheme.colors.text.accent, - strokeWidth = TangemTheme.dimens.size2, - ) - } - MarketsTokenDetailsUM.ChartState.Status.ERROR -> { - UnableToLoadData( - modifier = Modifier - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ) - .align(Alignment.Center), - onRetryClick = state.onLoadRetryClick, - ) - } - else -> {} - } - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt deleted file mode 100644 index 98e74fd06f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.GridItems -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.MetricsUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -const val MAX_METRICS_COUNT = 6 - -@Composable -internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) { - var isExpanded by remember { mutableStateOf(false) } - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_metrics), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - action = { - if (state.metrics.size > MAX_METRICS_COUNT) { - ShowLessMoreButton(expanded = isExpanded, onClick = { isExpanded = !isExpanded }) - } - }, - content = { - val metrics = if (isExpanded) { - state.metrics - } else { - state.metrics.take(MAX_METRICS_COUNT).toImmutableList() - } - - GridItems( - items = metrics, - itemContent = { - InfoPoint(infoPointUM = it) - }, - ) - }, - ) -} - -// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock -@Composable -private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) { - // FIXME add string resources - val text = if (expanded) { - "See less" - } else { - "See more" - } - - TextButton( - text = text, - onClick = onClick, - colors = TangemButtonsDefaults.positiveButtonColors, - textStyle = TangemTheme.typography.body2, - ) -} - -@Composable -internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - title = { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - style = TangemTheme.typography.subtitle2, - ) - }, - action = { - Box(Modifier) - }, - content = { - GridItems( - items = List(size = 6) { it }.toImmutableList(), - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - itemContent = { - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - }, - ) - }, - ) -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BlockPreview() { - TangemThemePreview { - MetricsBlock( - state = MetricsUM( - metrics = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_capitalization), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_rating), - value = "A", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_trading_volume), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_circulating_supply), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_total_supply), - value = "1.2T", - onInfoClick = {}, - ), - ), - ), - ) - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - actualContent = { BlockPreview() }, - shimmerContent = { MetricsBlockPlaceholder() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt deleted file mode 100644 index 17e8d19e3f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ /dev/null @@ -1,256 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemAnimations -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.getText -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) { - var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_price_performance), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - action = { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.MONTH, - PriceChangeInterval.ALL_TIME, - ), - initialSelectedItem = PriceChangeInterval.H24, - onClick = { interval -> - currentInterval = interval - state.onIntervalChanged(interval) - }, - modifier = Modifier.width(IntrinsicSize.Min), - ) { interval -> - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - horizontal = 14.dp, - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = interval.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - }, - content = { - val value = when (currentInterval) { - PriceChangeInterval.H24 -> state.h24 - PriceChangeInterval.MONTH -> state.month - PriceChangeInterval.ALL_TIME -> state.all - else -> error("") - } - - Content( - modifier = Modifier.fillMaxWidth(), - state = value, - ) - }, - ) -} - -@Composable -private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) { - val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState( - targetFraction = state.indicatorFraction, - ) - - Column( - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing8), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResourceSafe(R.string.markets_token_details_low), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - SpacerW8() - Text( - text = stringResourceSafe(R.string.markets_token_details_high), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - TangemLinearProgressIndicator( - modifier = Modifier - .height(TangemTheme.dimens.size6) - .fillMaxWidth(), - progress = { animatedIndicatorFraction }, - color = TangemTheme.colors.text.accent, - backgroundColor = TangemTheme.colors.background.tertiary, - strokeCap = StrokeCap.Round, - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - modifier = Modifier.weight(1f), - text = state.low, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.weight(1f), - text = state.high, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ) - } - } -} - -@Composable -internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) { - val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } - val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } - val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 - - InformationBlock( - modifier = modifier, - title = { - RectangleShimmer( - modifier = Modifier - .height(headerHeight) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - }, - content = { - Column( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.width(35.dp), - style = TangemTheme.typography.caption2, - ) - SpacerW8() - TextShimmer( - modifier = Modifier.width(35.dp), - style = TangemTheme.typography.caption2, - ) - } - RectangleShimmer( - modifier = Modifier - .height(TangemTheme.dimens.size6) - .fillMaxWidth(), - radius = 27.dp, - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.width(TangemTheme.dimens.size56), - style = TangemTheme.typography.body1, - ) - SpacerW8() - TextShimmer( - modifier = Modifier.width(TangemTheme.dimens.size56), - style = TangemTheme.typography.body1, - ) - } - } - }, - ) -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - PricePerformanceBlock( - modifier = Modifier, - state = PricePerformanceUM( - h24 = PricePerformanceUM.Value( - low = "\$38,5K", - high = "\$58,5K", - indicatorFraction = 0.5f, - ), - month = PricePerformanceUM.Value( - low = "\$500,5K", - high = "\$5800,5K", - indicatorFraction = 0.8f, - ), - all = PricePerformanceUM.Value( - low = "\$58,52", - high = "\$580,5M", - indicatorFraction = 0.2f, - ), - onIntervalChanged = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PlaceholderPreview() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - PricePerformanceBlockPlaceholder() - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt deleted file mode 100644 index d7d047c38b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.annotation.FloatRange -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.impl.R -import kotlin.math.round - -private const val STARS_COUNT = 5 - -@Composable -internal fun ScoreStarsBlock( - score: Float, - horizontalSpacing: Dp, - scoreTextStyle: TextStyle, - modifier: Modifier = Modifier, -) { - val rounded = score.roundTo1decimal() - val percentage = rounded / STARS_COUNT - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = rounded.toString(), - style = scoreTextStyle, - color = TangemTheme.colors.text.primary1, - ) - Stars(fraction = percentage) - } -} - -@Suppress("MagicNumber") -@Composable -private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { - val grayColor = TangemTheme.colors.icon.inactive - - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - repeat(times = 5) { i -> - Box( - modifier = Modifier.size(TangemTheme.dimens.size16), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(16.dp) - .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) - .drawWithCache { - onDrawWithContent { - val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) - val starFractionFloat = starFraction - .toFloat() - .roundTo1decimal() - - drawContent() - drawRect( - color = grayColor, - topLeft = Offset(x = size.width * starFractionFloat, y = 0f), - size = Size(size.width * (1 - starFractionFloat), size.height), - blendMode = BlendMode.SrcIn, - ) - } - }, - imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } - } -} - -@Suppress("MagicNumber") -private fun Float.roundTo1decimal(): Float { - return round(this * 10) / 10 -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt deleted file mode 100644 index 468b7f7b59..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -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.draw.clip -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.text.TooltipText -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 -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM -import com.tangem.features.markets.impl.R - -@Composable -internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .fillMaxWidth() - .heightIn(max = TangemTheme.dimens.size72) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier - .weight(1F) - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - TooltipText( - text = resourceReference(R.string.markets_token_details_security_score), - onInfoClick = state.onInfoClick, - textStyle = TangemTheme.typography.subtitle2, - ) - - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - ScoreStarsBlock( - score = state.score, - scoreTextStyle = TangemTheme.typography.body1, - horizontalSpacing = TangemTheme.dimens.spacing8, - ) - } -} - -@Composable -internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary) - .fillMaxWidth() - .heightIn(max = TangemTheme.dimens.size72) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column( - modifier = Modifier - .fillMaxWidth(fraction = 0.4f) - .padding(vertical = TangemTheme.dimens.spacing2) - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - textSizeHeight = true, - ) - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } -} - -@Preview(widthDp = 328, showBackground = true) -@Preview(widthDp = 328, showBackground = true, locale = "ru") -@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - SecurityScoreBlock( - state = SecurityScoreUM( - score = 3.5f, - description = stringReference("Based on 3 ratings"), - onInfoClick = {}, - ), - ) - } -} - -@Preview(widthDp = 328, showBackground = true) -@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - SecurityScoreBlockPlaceholder( - modifier = Modifier.fillMaxWidth(), - ) - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt deleted file mode 100644 index b8c4242c9a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.util.fastForEachIndexed -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.components.inputrow.inner.DividerContainer -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.preview.SecurityScorePreviewData -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent - -@Composable -internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { TangemBottomSheetTitle(title = it.title) }, - content = { content -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Text( - text = content.description.resolveReference(), - style = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.secondary, - ), - ) - - SpacerH12() - content.providers.fastForEachIndexed { index, provider -> - DividerContainer( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.providers.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - showDivider = index != content.providers.lastIndex, - ) { - SecurityScoreProviderRow( - providerUM = provider, - onLinkClick = { content.onProviderLinkClick(provider) }, - ) - } - } - - SpacerH16() - SpacerH(bottomBarHeight) - } - }, - ) -} - -@Composable -private fun SecurityScoreProviderRow( - providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM, - onLinkClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing12) - .heightIn(min = TangemTheme.dimens.size68), - verticalAlignment = Alignment.CenterVertically, - ) { - SubcomposeAsyncImage( - modifier = Modifier - .size(size = TangemTheme.dimens.size40) - .clip(TangemTheme.shapes.roundedCorners8), - model = ImageRequest.Builder(context = LocalContext.current) - .data(providerUM.iconUrl) - .crossfade(enable = true) - .allowHardware(false) - .build(), - loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, - error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, - contentDescription = null, - ) - - Column( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - Text( - text = providerUM.name, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - if (providerUM.lastAuditDate != null) { - Text( - text = providerUM.lastAuditDate, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - - SpacerWMax() - - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - modifier = Modifier.clickable( - enabled = providerUM.urlData != null, - indication = ripple(bounded = false), - interactionSource = remember { MutableInteractionSource() }, - onClick = onLinkClick, - ), - ) { - ScoreStarsBlock( - score = providerUM.score, - scoreTextStyle = TangemTheme.typography.body2, - horizontalSpacing = TangemTheme.dimens.spacing3, - ) - - UrlBlock(providerUM) - } - } -} - -@Composable -private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) { - val urlData = providerUM.urlData - val rootHost = urlData?.rootHost - if (urlData != null && rootHost != null) { - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - Text( - text = urlData.rootHost, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_arrow_top_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - SecurityScoreBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = SecurityScorePreviewData.bottomSheetContent, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt deleted file mode 100644 index 13124c707d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.items.DescriptionItem -import com.tangem.core.ui.components.items.DescriptionPlaceholder -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R - -@Suppress("CanBeNonNullable") -internal fun LazyListScope.tokenMarketDetailsBody( - state: MarketsTokenDetailsUM.Body, - isAccountEnabled: Boolean, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - when (state) { - MarketsTokenDetailsUM.Body.Loading -> { - item("description-loading") { - DescriptionPlaceholder(modifier = Modifier.blockPaddings()) - } - - if (portfolioBlock != null) { - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) - } - } - - if (isAccountEnabled) { - aboutCoinHeader() - } - - loadingInfoBlocks() - } - is MarketsTokenDetailsUM.Body.Content -> { - if (state.description != null) { - description(state.description) - } - - if (portfolioBlock != null) { - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) - } - } - - if (isAccountEnabled) { - aboutCoinHeader() - } - - infoBlocksList(state.infoBlocks) - } - is MarketsTokenDetailsUM.Body.Error -> { - error(state) - } - MarketsTokenDetailsUM.Body.Nothing -> { - // Do nothing - } - } -} - -private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) { - item("body-error") { - Box(Modifier.fillMaxWidth()) { - UnableToLoadData( - modifier = Modifier - .align(Alignment.Center) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing40, - ), - onRetryClick = state.onLoadRetryClick, - ) - } - } -} - -private fun LazyListScope.aboutCoinHeader() { - item("aboutCoinHeader") { - Text( - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing20, - ), - text = stringResourceSafe(R.string.markets_about_coin_header), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h3, - ) - } -} - -private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) { - item("description") { - DescriptionItem( - modifier = Modifier.blockPaddings(), - description = description.shortDescription, - hasFullDescription = description.fullDescription != null, - onReadMoreClick = description.onReadMoreClick, - ) - } -} - -internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) { - if (state.insights != null) { - item("insights") { - InsightsBlock( - modifier = Modifier.blockPaddings(), - state = state.insights, - ) - } - } - - if (state.securityScore != null) { - item("securityScore") { - SecurityScoreBlock( - modifier = Modifier.blockPaddings(), - state = state.securityScore, - ) - } - } - - if (state.metrics != null) { - item("metrics") { - MetricsBlock( - modifier = Modifier.blockPaddings(), - state = state.metrics, - ) - } - } - - if (state.pricePerformance != null) { - item("pricePerformance") { - PricePerformanceBlock( - modifier = Modifier.blockPaddings(), - state = state.pricePerformance, - ) - } - } - - item(key = "listedOn") { - ListedOnBlock( - state = state.listedOn, - modifier = Modifier.blockPaddings(), - ) - } - - if (state.links != null) { - item("links") { - LinksBlock( - modifier = Modifier.blockPaddings(), - state = state.links, - ) - } - } -} - -private fun LazyListScope.loadingInfoBlocks() { - item("insights-loading") { - InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("securityScore-loading") { - SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("metrics-loading") { - MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("pricePerformance-loading") { - PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item(key = "listedOn-loading") { - ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("links-loading") { - LinksBlockPlaceholder(modifier = Modifier.blockPaddings()) - } -} - -@Composable -private fun Modifier.blockPaddings(): Modifier { - return this.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt deleted file mode 100644 index 83be0ff031..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.preview - -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.* -import kotlinx.collections.immutable.persistentListOf - -internal object MarketsTokenDetailsPreview { - private val infoPoint = InfoPointUM( - title = stringReference("1"), - value = "2", - change = InfoPointUM.ChangeType.DOWN, - onInfoClick = {}, - ) - - val loadingState = MarketsTokenDetailsUM( - tokenName = "Token Name", - priceText = "$0.00000000324", - dateTimeText = stringReference("Today"), - priceChangePercentText = "52.00%", - iconUrl = "", - priceChangeType = PriceChangeType.UP, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = MarketChartDataProducer.build { }, - onLoadRetryClick = {}, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = { _, _ -> }, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = { }, - body = MarketsTokenDetailsUM.Body.Loading, - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - isMarkerSet = false, - triggerPriceChange = consumedEvent(), - ) - - val contentState = MarketsTokenDetailsUM( - tokenName = "Token Name", - priceText = "$0.00000000324", - dateTimeText = stringReference("Today"), - priceChangePercentText = "52.00%", - iconUrl = "", - priceChangeType = PriceChangeType.UP, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = MarketChartDataProducer.build { }, - onLoadRetryClick = {}, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = { _, _ -> }, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = { }, - body = MarketsTokenDetailsUM.Body.Content( - description = MarketsTokenDetailsUM.Description( - shortDescription = stringReference("markets_token_details_description_short"), - fullDescription = stringReference("markets_token_details_description_full"), - onReadMoreClick = {}, - ), - infoBlocks = MarketsTokenDetailsUM.InformationBlocks( - insights = InsightsUM( - h24Info = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - weekInfo = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - monthInfo = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - onInfoClick = {}, - onIntervalChanged = {}, - ), - securityScore = SecurityScoreUM( - score = 2.3f, - description = stringReference("markets_token_details_security_score_description"), - onInfoClick = {}, - ), - metrics = MetricsUM( - metrics = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - ), - pricePerformance = PricePerformanceUM( - h24 = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - month = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - all = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - onIntervalChanged = {}, - ), - listedOn = ListedOnUM.Empty, - links = null, - ), - ), - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - isMarkerSet = true, - triggerPriceChange = consumedEvent(), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt deleted file mode 100644 index d80e49af6c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.preview - -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent - -internal object SecurityScorePreviewData { - - val bottomSheetContent = SecurityScoreBottomSheetContent( - title = stringReference("Security score"), - description = stringReference( - "Security score of a token is a metric that assesses the " + - "security level of a blockchain or token based on various factors and is compiled from " + - "the sources listed below.", - ), - providers = listOf( - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Moralis", - lastAuditDate = "21.10.2024", - score = 4.9F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://moralis.com/", - rootHost = "moralis.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Certik", - lastAuditDate = "10.07.2024", - score = 4.6F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://certik.com/", - rootHost = "certik.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Cyberscope", - lastAuditDate = "25.06.2023", - score = 4.5F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://cyberscope.com/", - rootHost = "cyberscope.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "TokenInsight", - lastAuditDate = "17.01.2022", - score = 4.0F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://tokeninsight.com/", - rootHost = "tokeninsight.com", - ), - iconUrl = "", - ), - - ), - onProviderLinkClick = {}, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt deleted file mode 100644 index cccbf85d31..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.StringRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.plus -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.impl.R -import com.tangem.utils.StringsSigns.DOT -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -/** - * Exchanges bottom sheet content - * -[REDACTED_AUTHOR] - */ -internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent { - - /** Title of bottom sheet. Like, app bar. */ - @get:StringRes - val titleResId: Int - get() = R.string.markets_token_details_exchanges_title - - /** Subtitle */ - @get:StringRes - val subtitleResId: Int - get() = R.string.markets_token_details_exchange - - /** Volume info */ - @get:StringRes - val volumeReference: TextReference - get() = resourceReference(id = R.string.markets_token_details_volume) + - stringReference(value = " $DOT ") + - resourceReference(id = R.string.markets_selector_interval_24h_title) - - /** Exchange items */ - val exchangeItems: ImmutableList - - /** - * Loading state - * - * @property exchangesCount count of exchanges - */ - data class Loading(val exchangesCount: Int) : ExchangesBottomSheetContent { - - override val exchangeItems: ImmutableList - get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") } - .toImmutableList() - } - - /** - * Content state - * - * @property exchangeItems exchanges - */ - data class Content( - override val exchangeItems: ImmutableList, - ) : ExchangesBottomSheetContent - - /** Error state */ - data class Error( - val onRetryClick: () -> Unit, - ) : ExchangesBottomSheetContent { - override val exchangeItems: ImmutableList = persistentListOf() - - @StringRes - val message: Int = R.string.markets_loading_error_title - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt deleted file mode 100644 index d6af118609..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class InfoBottomSheetContent( - val title: TextReference, - val body: TextReference, - val generatedAINotificationUM: GeneratedAINotificationUM? = null, -) : TangemBottomSheetConfigContent { - - data class GeneratedAINotificationUM(val onClick: () -> Unit) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt deleted file mode 100644 index 383db4e627..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.extensions.TextReference - -internal data class InfoPointUM( - val title: TextReference, - val value: String, - val change: ChangeType? = null, - val onInfoClick: (() -> Unit)? = null, -) { - enum class ChangeType { - UP, DOWN - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt deleted file mode 100644 index e2d0b3270b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.domain.markets.PriceChangeInterval -import kotlinx.collections.immutable.ImmutableList - -internal data class InsightsUM( - val h24Info: ImmutableList, - val weekInfo: ImmutableList, - val monthInfo: ImmutableList, - val onInfoClick: () -> Unit, - val onIntervalChanged: (PriceChangeInterval) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt deleted file mode 100644 index 5b4ea87e18..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.DrawableRes -import kotlinx.collections.immutable.ImmutableList - -internal data class LinksUM( - val officialLinks: ImmutableList, - val social: ImmutableList, - val repository: ImmutableList, - val blockchainSite: ImmutableList, - val onLinkClick: (Link) -> Unit, -) { - data class Link( - @DrawableRes val iconRes: Int, - val title: String, - val url: String, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt deleted file mode 100644 index a853675b10..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.markets.impl.R - -/** - * "Listed on" block UI model - * -[REDACTED_AUTHOR] - */ -internal sealed interface ListedOnUM { - - /** Title */ - val title: TextReference - get() = resourceReference(id = R.string.markets_token_details_listed_on) - - /** Description */ - val description: TextReference - - /** Empty state. No exchanges found */ - data object Empty : ListedOnUM { - override val description = resourceReference(id = R.string.markets_token_details_empty_exchanges) - } - - /** - * Content with number of exchanges - * - * @property onClick lambda be invoked when button is clicked - * @property amount amount of exchanges - */ - data class Content( - val onClick: () -> Unit, - private val amount: Int, - ) : ListedOnUM { - override val description: TextReference = pluralReference( - id = R.plurals.markets_token_details_amount_exchanges, - count = amount, - formatArgs = wrappedList(amount), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt deleted file mode 100644 index 94f581efd7..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.markets.PriceChangeInterval -import java.math.BigDecimal - -internal data class MarketsTokenDetailsUM( - val tokenName: String, - val priceText: String, - val iconUrl: String?, - val dateTimeText: TextReference, - val priceChangePercentText: String?, - val priceChangeType: PriceChangeType, - val selectedInterval: PriceChangeInterval, - val isMarkerSet: Boolean, - val chartState: ChartState, - val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, - val bottomSheetConfig: TangemBottomSheetConfig, - val triggerPriceChange: StateEvent, - val body: Body, -) { - - data class ChartState( - val status: Status, - val dataProducer: MarketChartDataProducer, - val onLoadRetryClick: () -> Unit, - val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, - ) { - enum class Status { - LOADING, ERROR, DATA - } - } - - data class InformationBlocks( - val insights: InsightsUM?, - val securityScore: SecurityScoreUM?, - val metrics: MetricsUM?, - val pricePerformance: PricePerformanceUM?, - val listedOn: ListedOnUM, - val links: LinksUM?, - ) - - @Immutable - sealed interface Body { - - data class Error( - val onLoadRetryClick: () -> Unit, - ) : Body - - data object Loading : Body - - data class Content( - val description: Description?, - val infoBlocks: InformationBlocks, - ) : Body - - data object Nothing : Body - } - - data class Description( - val shortDescription: TextReference, - val fullDescription: TextReference?, - val onReadMoreClick: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt deleted file mode 100644 index 8b28533fb3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import kotlinx.collections.immutable.ImmutableList - -internal data class MetricsUM( - val metrics: ImmutableList, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt deleted file mode 100644 index 9448472a0d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.FloatRange -import com.tangem.domain.markets.PriceChangeInterval - -internal data class PricePerformanceUM( - val h24: Value, - val month: Value, - val all: Value, - val onIntervalChanged: (PriceChangeInterval) -> Unit, -) { - data class Value( - val low: String, - val high: String, - @FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt deleted file mode 100644 index ceb100bdfb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class SecurityScoreBottomSheetContent( - val title: TextReference, - val description: TextReference, - val providers: List, - val onProviderLinkClick: (SecurityScoreProviderUM) -> Unit, -) : TangemBottomSheetConfigContent { - - data class SecurityScoreProviderUM( - val name: String, - val lastAuditDate: String?, - val score: Float, - val urlData: UrlData?, - val iconUrl: String?, - ) { - data class UrlData( - val fullUrl: String, - val rootHost: String?, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt deleted file mode 100644 index d4ebd3a9ee..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.FloatRange -import com.tangem.core.ui.extensions.TextReference - -internal data class SecurityScoreUM( - @FloatRange(from = 0.0, to = 5.0) val score: Float, - val description: TextReference, - val onInfoClick: () -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt deleted file mode 100644 index 9f441298d5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.popWhile -import com.arkivanov.decompose.value.Value -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.navigation.inner.InnerRouter -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child -import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsEntryComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - private val marketsEntryChildFactory: MarketsEntryChildFactory, -) : MarketsEntryComponent, AppComponentContext by context { - - private val stackNavigation = StackNavigation() - - private val innerRouter = InnerRouter( - stackNavigation = stackNavigation, - popCallback = { onChildBack() }, - ) - - private val stack: Value> = childStack( - key = "main", - source = stackNavigation, - serializer = Child.serializer(), - initialConfiguration = Child.TokenList, - handleBackButton = false, - childFactory = { configuration, factoryContext -> - marketsEntryChildFactory.createChild( - child = configuration, - appComponentContext = childByContext( - componentContext = factoryContext, - router = innerRouter, - ), - onTokenClick = ::marketsListTokenSelected, - ) - }, - ) - - @Suppress("LongMethod") - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - EntryBottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - stackState = stack.subscribeAsState(), - modifier = modifier, - ) - } - - @OptIn(ExperimentalDecomposeApi::class) - private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { - innerRouter.push( - route = Child.TokenDetails( - params = MarketsTokenDetailsComponent.Params( - token = token, - appCurrency = appCurrency, - shouldShowPortfolio = true, - analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = null, - source = "Market", - ), - ), - ), - ) - } - - private fun onChildBack() { - if (stack.value.active.configuration !is Child.TokenList) { - stackNavigation.popWhile { it != Child.TokenList } - } - } - - @AssistedFactory - interface Factory : MarketsEntryComponent.Factory { - override fun create(context: AppComponentContext): DefaultMarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt deleted file mode 100644 index 785ef74cdb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import androidx.compose.runtime.Immutable -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.tokenlist.MarketsTokenListComponent -import kotlinx.serialization.Serializable -import javax.inject.Inject - -internal class MarketsEntryChildFactory @Inject constructor( - private val tokenListComponentFactory: MarketsTokenListComponent.FactoryBottomSheet, - private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, -) { - - @Serializable - @Immutable - sealed interface Child : Route { - - @Serializable - @Immutable - data object TokenList : Child - - @Serializable - @Immutable - data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child - } - - fun createChild( - child: Child, - appComponentContext: AppComponentContext, - onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { - return when (child) { - is Child.TokenDetails -> { - tokenDetailsComponentFactory.create( - context = appComponentContext, - params = child.params, - ) - } - is Child.TokenList -> { - tokenListComponentFactory.create( - context = appComponentContext, - params = Unit, - onTokenClick = onTokenClick, - ) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt deleted file mode 100644 index 4603041300..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.entry.impl.di - -import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.entry.impl.DefaultMarketsEntryComponent -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 ComponentModule { - - @Binds - @Singleton - fun bindMarketsEntryComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt deleted file mode 100644 index 7814672735..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.features.markets.entry.impl.ui - -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector4D -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.slide -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation -import com.arkivanov.decompose.router.stack.ChildStack -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory -import com.tangem.features.markets.tokenlist.MarketsTokenListComponent - -@Composable -internal fun EntryBottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - stackState: State>, - modifier: Modifier = Modifier, -) { - val primary = TangemTheme.colors.background.primary - val backgroundColor = remember { Animatable(primary) } - - LocalMainBottomSheetColor.current.value = backgroundColor.value - - Children( - stack = stackState.value, - animation = stackAnimation(slide()), - modifier = modifier, - ) { child -> - when (child.configuration) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - (child.instance as MarketsTokenDetailsComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = Modifier, - ) - } - is MarketsEntryChildFactory.Child.TokenList -> { - (child.instance as MarketsTokenListComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = Modifier, - ) - } - } - } - - val activeChild = stackState.value.active.configuration - - BackgroundColorEffects( - activeChild = activeChild, - backgroundColor = backgroundColor, - bottomSheetState = bottomSheetState, - ) -} - -@Composable -private fun BackgroundColorEffects( - activeChild: MarketsEntryChildFactory.Child, - backgroundColor: Animatable, - bottomSheetState: State, -) { - val primary = TangemTheme.colors.background.primary - val tertiary = TangemTheme.colors.background.tertiary - - // Order of LaunchedEffects is important here - - LaunchedEffect(activeChild) { - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.animateTo( - tertiary, - animationSpec = tween(durationMillis = 500), - ) - } - is MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 500), - ) - } - } - } - - LaunchedEffect(bottomSheetState.value) { - if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { - when (bottomSheetState.value) { - BottomSheetState.EXPANDED -> { - backgroundColor.animateTo( - tertiary, - animationSpec = tween(durationMillis = 100), - ) - } - BottomSheetState.COLLAPSED -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 100), - ) - } - } - } - } - - LaunchedEffect(primary, tertiary) { - if (backgroundColor.isRunning) return@LaunchedEffect - - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(tertiary) - } - is MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.snapTo(primary) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt deleted file mode 100644 index f35084b04a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent - -internal interface AddToPortfolioComponent : ComposableBottomSheetComponent { - - data class Params( - val addToPortfolioManager: AddToPortfolioManager, - val callback: Callback, - ) - - interface Callback { - fun onDismiss() - } - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt deleted file mode 100644 index 28daa1389c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent.AnalyticsParams -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -internal interface AddToPortfolioManager { - - val token: TokenMarketParams - val analyticsParams: AnalyticsParams? - val portfolioFetcher: PortfolioFetcher - - val state: StateFlow - - val allAvailableNetworks: Flow> - fun setTokenNetworks(networks: List) - - sealed interface State { - data object Init : State - data class AvailableToAdd( - val availableToAddData: AvailableToAddData, - ) : State - - data object NothingToAdd : State - } - - interface Factory { - fun create( - scope: CoroutineScope, - token: TokenMarketParams, - analyticsParams: AnalyticsParams?, - ): AddToPortfolioManager - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt deleted file mode 100644 index 976ad0a6c4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.serialization.Serializable - -internal data class AvailableToAddData( - val availableToAddWallets: Map, -) { - val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd } - val isSinglePortfolio: Boolean - get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1 -} - -internal data class AvailableToAddWallet( - val userWallet: UserWallet, - val accounts: List, - val availableNetworks: Set, - val availableToAddAccounts: Map, -) { - val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd } -} - -@Serializable -internal data class AvailableToAddAccount( - val account: AccountStatus, - val availableNetworks: Set, - val addedNetworks: Set, -) { - - val isSingleNetwork: Boolean - get() = availableNetworks.size == 1 - - val availableToAddNetworks: Set = availableNetworks - .filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } } - .toSet() - - val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty() - - val addedMarketNetworks: Set = availableNetworks - .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } - .toSet() -} - -@Serializable -internal data class SelectedPortfolio( - val userWallet: UserWallet, - val account: AvailableToAddAccount, - val isAccountMode: Boolean, - val hasMorePortfoliosAvailable: Boolean, -) - -internal data class SelectedNetwork( - val selectedNetwork: TokenMarketInfo.Network, - val cryptoCurrency: CryptoCurrency, - val hasMoreNetworksAvailable: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt deleted file mode 100644 index ed74502649..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel -import com.tangem.common.ui.addtoken.AddTokenContent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow - -internal class AddTokenComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: AddTokenModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val um = state.value ?: return - AddTokenContent( - modifier = modifier, - state = um, - ) - } - - data class Params( - val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val selectedPortfolio: Flow, - val selectedNetwork: Flow, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onChangeNetworkClick() - fun onChangePortfolioClick() - fun onTokenAdded(status: CryptoCurrencyStatus) - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): AddTokenComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt deleted file mode 100644 index e954b972bd..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -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.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.markets.portfolio.add.impl.ui.ChooseNetworkContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class ChooseNetworkComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: ChooseNetworkModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - ChooseNetworkContent(state) - } - - data class Params( - val selectedPortfolio: SelectedPortfolio, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onNetworkSelected(network: TokenMarketInfo.Network) - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): ChooseNetworkComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt deleted file mode 100644 index 81c2f24a63..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.backStack -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop -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.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent.Params -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioRoutes -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultAddToPortfolioComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, - portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, - addTokenComponentFactory: AddTokenComponent.Factory, - tokenActionsComponentFactory: TokenActionsComponent.Factory, - private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory, -) : AppComponentContext by context, AddToPortfolioComponent { - - private val model: AddToPortfolioModel = getOrCreateModel(params) - - private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create( - context = child("portfolioSelectorComponent"), - params = PortfolioSelectorComponent.Params( - portfolioFetcher = model.portfolioFetcher, - controller = model.portfolioSelectorController, - ), - ) - - private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create( - context = child("addTokenComponent"), - params = AddTokenComponent.Params( - eventBuilder = model.eventBuilder, - callbacks = model, - selectedPortfolio = model.selectedPortfolio, - selectedNetwork = model.selectedNetwork, - ), - ) - - private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create( - context = child("tokenActionsComponent"), - params = TokenActionsComponent.Params( - eventBuilder = model.eventBuilder, - callbacks = model, - data = model.tokenActionsData, - ), - ) - - private val childStack = childStack( - key = "addToPortfolioStack", - handleBackButton = true, - source = model.navigation, - serializer = AddToPortfolioRoutes.serializer(), - initialStack = { model.currentStack }, - childFactory = ::contentChild, - ) - - private fun onBack() { - if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss() - } - - override fun dismiss() { - params.callback.onDismiss() - } - - @Composable - override fun BottomSheet() { - val stack by childStack.subscribeAsState() - val contentStack = remember { mutableStateOf(stack) } - val currentRoute = stack.active.configuration - val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty - if (isNotEmpty) { - contentStack.value = stack - } - - TangemModalBottomSheet( - scrollableContent = false, - onBack = ::onBack, - config = TangemBottomSheetConfig( - isShown = isNotEmpty, - onDismissRequest = ::dismiss, - content = TangemBottomSheetConfigContent.Empty, - ), - containerColor = TangemTheme.colors.background.tertiary, - title = { state -> - AnimatedContent(targetState = contentStack.value) { stack -> - BottomSheetTitle( - stack = stack, - onBackClick = ::onBack, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - content = { state -> - AnimatedContent(targetState = contentStack.value) { stack -> - val paddingModifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ) - val isScrollableContent = when (stack.active.configuration) { - AddToPortfolioRoutes.PortfolioSelector -> false - AddToPortfolioRoutes.AddToken, - AddToPortfolioRoutes.Empty, - is AddToPortfolioRoutes.NetworkSelector, - AddToPortfolioRoutes.TokenActions, - -> true - } - if (isScrollableContent) { - Column( - modifier = paddingModifier.verticalScroll(rememberScrollState()), - ) { - stack.active.instance.Content(modifier = Modifier) - } - } else { - stack.active.instance.Content(modifier = paddingModifier) - } - } - }, - ) - } - - @Composable - private fun BottomSheetTitle( - stack: ChildStack, - onBackClick: (() -> Unit), - modifier: Modifier = Modifier, - ) { - val title: TextReference = when (stack.active.configuration) { - AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token) - AddToPortfolioRoutes.Empty -> TextReference.EMPTY - is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network) - AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token) - AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent) - .title.collectAsStateWithLifecycle().value - } - val startIconRes: Int? - val endIconRes: Int? - if (stack.backStack.isNotEmpty()) { - startIconRes = R.drawable.ic_back_24 - endIconRes = null - } else { - startIconRes = null - endIconRes = R.drawable.ic_close_24 - } - TangemModalBottomSheetTitle( - modifier = modifier, - title = title, - startIconRes = startIconRes, - endIconRes = endIconRes, - onStartClick = onBackClick, - onEndClick = onBackClick, - ) - } - - private fun contentChild( - config: AddToPortfolioRoutes, - componentContext: ComponentContext, - ): ComposableContentComponent = when (config) { - AddToPortfolioRoutes.AddToken -> addTokenComponent - AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent - AddToPortfolioRoutes.TokenActions -> tokenActionsComponent - AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY - is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create( - context = childByContext(componentContext), - params = ChooseNetworkComponent.Params( - selectedPortfolio = config.selectedPortfolio, - callbacks = model, - ), - ) - } - - @AssistedFactory - interface Factory : AddToPortfolioComponent.Factory { - override fun create(context: AppComponentContext, params: Params): DefaultAddToPortfolioComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt deleted file mode 100644 index 93bf26a76f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel -import com.tangem.features.markets.portfolio.add.impl.ui.TokenActionsContent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.tokenreceive.TokenReceiveComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow - -internal class TokenActionsComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: TokenActionsModel = getOrCreateModel(params) - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = TokenReceiveConfig.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - val tokenActionsUM = state.value ?: return - TokenActionsContent( - modifier = modifier, - state = tokenActionsUM, - ) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: TokenReceiveConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - - data class Params( - val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val data: Flow, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onLaterClick() - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): TokenActionsComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt deleted file mode 100644 index 3a311b6854..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.converter - -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.GetTokenMarketCryptoCurrency -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.add.api.AvailableToAddAccount -import com.tangem.features.markets.portfolio.add.api.AvailableToAddData -import com.tangem.features.markets.portfolio.add.api.AvailableToAddWallet -import javax.inject.Inject - -internal class AvailableToAddDataConverter @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, - private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, -) { - - suspend fun convert( - balances: Map, - availableNetworks: Set, - marketParams: TokenMarketParams, - ): AvailableToAddData { - suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { - val currencies = availableNetworks - .mapNotNull { network -> - createCryptoCurrency( - userWallet = wallet, - network = network, - marketParams = marketParams, - account = this.account, - ) - } - - if (currencies.isEmpty()) return null - - val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, currencies) - .fold( - ifEmpty = { emptySet() }, - ifSome = { map -> - map.values.flatMapTo(hashSetOf()) { statuses -> - statuses.map { it.currency.network } - } - }, - ) - - return AvailableToAddAccount( - account = this, - availableNetworks = availableNetworks, - addedNetworks = addedNetworks, - ) - } - - suspend fun getAvailableToAddWallet( - entry: Map.Entry, - ): AvailableToAddWallet { - val (_, balance) = entry - val wallet = balance.userWallet - val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks) - val accounts = balance.accountsBalance.accountStatuses - val availableToAddAccounts: Map = accounts - .mapNotNull { accountStatus -> - val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null - accountStatus.account.accountId to availableToAddAccount - } - .toMap() - return AvailableToAddWallet( - userWallet = wallet, - accounts = accounts, - availableNetworks = filteredNetworks, - availableToAddAccounts = availableToAddAccounts, - ) - } - - val availableToAddWallets: Map = balances - .map { entry -> - val (walletId, _) = entry - val availableToAddWallet = getAvailableToAddWallet(entry) - walletId to availableToAddWallet - } - .filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() } - .toMap() - - return AvailableToAddData( - availableToAddWallets = availableToAddWallets, - ) - } - - private fun UserWallet.filteredAvailableNetworks(networks: Set) = - filterAvailableNetworksForWalletUseCase( - userWalletId = this.walletId, - networks = networks, - ) - - private suspend fun createCryptoCurrency( - userWallet: UserWallet, - network: TokenMarketInfo.Network, - marketParams: TokenMarketParams, - account: Account, - ): CryptoCurrency? { - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - return getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = marketParams, - network = network, - accountIndex = derivationIndex, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt deleted file mode 100644 index 39e8de3e0b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.di - -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager -import com.tangem.features.markets.portfolio.add.impl.DefaultAddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.impl.ui.DefaultAddToPortfolioManager -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal interface AddToPortfolioComponentModule { - - @Binds - fun bindAddToPortfolioComponent(factory: DefaultAddToPortfolioComponent.Factory): AddToPortfolioComponent.Factory - - @Binds - fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt deleted file mode 100644 index b093d471f6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel -import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface AddToPortfolioModelModule { - - @Binds - @IntoMap - @ClassKey(AddTokenModel::class) - fun addTokenModel(model: AddTokenModel): Model - - @Binds - @IntoMap - @ClassKey(AddToPortfolioModel::class) - fun addToPortfolioModel(model: AddToPortfolioModel): Model - - @Binds - @IntoMap - @ClassKey(TokenActionsModel::class) - fun tokenActionsModel(model: TokenActionsModel): Model - - @Binds - @IntoMap - @ClassKey(ChooseNetworkModel::class) - fun chooseNetworkModel(model: ChooseNetworkModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt deleted file mode 100644 index 71b3634b3d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ /dev/null @@ -1,383 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.popToFirst -import com.arkivanov.decompose.router.stack.pushNew -import com.arkivanov.decompose.router.stack.replaceAll -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 -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.markets.GetTokenMarketCryptoCurrency -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.account.PortfolioSelectorController -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.* -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter.Companion.toQuickActions -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -private const val TOKEN_ACTIONS_DELAY = 500L - -@ModelScoped -@Suppress("LongParameterList") -internal class AddToPortfolioModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val callbackDelegate: AddToPortfolioCallbackDelegate, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, - private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, - private val messageSender: UiMessageSender, - private val analyticsEventHandler: AnalyticsEventHandler, - val portfolioSelectorController: PortfolioSelectorController, -) : Model(), - ChooseNetworkComponent.Callbacks by callbackDelegate, - TokenActionsComponent.Callbacks by callbackDelegate, - AddTokenComponent.Callbacks by callbackDelegate { - - private val params = paramsContainer.require() - val navigation = StackNavigation() - var currentStack = listOf(AddToPortfolioRoutes.Empty) - - /* Flows that hold state and provide it to child models */ - val selectedNetwork: MutableSharedFlow = replayMutableSharedFlow() - val selectedPortfolio: MutableSharedFlow = replayMutableSharedFlow() - val tokenActionsData: MutableSharedFlow = replayMutableSharedFlow() - - private val addToPortfolioManager = params.addToPortfolioManager - val portfolioFetcher = addToPortfolioManager.portfolioFetcher - val eventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = addToPortfolioManager.token, - source = addToPortfolioManager.analyticsParams?.source, - ) - - val featureData: Flow = combineFeatureData() - - init { - navigation.subscribe { currentStack = it.transformer.invoke(currentStack) } - startAddToPortfolioFlow() - } - - private fun replayMutableSharedFlow() = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - @Suppress("LongMethod") - private fun startAddToPortfolioFlow() { - channelFlow { - fun finishFlow() { - params.callback.onDismiss() - channel.close() - } - val featureDataFlow: StateFlow = featureData - .filterIsInstance() - .map { it.availableToAddData } - .distinctUntilChanged() - .stateIn(this) - val isAccountMode = portfolioSelectorController.isAccountModeSync() - - // use snapshot data, looks like we don’t need to remap at runtime - val data = featureDataFlow.value - - // you must control it via [AddToPortfolioManager.state] - if (!data.isAvailableToAdd) { - finishFlow() - return@channelFlow - } - - // init data flows, emits on user/code selection, updates state holder - val firstSelectedPortfolio = setupPortfolioFlow(data) - .onEach { selectedPortfolio.emit(it) } - val firstSelectedNetwork = setupNetworkFlow(firstSelectedPortfolio) - .onEach { selectedNetwork.emit(it) } - - val isSinglePortfolio = data.isSinglePortfolio - if (isSinglePortfolio) { - val accountId = data.availableToAddWallets.values.first() - .availableToAddAccounts.values.first() - .account.account.accountId - // force select a portfolio, triggers [selectedPortfolio] - portfolioSelectorController.selectAccount(accountId) - } else { - logAccountSelector(isAccountMode) - navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector) - } - - val firstPartOfNavigation: Job = firstSelectedPortfolio - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - when { - // force select a network, triggers [selectedNetwork] - isSingleAvailableNetwork -> { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } - // it's important to control root screen, UI depends on it(close/arrow icon) - isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio)) - else -> navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - .launchIn(this) - - // main flow that combine all require data - val allRequireForAdd = combine( - flow = firstSelectedNetwork, - flow2 = firstSelectedPortfolio, - transform = { a, b -> a to b }, - ) - - // suspend until all required data is selected - allRequireForAdd.first() - // line of navigation to AddToken screen is finished; cancel the job, select a new root screen - firstPartOfNavigation.cancel() - - analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) - navigation.replaceAll(AddToPortfolioRoutes.AddToken) - - var middleNavigationJob: Job? = null - // handle actions from AddToken screen - callbackDelegate.onChangeNetworkClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changeNetworkNavigationFlow() - .launchIn(this) - val route = routeToNetworkSelector(selectedPortfolio.first()) - navigation.pushNew(route) - } - .launchIn(this) - // handle actions from AddToken screen - callbackDelegate.onChangePortfolioClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this) - logAccountSelector(isAccountMode) - navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) - } - .launchIn(this) - - // suspend until token is added - val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() - middleNavigationJob?.cancel() - val selectedPortfolio = selectedPortfolio.first() - - messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) - - setupTokenActionsFlow(selectedPortfolio, addedToken) - .onEach { cryptoCurrencyData -> - tokenActionsData.emit(cryptoCurrencyData) - navigation.replaceAll(AddToPortfolioRoutes.TokenActions) - } - .onEmpty { finishFlow() } - .launchIn(this) - - callbackDelegate.onLaterClick.receiveAsFlow().first() - finishFlow() - } - .catch { error -> - Timber.e(error) - params.callback.onDismiss() - } - .launchIn(modelScope) - } - - private fun logAccountSelector(isAccountMode: Boolean) { - if (isAccountMode) { - analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) - } - } - - private fun changeNetworkNavigationFlow(): Flow { - return setupNetworkFlow(selectedPortfolio) - .onEach { newNetwork -> - selectedNetwork.emit(newNetwork) - navigation.popToFirst() - } - } - - private fun changePortfolioNavigationFlow(data: AvailableToAddData): Flow = flow { - val selectedPortfolioValue = selectedPortfolio.first() - val selectedAccount = selectedPortfolioValue.account.account.account.accountId - portfolioSelectorController.selectAccount(selectedAccount) - val changedPortfolio = setupPortfolioFlow(data) - .drop(1) - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - if (isSingleAvailableNetwork) { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } else { - navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - val changedNetwork = setupNetworkFlow(changedPortfolio) - combine( - flow = changedPortfolio, - flow2 = changedNetwork, - transform = { newPortfolio, newNetwork -> - selectedPortfolio.tryEmit(newPortfolio) - selectedNetwork.tryEmit(newNetwork) - navigation.popToFirst() - }, - ).collect { emit(it) } - } - - private fun setupTokenActionsFlow( - selectedPortfolio: SelectedPortfolio, - addedToken: CryptoCurrencyStatus, - ): Flow { - val timeFlow = channelFlow { - val timerJob = launch { delay(TOKEN_ACTIONS_DELAY) } - getCryptoCurrencyActionsUseCase( - currency = addedToken.currency, - accountId = selectedPortfolio.account.account.account.accountId, - ).onEach { state -> - val requestedQuickActions = toQuickActions(state.states) - when { - requestedQuickActions.isNotEmpty() -> { - timerJob.cancel() - send(state) - } - // wait any requestedQuickActions while timer active - timerJob.isActive -> Unit - else -> close() - } - }.collect() - } - return timeFlow.map { actionsState -> - PortfolioData.CryptoCurrencyData( - userWallet = selectedPortfolio.userWallet, - status = actionsState.cryptoCurrencyStatus, - actions = actionsState.states, - ) - } - } - - private fun setupPortfolioFlow(data: AvailableToAddData): Flow = combine( - flow = portfolioSelectorController.isAccountMode, - flow2 = portfolioSelectorController.selectedAccount, - transform = { isAccountMode, selectedAccountId -> - selectedAccountId ?: return@combine null - val availableToAddWallets = - data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null - val availableToAddAccount = - availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null - if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged()) - SelectedPortfolio( - isAccountMode = isAccountMode, - userWallet = availableToAddWallets.userWallet, - account = availableToAddAccount, - hasMorePortfoliosAvailable = !data.isSinglePortfolio, - ) - }, - ) - .filterNotNull() - - private fun setupNetworkFlow(selectedPortfolioFlow: Flow): Flow = combine( - flow = selectedPortfolioFlow, - flow2 = callbackDelegate.onNetworkSelected.receiveAsFlow(), - transform = transform@{ selectedPortfolio, selectedNetwork -> - SelectedNetwork( - cryptoCurrency = createCryptoCurrency( - userWallet = selectedPortfolio.userWallet, - network = selectedNetwork, - account = selectedPortfolio.account, - ) ?: return@transform null, - selectedNetwork = selectedNetwork, - hasMoreNetworksAvailable = !selectedPortfolio.account.isSingleNetwork, - ) - }, - ) - .filterNotNull() - - private suspend fun createCryptoCurrency( - userWallet: UserWallet, - network: TokenMarketInfo.Network, - account: AvailableToAddAccount, - ): CryptoCurrency? { - val accountIndex = when (account.account) { - is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = addToPortfolioManager.token, - network = network, - accountIndex = accountIndex, - ) - } - - private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector { - return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio) - } - - private fun combineFeatureData() = addToPortfolioManager.state.onEach { state -> - when (state) { - is AddToPortfolioManager.State.AvailableToAdd -> - portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> - val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId] - ?: return@isEnabled false - val isAvailableAccount = - availableWallet.availableToAddAccounts[accountStatus.account.accountId] - ?.isAvailableToAdd == true - return@isEnabled isAvailableAccount - } - AddToPortfolioManager.State.Init, - AddToPortfolioManager.State.NothingToAdd, - -> Unit - } - } -} - -@ModelScoped -internal class AddToPortfolioCallbackDelegate @Inject constructor() : - ChooseNetworkComponent.Callbacks, - TokenActionsComponent.Callbacks, - AddTokenComponent.Callbacks { - - val onNetworkSelected = Channel() - val onLaterClick = Channel() - val onChangeNetworkClick = Channel() - val onChangePortfolioClick = Channel() - val onTokenAdded = Channel() - - override fun onNetworkSelected(network: TokenMarketInfo.Network) { - onNetworkSelected.trySend(network) - } - - override fun onLaterClick() { - onLaterClick.trySend(Unit) - } - - override fun onChangeNetworkClick() { - onChangeNetworkClick.trySend(Unit) - } - - override fun onChangePortfolioClick() { - onChangePortfolioClick.trySend(Unit) - } - - override fun onTokenAdded(status: CryptoCurrencyStatus) { - onTokenAdded.trySend(status) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt deleted file mode 100644 index 4f46fac1d3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import androidx.compose.runtime.Immutable -import com.tangem.core.decompose.navigation.Route -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import kotlinx.serialization.Serializable - -@Serializable -@Immutable -internal sealed interface AddToPortfolioRoutes : Route { - - @Serializable - data object Empty : AddToPortfolioRoutes - - @Serializable - data object PortfolioSelector : AddToPortfolioRoutes - - @Serializable - data class NetworkSelector( - val selectedPortfolio: SelectedPortfolio, - ) : AddToPortfolioRoutes - - @Serializable - data object AddToken : AddToPortfolioRoutes - - @Serializable - data object TokenActions : AddToPortfolioRoutes -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt deleted file mode 100644 index be45fedb3a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.common.ui.addtoken.AddTokenUM -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 -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase -import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase -import com.tangem.domain.models.account.Account -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class AddTokenModel @Inject constructor( - paramsContainer: ParamsContainer, - private val uiBuilder: AddTokenUiBuilder, - private val messageSender: UiMessageSender, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val checkCurrencyUnsupportedDelegate: CheckCurrencyUnsupportedDelegate, -) : Model() { - - private val params = paramsContainer.require() - private val analyticsEventBuilder = params.eventBuilder - private val addTokenJob = JobHolder() - - val uiState: StateFlow - field = MutableStateFlow(value = null) - - init { - combine( - flow = params.selectedNetwork.distinctUntilChanged(), - flow2 = params.selectedPortfolio.distinctUntilChanged(), - transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.join() - val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) - uiBuilder.updateContent( - selectedPortfolio = selectedPortfolio, - selectedNetwork = selectedNetwork, - isTangemIconVisible = isTangemIconVisible, - onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) }, - ) - }, - ) - .onEach { newUI -> uiState.value = newUI } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } - - private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) = - modelScope.launch(dispatchers.default) { - val um = uiState.value ?: return@launch - - val cryptoCurrency = selectedNetwork.cryptoCurrency - val account = selectedPortfolio.account.account.account - val accountId = account.accountId - val isMainNetwork = selectedNetwork.selectedNetwork.contractAddress == null - - val unsupportedCurrency = checkCurrencyUnsupportedDelegate.checkCurrencyUnsupportedState( - userWalletId = accountId.userWalletId, - rawNetworkId = selectedNetwork.selectedNetwork.networkId, - isMainNetwork = isMainNetwork, - ) - - if (unsupportedCurrency != null) return@launch - - uiState.value = um.toggleProgress(true) - val blockchainNames = listOf(selectedNetwork.selectedNetwork) - .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) - analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) - - manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) - .onLeft { error -> - processError(error = error) - uiState.value = um.toggleProgress(false) - return@launch - } - - val status = getAccountCurrencyStatusUseCase( - userWalletId = accountId.userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ).firstOrNull() - if (status == null) { - processError(error = null) - } else { - when (account) { - is Account.CryptoPortfolio -> if (!account.isMainAccount) { - analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount()) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - - analyticsEventHandler.send( - event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), - ) - - params.callbacks.onTokenAdded(status.status) - } - uiState.value = um.toggleProgress(false) - } - - private suspend fun needColdWalletInteraction( - selectedNetwork: SelectedNetwork, - selectedPortfolio: SelectedPortfolio, - ): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedPortfolio.userWallet.walletId, - networksWithDerivationPath = mapOf( - selectedNetwork.selectedNetwork.networkId to selectedNetwork.cryptoCurrency.network.derivationPath.value, - ), - ) - - private fun processError(error: Throwable?) { - val message = error?.message?.let { stringReference(it) } - ?: resourceReference(R.string.common_something_went_wrong) - messageSender.send(ToastMessage(message = message)) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt deleted file mode 100644 index ec88d88216..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.common.ui.account.AccountIconUM -import com.tangem.common.ui.account.CryptoPortfolioIconConverter -import com.tangem.common.ui.account.PortfolioSelectUM -import com.tangem.common.ui.account.toUM -import com.tangem.common.ui.addtoken.AddTokenUM -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.account.AccountStatus.* -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import javax.inject.Inject - -@ModelScoped -internal class AddTokenUiBuilder @Inject constructor( - paramsContainer: ParamsContainer, -) { - private val params = paramsContainer.require() - - private fun createNetwork(selectedNetwork: SelectedNetwork): AddTokenUM.Network { - return AddTokenUM.Network( - icon = selectedNetwork.cryptoCurrency.network.iconResId, - name = stringReference(selectedNetwork.cryptoCurrency.network.name), - editable = selectedNetwork.hasMoreNetworksAvailable, - onClick = { params.callbacks.onChangeNetworkClick() }, - ) - } - - private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM? - val portfolioName: TextReference - when (selectedPortfolio.isAccountMode) { - false -> { - accountIcon = null - portfolioName = stringReference(selectedPortfolio.userWallet.name) - } - true -> { - val accountStatus = selectedPortfolio.account.account - portfolioName = accountStatus.account.accountName.toUM().value - accountIcon = when (accountStatus) { - is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) - is Payment -> AccountIconUM.Payment - } - } - } - return PortfolioSelectUM( - icon = accountIcon, - name = portfolioName, - isAccountMode = selectedPortfolio.isAccountMode, - isMultiChoice = selectedPortfolio.hasMorePortfoliosAvailable, - onClick = { params.callbacks.onChangePortfolioClick() }, - ) - } - - fun updateContent( - selectedPortfolio: SelectedPortfolio, - selectedNetwork: SelectedNetwork, - isTangemIconVisible: Boolean, - onConfirmClick: () -> Unit, - ): AddTokenUM { - // its may happens when change portfolio after selected both params in line navigation - val isAvailableNetwork = selectedPortfolio.account.availableToAddNetworks - .any { selectedNetwork.selectedNetwork.networkId == it.networkId } - val button = AddTokenUM.Button( - isEnabled = isAvailableNetwork, - showProgress = false, - isTangemIconVisible = isTangemIconVisible, - text = resourceReference(R.string.common_add), - onConfirmClick = onConfirmClick, - ) - val networkUM = createNetwork(selectedNetwork) - val portfolioUM = createPortfolio(selectedPortfolio) - val currency = selectedNetwork.cryptoCurrency - val tokenToAdd = TokenItemState.Content( - id = currency.id.value, - iconState = CryptoCurrencyToIconStateConverter().convert(currency), - titleState = TokenItemState.TitleState.Content(stringReference(currency.name)), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""), - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)), - onItemClick = null, - onItemLongClick = null, - ) - return AddTokenUM( - tokenToAdd = tokenToAdd, - network = networkUM, - portfolio = portfolioUM, - button = button, - ) - } - - companion object { - - fun AddTokenUM.toggleProgress(showProgress: Boolean) = this.copy( - button = this.button.copy(showProgress = showProgress), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt deleted file mode 100644 index f6d5e737b6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import arrow.core.getOrElse -import com.tangem.core.decompose.ui.UiMessageSender -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.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.markets.impl.R -import timber.log.Timber -import javax.inject.Inject - -class CheckCurrencyUnsupportedDelegate @Inject constructor( - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val messageSender: UiMessageSender, -) { - - suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - val result = checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { throwable -> - Timber.e( - throwable, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = throwable.localizedMessage?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - - if (result != null) { - showUnsupportedWarning(result) - } - return result - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - ) - - messageSender.send(message) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt deleted file mode 100644 index 4834f7cbbb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -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.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM -import com.tangem.features.markets.portfolio.impl.model.BlockchainRowUMConverter -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class ChooseNetworkModel @Inject constructor( - paramsContainer: ParamsContainer, - private val checkCurrencyUnsupportedDelegate: CheckCurrencyUnsupportedDelegate, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - val uiState: StateFlow = MutableStateFlow(buildUI()) - - private fun buildUI(): ChooseNetworkUM { - val allAvailable = params.selectedPortfolio.account.availableNetworks - val alreadyAdded = allAvailable - .subtract(params.selectedPortfolio.account.availableToAddNetworks) - val converter = BlockchainRowUMConverter( - alreadyAddedNetworks = alreadyAdded.mapTo(mutableSetOf()) { it.networkId }, - ) - val allAvailableNetworks = allAvailable.map { it to true } - return ChooseNetworkUM( - networks = converter.convertList(allAvailableNetworks).toPersistentList(), - onNetworkClick = onNetworkClick@{ row -> - val network = allAvailable - .find { it.networkId == row.id } - ?: return@onNetworkClick - checkNetwork(row, network) - }, - ) - } - - private fun checkNetwork(row: BlockchainRowUM, network: TokenMarketInfo.Network) = modelScope.launch { - val selectedWalletId = params.selectedPortfolio.userWallet.walletId - val unsupportedState = checkCurrencyUnsupportedDelegate.checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = row.id, - isMainNetwork = row.isMainNetwork, - ) - if (unsupportedState == null) { - params.callbacks.onNetworkSelected(network) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt deleted file mode 100644 index 466befc011..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -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 -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler.HandledQuickAction -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class TokenActionsModel @Inject constructor( - paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val uiBuilder: TokenActionsUiBuilder, - private val analyticsEventHandler: AnalyticsEventHandler, - private val receiveAddressesFactory: ReceiveAddressesFactory, -) : Model() { - - private val params = paramsContainer.require() - private val analyticsEventBuilder get() = params.eventBuilder - private val currentAppCurrency = getSelectedAppCurrencyUseCase.invokeOrDefault() - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler: TokenActionsHandler = - tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) }, - ) - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val uiState: StateFlow = params.data - .mapLatest { uiBuilder.build(it, tokenActionsHandler) } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - - private fun handledQuickAction(handledAction: HandledQuickAction) { - val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action) - analyticsEventHandler.send(event) - val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive - if (!isReceive) return - modelScope.launch { - val tokenConfig = receiveAddressesFactory.create( - status = handledAction.cryptoCurrencyData.status, - userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId, - ) ?: return@launch - bottomSheetNavigation.activate(tokenConfig) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt deleted file mode 100644 index 6a1dcb993e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler -import javax.inject.Inject - -@ModelScoped -internal class TokenActionsUiBuilder @Inject constructor( - paramsContainer: ParamsContainer, - private val analyticsEventHandler: AnalyticsEventHandler, -) { - private val params = paramsContainer.require() - - fun build(data: PortfolioData.CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM { - val status = data.status - val tokenUM = TokenItemState.Content( - id = status.currency.id.value, - iconState = CryptoCurrencyToIconStateConverter().convert(status.currency), - titleState = TokenItemState.TitleState.Content(stringReference(status.currency.name)), - fiatAmountState = null, - subtitle2State = null, - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(status.currency.symbol)), - onItemClick = null, - onItemLongClick = null, - ) - return TokenActionsUM( - token = tokenUM, - onLaterClick = { - analyticsEventHandler.send(params.eventBuilder.getTokenLater()) - params.callbacks.onLaterClick() - }, - quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt deleted file mode 100644 index e2f26ae35b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -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.util.fastForEachIndexed -import com.tangem.core.ui.components.label.Label -import com.tangem.core.ui.components.label.entity.LabelStyle -import com.tangem.core.ui.components.label.entity.LabelUM -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -private const val DISABLED_ALPHA = 0.4f - -@Composable -internal fun ChooseNetworkContent(state: ChooseNetworkUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.action), - ) { - state.networks.fastForEachIndexed { index, model -> - key(model.id) { - BlockchainRow( - model = model, - itemPadding = PaddingValues( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing14, - ), - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = model.isEnabled, onClick = { state.onNetworkClick(model) }), - ) { - if (!model.isEnabled) { - Label( - modifier = Modifier.alpha(DISABLED_ALPHA), - state = LabelUM( - text = resourceReference(R.string.common_added), - style = LabelStyle.REGULAR, - ), - ) - } - } - } - } - } -} - -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview(@PreviewParameter(ChooseNetworkContentProvider::class) content: ChooseNetworkUM) { - TangemThemePreview { - ChooseNetworkContent( - state = content, - ) - } -} - -internal class ChooseNetworkContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = UUID.randomUUID().toString(), - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.img_eth_22, - isMainNetwork = false, - isSelected = true, - isEnabled = true, - ) - - override val values: Sequence - get() = sequenceOf( - ChooseNetworkUM( - onNetworkClick = {}, - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - ), - blockchainRow.copy( - iconResId = R.drawable.ic_bsc_16, - isEnabled = false, - ), - blockchainRow.copy(iconResId = R.drawable.img_polygon_22), - blockchainRow.copy(iconResId = R.drawable.img_optimism_22), - ), - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt deleted file mode 100644 index e396ef008e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager.State -import com.tangem.features.markets.portfolio.add.impl.converter.AvailableToAddDataConverter -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -internal class DefaultAddToPortfolioManager @AssistedInject constructor( - private val availableToAddDataConverter: AvailableToAddDataConverter, - @Assisted override val token: TokenMarketParams, - @Assisted override val analyticsParams: MarketsPortfolioComponent.AnalyticsParams?, - @Assisted val scope: CoroutineScope, - dispatchers: CoroutineDispatcherProvider, - portfolioFetcherFactory: PortfolioFetcher.Factory, -) : AddToPortfolioManager { - - private val _allAvailableNetworks = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val allAvailableNetworks: Flow> = _allAvailableNetworks.asSharedFlow() - override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), - scope = scope, - ) - - override val state: StateFlow = - combine( - flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(), - flow2 = allAvailableNetworks.map { it.toSet() }.distinctUntilChanged(), - ) { balances, availableNetworks -> - val data = availableToAddDataConverter.convert( - balances = balances, - availableNetworks = availableNetworks, - marketParams = token, - ) - if (data.isAvailableToAdd) { - State.AvailableToAdd(data) - } else { - State.NothingToAdd - } - } - .flowOn(dispatchers.default) - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = State.Init, - ) - - override fun setTokenNetworks(networks: List) { - _allAvailableNetworks.tryEmit(networks) - } - - @AssistedFactory - interface Factory : AddToPortfolioManager.Factory { - override fun create( - scope: CoroutineScope, - token: TokenMarketParams, - analyticsParams: MarketsPortfolioComponent.AnalyticsParams?, - ): DefaultAddToPortfolioManager - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt deleted file mode 100644 index e0b24bf672..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.icons.badge.drawBadge -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -@Composable -internal fun TokenActionsContent(state: TokenActionsUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - ) { - TokenItem( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action), - state = state.token, - isBalanceHidden = false, - ) - - SpacerH(TangemTheme.dimens.spacing14) - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.action), - ) { - state.quickActions.actions.fastForEach { action -> - key(action.title) { - ActionRow( - state = action, - onClick = { state.quickActions.onQuickActionClick(action) }, - onLongClick = { state.quickActions.onQuickActionLongClick(action) }, - ) - } - } - } - - SpacerH16() - - SecondaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_later), - onClick = state.onLaterClick, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun ActionRow( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit), - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable( - onLongClick = onLongClickInternal.takeIf { state.isLongClickAvailable }, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing15), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - val containerColor = TangemTheme.colors.background.action - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), - shape = CircleShape, - ) - .size(36.dp) - .drawWithContent { - drawContent() - if (state is QuickActionUM.Exchange && state.shouldShowBadge) { - drawBadge(containerColor = containerColor, offset = 4.dp) - } - }, - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(TokenActionsContentPreviewProvider::class) state: TokenActionsUM) { - TangemThemePreview { - TokenActionsContent( - state = state, - ) - } -} - -private class TokenActionsContentPreviewProvider : PreviewParameterProvider { - private val tokenState - get() = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_eth_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content( - text = stringReference(value = "Tether"), - ), - fiatAmountState = null, - subtitle2State = null, - subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")), - onItemClick = {}, - onItemLongClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - TokenActionsUM( - quickActions = PortfolioTokenUM.QuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - onQuickActionClick = {}, - onQuickActionLongClick = {}, - ), - token = tokenState, - onLaterClick = {}, - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt deleted file mode 100644 index 8e27218757..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -data class ChooseNetworkUM( - val networks: ImmutableList, - val onNetworkClick: (BlockchainRowUM) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt deleted file mode 100644 index cb2466e02a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM - -internal data class TokenActionsUM( - val token: TokenItemState, - val quickActions: PortfolioTokenUM.QuickActions, - val onLaterClick: () -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt deleted file mode 100644 index 62babbdc62..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.features.markets.portfolio.api - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import kotlinx.serialization.Serializable - -@Stable -interface MarketsPortfolioComponent : ComposableContentComponent { - - @Serializable - data class Params( - val token: TokenMarketParams, - val analyticsParams: AnalyticsParams?, - ) - - @Serializable - data class AnalyticsParams( - val source: String, - ) - - fun setTokenNetworks(networks: List) - - fun setNoNetworksAvailable() - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt deleted file mode 100644 index 200d6916bb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.markets.portfolio.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioRoute -import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio -import com.tangem.features.tokenreceive.TokenReceiveComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: MarketsPortfolioComponent.Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, - private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, -) : AppComponentContext by context, MarketsPortfolioComponent { - - private val model: MarketsPortfolioModel = getOrCreateModel(params) - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = MarketsPortfolioRoute.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - override fun setTokenNetworks(networks: List) { - model.setTokenNetworks(networks) - } - - override fun setNoNetworksAvailable() { - model.setNoNetworksAvailable() - } - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - MyPortfolio(modifier = modifier, state = state) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: MarketsPortfolioRoute, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create( - context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = requireNotNull(model.newAddToPortfolioManager) { - "newAddToPortfolioManager must be initialized" - }, - callback = model.addToPortfolioCallback, - ), - ) - is MarketsPortfolioRoute.TokenReceive -> tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config.config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - } - - @AssistedFactory - interface Factory : MarketsPortfolioComponent.Factory { - override fun create( - context: AppComponentContext, - params: MarketsPortfolioComponent.Params, - ): DefaultMarketsPortfolioComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt deleted file mode 100644 index 2658bb0659..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM - -internal class PortfolioAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { - - data class EventBuilder( - val token: TokenMarketParams, - val source: String?, - ) { - - fun addToPortfolioClicked() = PortfolioAnalyticsEvent( - event = "Button - Add To Portfolio", - params = buildMap { - put("Token", token.symbol) - if (source != null) put("Source", source) - }, - ) - - fun popupToChooseAccount() = PortfolioAnalyticsEvent( - event = "Choose Account Opened", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun popupToConfirm() = PortfolioAnalyticsEvent( - event = "Add Token Screen Opened", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addToNotMainAccount() = PortfolioAnalyticsEvent( - event = "Button - Add To Account", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addButtonClick() = PortfolioAnalyticsEvent( - event = "Button - Add Token", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( - event = "Wallet Selected", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( - event = "Token Network Selected", - params = buildMap { - put("Count", blockchainNames.size.toString()) - put("Token", token.symbol) - put("blockchain", blockchainNames.joinToString(separator = ", ")) - if (source != null) put("Source", source) - }, - ) - - fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( - event = "Token Added", - params = buildMap { - put("Token", token.symbol) - put("Blockchain", blockchainName) - if (source != null) put("Source", source) - }, - ) - - fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = - PortfolioAnalyticsEvent( - event = when (actionUM) { - TokenActionsBSContentUM.Action.Buy -> "Button - Buy" - TokenActionsBSContentUM.Action.Receive -> "Button - Receive" - TokenActionsBSContentUM.Action.Exchange -> "Button - Swap" - TokenActionsBSContentUM.Action.Stake -> "Button - Stake" - TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode" - else -> "error" - }, - params = buildMap { - put("Token", token.symbol) - if (source != null) put("Source", source) - put("blockchain", blockchainName) - }, - ) - - fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent( - event = when (actionUM) { - TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy" - TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive" - TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange" - TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" - else -> "error" - }, - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun getTokenLater() = PortfolioAnalyticsEvent( - event = "Popup Get token - Button Later", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt deleted file mode 100644 index d011fbf799..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.di - -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent -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 ComponentModule { - - @Binds - @Singleton - fun bindMarketsPortfolioComponent( - factory: DefaultMarketsPortfolioComponent.Factory, - ): MarketsPortfolioComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt deleted file mode 100644 index 2b35fe3c10..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface ModelModule { - - @Binds - @IntoMap - @ClassKey(MarketsPortfolioModel::class) - fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt deleted file mode 100644 index ac34715c4d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.loader - -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.TokenActionsState - -/** - * Portfolio data. Combined data from all flows that required to setup portfolio - * - * @property walletsWithCurrencies wallets with crypto currency statuses - * @property appCurrency app currency - * @property isBalanceHidden flag that indicates if balance should be hidden - * @property walletsWithBalance wallets with total balance - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioData( - val walletsWithCurrencies: Map>, - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, - val walletsWithBalance: Map>, -) { - data class CryptoCurrencyData( - val userWallet: UserWallet, - val status: CryptoCurrencyStatus, - val actions: List, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt deleted file mode 100644 index 0dadd3bcf4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.loader - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Loader of portfolio data - * - * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses - * @property getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings - * @property getWalletTotalBalanceUseCase use case for getting wallet total balance - * -[REDACTED_AUTHOR] - */ -internal class PortfolioDataLoader @Inject constructor( - private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) { - - /** Load data by [currencyRawId] */ - @OptIn(ExperimentalCoroutinesApi::class) - fun load(currencyRawId: CryptoCurrency.RawID): Flow { - return combine( - flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> - PortfolioData( - walletsWithCurrencies = walletsWithCurrencies, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - walletsWithBalance = emptyMap(), - ) - } - // setup balances for wallets from walletsWithCurrencyStatuses - .flatMapLatest { portfolioData -> - getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), - ) - .map { portfolioData.copy(walletsWithBalance = it) } - .onEmpty { emit(portfolioData) } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun getAllWalletsCryptoCurrenciesData( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) - .distinctUntilChanged() - .map { walletsWithMaybeStatuses -> - walletsWithMaybeStatuses.mapValues { entry -> - entry.value.mapNotNull { it.getOrNull() } - } - } - .flatMapLatest { walletsWithStatuses -> - val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> - statuses.map { status -> - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { - YieldSupplyAvailability.Unavailable - } - getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) - .map { actionStates -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = actionStates.states, - ) - } - } - } - - combine(actionsFlows) { actions -> - walletsWithStatuses.mapValues { entry -> - entry.value.mapNotNull { status -> - actions.firstOrNull { data -> - data.userWallet == entry.key && data.status == status - } - } - } - }.onEmpty { - emit( - walletsWithStatuses.mapValues { (wallet, statuses) -> - statuses.map { status -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = emptyList(), - ) - } - }, - ) - } - }.onEmpty { - emit(emptyMap()) - } - .distinctUntilChanged() - } - - private fun getWalletsWithTotalBalanceFlow( - ids: List, - ): Flow>> { - return combine( - flows = ids - .map { userWalletId -> - getWalletTotalBalanceUseCase(userWalletId) - .map { userWalletId to it } - .distinctUntilChanged() - }, - transform = { it.toMap() }, - ) - .distinctUntilChanged() - .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt deleted file mode 100644 index bf294a2895..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.toImmutableList - -/** - * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] - * - * @property token token params - * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed - * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed - * @property onNetworkSwitchClick callback is invoked when network switch is clicked - * @property onAnotherWalletSelect callback is invoked when wallet is selected - * @property onContinueClick callback is invoked when continue button is clicked - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class AddToPortfolioBSContentUMFactory( - private val addToPortfolioManager: AddToPortfolioManager, - private val token: TokenMarketParams, - private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, - private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, - private val onAnotherWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, -) { - - /** - * Create [TangemBottomSheetConfig] - * - - * @param portfolioData portfolio data - * @param portfolioUIData portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param alreadyAddedNetworks already added networks - */ - @Suppress("LongParameterList") - fun create( - currentState: TangemBottomSheetConfig?, - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - selectedWallet: UserWallet?, - alreadyAddedNetworks: Set?, - artworks: Map, - ): TangemBottomSheetConfig { - return (currentState ?: TangemBottomSheetConfig.Empty).copy( - isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible, - onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, - content = if (selectedWallet != null && alreadyAddedNetworks != null) { - AddToPortfolioBSContentUM( - selectedWallet = selectedWallet.toSelectedUserWalletItemUM( - portfolioData = portfolioData, - balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(), - artwork = artworks[selectedWallet.walletId], - ), - selectNetworkUM = SelectNetworkUMConverter( - networksWithToggle = addToPortfolioManager.associateWithToggle( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - addToPortfolioData = portfolioUIData.addToPortfolioData, - ), - alreadyAddedNetworks = alreadyAddedNetworks, - onNetworkSwitchClick = onNetworkSwitchClick, - ).convert(value = token), - isScanCardNotificationVisible = portfolioUIData.shouldRequireColdWalletInteraction, - isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks( - userWalletId = selectedWallet.walletId, - ), - onContinueButtonClick = { - onContinueClick( - selectedWallet.walletId, - portfolioUIData.addToPortfolioData.getAddedNetworks( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - ), - ) - }, - walletSelectorConfig = createWalletSelectorBSConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible, - portfolioData = portfolioData, - selectedWalletId = selectedWallet.walletId, - artworks = artworks, - ), - isWalletBlockVisible = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency).size > 1, - ) - } else { - TangemBottomSheetConfigContent.Empty - }, - ) - } - - private fun UserWallet.toSelectedUserWalletItemUM( - artwork: UserWalletItemUM.ImageState? = null, - portfolioData: PortfolioData, - balance: TotalFiatBalance?, - ): UserWalletItemUM { - return UserWalletItemUMConverter( - onClick = { onWalletSelectorVisibilityChange(true) }, - endIcon = UserWalletItemUM.EndIcon.Arrow, - balance = balance, - artwork = artwork, - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - ).convert(value = this) - } - - private fun createWalletSelectorBSConfig( - isShow: Boolean, - portfolioData: PortfolioData, - selectedWalletId: UserWalletId, - artworks: Map, - ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShown = isShow, - onDismissRequest = { onWalletSelectorVisibilityChange(false) }, - content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency) - .map { it.key } - .map { userWallet -> - val balance = portfolioData.walletsWithBalance[userWallet.walletId] - - UserWalletItemUMConverter( - onClick = { walletId -> - if (walletId != selectedWalletId) { - onAnotherWalletSelect(walletId) - onWalletSelectorVisibilityChange(false) - } - }, - appCurrency = portfolioData.appCurrency, - balance = balance?.getOrNull(), - isBalanceHidden = portfolioData.isBalanceHidden, - endIcon = if (userWallet.walletId == selectedWalletId) { - UserWalletItemUM.EndIcon.Checkmark - } else { - UserWalletItemUM.EndIcon.None - }, - artwork = artworks[userWallet.walletId], - ).convert(userWallet) - } - .toImmutableList(), - onBack = { onWalletSelectorVisibilityChange(false) }, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt deleted file mode 100644 index 15718128de..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update -import timber.log.Timber -import javax.inject.Inject - -internal typealias WalletsWithNetworks = Map> - -/** - * Manager for tracking changing networks in AddToPortfolio - * -[REDACTED_AUTHOR] - */ -internal class AddToPortfolioManager @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, -) { - - val availableNetworks = MutableStateFlow?>(value = null) - private val addedNetworks = MutableStateFlow(value = emptyMap()) - private val removedNetworks = MutableStateFlow(value = emptyMap()) - - /** Get [AddToPortfolioData] as flow */ - fun getAddToPortfolioData(): Flow { - return combine( - flow = availableNetworks, - flow2 = addedNetworks, - flow3 = removedNetworks, - transform = ::AddToPortfolioData, - ) - } - - /** Set available networks [networks] */ - fun setAvailableNetworks(networks: List) { - availableNetworks.value = networks.toSet() - } - - /** Add network [networkId] to [userWalletId] */ - fun addNetwork(userWalletId: UserWalletId, networkId: String) { - addedNetworks.add(userWalletId, networkId) - - removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId) - } - - /** Remove network [networkId] from [userWalletId] */ - fun removeNetwork(userWalletId: UserWalletId, networkId: String) { - removedNetworks.add(userWalletId, networkId) - - addedNetworks.cancelPrevChangeIfExist( - userWalletId = userWalletId, - networkId = networkId, - ) - } - - /** Remove all networks by [userWalletId] */ - fun removeAllChanges(userWalletId: UserWalletId) { - addedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - - removedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - } - - fun associateWithToggle( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - addToPortfolioData: AddToPortfolioData, - ): Map { - val filteredNetworks = filterAvailableNetworksForWalletUseCase( - userWalletId = userWalletId, - networks = addToPortfolioData.availableNetworks.orEmpty(), - ) - // Use user choice or check already added networks - return filteredNetworks.associateWith { availableNetwork -> - val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork) - - if (isAddedByUser == true) return@associateWith true - - val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork) - - if (isRemovedByUser == true) return@associateWith false - - val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId } - - isAddedBefore - } - } - - private fun MutableStateFlow.cancelPrevChangeIfExist( - userWalletId: UserWalletId, - networkId: String, - ) { - if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId) - } - - private fun MutableStateFlow.add(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = true) - } - - private fun MutableStateFlow.remove(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = false) - } - - private fun MutableStateFlow.change( - userWalletId: UserWalletId, - networkId: String, - isAddAction: Boolean, - ) { - val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId } - - if (network == null) { - Timber.d( - "Network [$networkId] doesn't contain in available networks [%s]", - availableNetworks.value?.joinToString { it.networkId }, - ) - - return - } - - update { currentMap -> - currentMap.toMutableMap().apply { - this[userWalletId] = if (isAddAction) { - this[userWalletId].orEmpty() + network - } else { - this[userWalletId].orEmpty() - network - } - } - } - } - - /** - * Add to portfolio data - * - * @property availableNetworks available networks that user can add to portfolio - * @property addedNetworks networks that user toggled on, but it might have already been added to the wallet - * @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet - * - * Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just - * toggle it. But when we will save user changes, we will check what tokens have already been added or - * haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks] - */ - data class AddToPortfolioData( - val availableNetworks: Set?, - val addedNetworks: WalletsWithNetworks, - val removedNetworks: WalletsWithNetworks, - ) { - - fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() || - removedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - /** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */ - fun getAddedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val addedNetworksByUser = addedNetworks[userWalletId].orEmpty() - - return addedNetworksByUser.map { it.networkId } - .minus(alreadyAddedNetworkIds) - .mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - - /** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */ - fun getRemovedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val removedNetworksByUser = removedNetworks[userWalletId].orEmpty() - - return alreadyAddedNetworkIds - .minus(removedNetworksByUser.map { it.networkId }.toSet()) - .mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt deleted file mode 100644 index c9ec3f67cb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.converter.Converter - -/** - * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] - * - * @property alreadyAddedNetworks set of already added networks - * -[REDACTED_AUTHOR] - */ -internal class BlockchainRowUMConverter( - private val alreadyAddedNetworks: Set, -) : Converter, BlockchainRowUM> { - - override fun convert(value: Pair): BlockchainRowUM { - val (network, isSelected) = value - - val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId) - ?: error("Can't find blockchain info for ${network.networkId}") - - val isMainNetwork = network.contractAddress == null - - val isEnabled = !alreadyAddedNetworks.contains(network.networkId) - - return BlockchainRowUM( - id = network.networkId, - name = blockchainInfo.name, - type = getNetworkType(network, blockchainInfo), - iconResId = if (isEnabled) { - if (isSelected) { - getActiveIconRes(blockchainInfo.blockchainId) - } else { - getGreyedOutIconRes(blockchainInfo.blockchainId) - } - } else { - getGreyedOutIconRes(blockchainInfo.blockchainId) - }, - isMainNetwork = isMainNetwork, - isSelected = isSelected, - isEnabled = isEnabled, - ) - } - - private fun getNetworkType( - network: TokenMarketInfo.Network, - blockchainInfo: BlockchainUtils.BlockchainInfo, - ): String { - val isMainNetwork = network.contractAddress == null - return when { - BlockchainUtils.isL2Network(networkId = network.networkId) -> MAIN_NETWORK_L2_TYPE_NAME - isMainNetwork -> MAIN_NETWORK_TYPE_NAME - else -> blockchainInfo.protocolName - } - } - - private companion object { - const val MAIN_NETWORK_TYPE_NAME = "MAIN" - const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2" - } -} \ No newline at end of file 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 deleted file mode 100644 index 0157de78d9..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ /dev/null @@ -1,426 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import androidx.compose.runtime.Stable -import arrow.core.getOrElse -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -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 -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.markets.SaveMarketTokensUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.features.wallet.utils.UserWalletImageFetcher -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.operations.attestation.ArtworkSize -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager as NewAddToPortfolioManager - -@Suppress("LongParameterList", "LargeClass") -@Stable -@ModelScoped -internal class MarketsPortfolioModel @Inject constructor( - paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val messageSender: UiMessageSender, - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - private val portfolioDataLoader: PortfolioDataLoader, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val addToPortfolioManager: AddToPortfolioManager, - private val analyticsEventHandler: AnalyticsEventHandler, - private val userWalletImageFetcher: UserWalletImageFetcher, - private val receiveAddressesFactory: ReceiveAddressesFactory, - accountsFeatureToggles: AccountsFeatureToggles, - newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory, - newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory, -) : Model() { - - private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - val state: StateFlow get() = _state - - private val params = paramsContainer.require() - private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = params.token, - source = params.analyticsParams?.source, - ) - - val newAddToPortfolioManager: NewAddToPortfolioManager? - val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate? - - /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ - private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) - - private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - } - - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler = tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> - val currencyNetwork = handledAction.cryptoCurrencyData.status.currency.network - analyticsEventHandler.send( - analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = currencyNetwork.name, - ), - ) - configureReceiveAddresses(handledAction) - }, - ) - - private val factory = MyPortfolioUMFactory( - onAddClick = { - onAddToPortfolioBSVisibilityChange(isShow = true) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioClicked(), - ) - }, - addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( - addToPortfolioManager = addToPortfolioManager, - token = params.token, - onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, - onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, - onNetworkSwitchClick = ::onNetworkSwitchClick, - onAnotherWalletSelect = { walletId -> - onWalletSelect(walletId) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioWalletChanged(), - ) - }, - onContinueClick = { selectedWalletId, addedNetworks -> - onContinueClick(selectedWalletId, addedNetworks) - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioContinue( - blockchainNames = addedNetworks.mapNotNull { - BlockchainUtils.getNetworkInfo(it.networkId)?.name - }, - ), - ) - }, - ), - currentState = Provider { _state.value }, - tokenActionsHandler = tokenActionsHandler, - updateTokens = { updateBlock -> - updateTokensState { state -> - state.copy(tokens = updateBlock(state.tokens)) - } - }, - ) - - init { - if (accountsFeatureToggles.isFeatureEnabled) { - newAddToPortfolioManager = newAddToPortfolioManagerFactory - .create( - modelScope, - params.token, - params.analyticsParams, - ) - newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create( - scope = modelScope, - token = params.token, - tokenActionsHandler = tokenActionsHandler, - buttonState = newAddToPortfolioManager.state.map { managerState -> - when (managerState) { - is NewAddToPortfolioManager.State.AvailableToAdd -> AddButtonState.Available - NewAddToPortfolioManager.State.Init -> AddButtonState.Loading - NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable - } - }, - onAddClick = { - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) - bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) - }, - ) - newMarketsPortfolioDelegate.combineData() - .onEach { _state.value = it } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - newAddToPortfolioManager = null - newMarketsPortfolioDelegate = null - // Subscribe on selected wallet flow to support actual selected wallet - subscribeOnSelectedMultiWalletUpdates() - - subscribeOnStateUpdates() - } - } - - fun setTokenNetworks(networks: List) { - addToPortfolioManager.setAvailableNetworks(networks) - newAddToPortfolioManager?.setTokenNetworks(networks) - newMarketsPortfolioDelegate?.setTokenNetworks(networks) - } - - fun setNoNetworksAvailable() { - addToPortfolioManager.setAvailableNetworks(emptyList()) - newAddToPortfolioManager?.setTokenNetworks(emptyList()) - newMarketsPortfolioDelegate?.setTokenNetworks(emptyList()) - } - - private fun subscribeOnSelectedMultiWalletUpdates() { - getSelectedWalletUseCase() - .getOrElse { e -> - Timber.e("Failed to load selected wallet: $e") - error("Failed to load selected wallet") - } - .onEach { wallet -> - selectedMultiWalletIdFlow.value = wallet.takeIf { it.isMultiCurrency }?.walletId - } - .launchIn(modelScope) - } - - private fun subscribeOnStateUpdates() { - combine( - flow = loadPortfolioDataWithArtworks(params.token.id), - flow2 = getPortfolioUIDataFlow(), - transform = { pair, portfolioUIData -> - val (portfolioData, artworks) = pair - factory.create(portfolioData, portfolioUIData, artworks) - }, - ) - .onEach { _state.value = it } - .launchIn(modelScope) - } - - private fun loadPortfolioDataWithArtworks( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - val wallets = Channel>() - val portfolioFlow = portfolioDataLoader - .load(currencyRawId) - .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - - val artworksFlow = wallets.receiveAsFlow() - .distinctUntilChanged() - .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } - - return combine( - flow = portfolioFlow, - flow2 = artworksFlow, - ) { portfolioData, artworks -> portfolioData to artworks } - } - - private fun getPortfolioUIDataFlow(): Flow { - return combine( - flow = portfolioBSVisibilityModelFlow, - flow2 = selectedMultiWalletIdFlow, - flow3 = addToPortfolioManager.getAddToPortfolioData(), - transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> - PortfolioUIData( - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - selectedWalletId = selectedWalletId, - addToPortfolioData = addToPortfolioData, - shouldRequireColdWalletInteraction = needColdWalletInteraction( - selectedWalletId, - addToPortfolioData, - ), - ) - }, - ) - } - - private suspend fun needColdWalletInteraction( - selectedWalletId: UserWalletId?, - addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - ): Boolean { - return if (selectedWalletId != null) { - coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedWalletId, - networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty() - .associate { it.networkId to null }, - ) - } else { - false - } - } - - private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) { - val selectedWalletId = selectedMultiWalletIdFlow.value - - if (selectedWalletId == null) { - Timber.e("Impossible to switch network when selected wallet is null") - return - } - - if (isChecked) { - modelScope.launch { - val unsupportedState = checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = blockchainRowUM.id, - isMainNetwork = blockchainRowUM.isMainNetwork, - ) - if (unsupportedState != null) { - showUnsupportedWarning(unsupportedState) - } else { - addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - } else { - addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - - private suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - return checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { error -> - Timber.e( - error, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = error.localizedMessage - ?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - ) - - messageSender.send(message) - } - - private fun onWalletSelect(userWalletId: UserWalletId) { - selectedMultiWalletIdFlow.update { prevUserWalletId -> - prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) - - userWalletId - } - } - - private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set) { - modelScope.launch { - saveMarketTokensUseCase( - userWalletId = userWalletId, - tokenMarketParams = params.token, - addedNetworks = addedNetworks, - removedNetworks = emptySet(), - ) - - onAddToPortfolioBSVisibilityChange(isShow = false) - - addToPortfolioManager.removeAllChanges(userWalletId) - } - } - - private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false) - } - } - - private fun onWalletSelectorVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow) - } - } - - private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { - _state.update { stateToUpdate -> - val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate - block(tokensState) - } - } - - private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { - val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive - if (isNewReceive) { - modelScope.launch { - val tokenConfig = receiveAddressesFactory.create( - status = quickAction.cryptoCurrencyData.status, - userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, - ) ?: return@launch - bottomSheetNavigation.activate(MarketsPortfolioRoute.TokenReceive(tokenConfig)) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt deleted file mode 100644 index 576d9cda78..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.models.TokenReceiveConfig -import kotlinx.serialization.Serializable - -@Serializable -sealed interface MarketsPortfolioRoute : Route { - - @Serializable - data object AddToPortfolio : MarketsPortfolioRoute - - @Serializable - data class TokenReceive( - val config: TokenReceiveConfig, - ) : MarketsPortfolioRoute -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt deleted file mode 100644 index d5c027adb8..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList - -/** - * Factory for creating [MyPortfolioUM] - * - * @property onAddClick callback when user wants to add new token - * @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content - * @property tokenActionsHandler token actions handler - * @property currentState current state provider - * @property updateTokens callback for updating tokens - * -[REDACTED_AUTHOR] - */ -internal class MyPortfolioUMFactory( - private val onAddClick: () -> Unit, - private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, - private val tokenActionsHandler: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) { - - fun create( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): MyPortfolioUM { - val addToPortfolioData = portfolioUIData.addToPortfolioData - - val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true - if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable - - val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { - portfolioData.walletsWithCurrencies - } else { - portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) - } - - val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() - if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() - - return if (hasMultiWallets) { - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - ) - } else { - MyPortfolioUM.UnavailableForWallet - } - } - - return TokensPortfolioUMConverter( - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - addButtonState = walletsWithCurrencies.getAddButtonState( - availableNetworks = addToPortfolioData.availableNetworks, - ), - bsConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - quickActionsIntents = tokenActionsHandler, - currentState = currentState, - updateTokens = updateTokens, - ) - .convert(walletsWithCurrencies) - } - - private fun createAddToPortfolioBSConfig( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): TangemBottomSheetConfig { - val selectedWallet = portfolioData.walletsWithCurrencies.keys - .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } - ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency } - - val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() - - val alreadyAddedNetworks = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(availableNetworks)[selectedWallet] - ?.filter { !it.status.currency.isCustom } - ?.map { it.status.currency.network.backendId } - ?.toSet() - - return addToPortfolioBSContentUMFactory.create( - currentState = currentState().addToPortfolioBSConfig, - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - selectedWallet = selectedWallet, - alreadyAddedNetworks = alreadyAddedNetworks, - artworks = artworks, - ) - } - - private fun Map>.getAddButtonState( - availableNetworks: Set?, - ): AddButtonState { - if (availableNetworks == null) return AddButtonState.Loading - - val networkIds = availableNetworks.map { it.networkId } - - val isAllAvailableNetworksAdded = this - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available - } - - /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( - networks: Set, - ): Map> { - return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } - } - - /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( - networks: Set, - ): List { - val networkIds = networks.map(TokenMarketInfo.Network::networkId) - - return mapNotNull { currencyData -> - currencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt deleted file mode 100644 index 17acbaf94a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import arrow.core.getOrElse -import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.common.ui.account.CryptoPortfolioIconConverter -import com.tangem.common.ui.account.toUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioHeader -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioListItem -import com.tangem.features.markets.portfolio.impl.ui.state.WalletHeader -import com.tangem.utils.extensions.isZero -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -@OptIn(ExperimentalCoroutinesApi::class) -@Suppress("LongParameterList") -internal class NewMarketsPortfolioDelegate @AssistedInject constructor( - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val allAccountSupplier: MultiAccountStatusListSupplier, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - @Assisted private val scope: CoroutineScope, - @Assisted private val token: TokenMarketParams, - @Assisted private val tokenActionsHandler: TokenActionsHandler, - @Assisted private val buttonState: Flow, - @Assisted private val onAddClick: () -> Unit, -) { - - private val currencyRawId: CryptoCurrency.RawID = token.id - private var expandedHolder: MutableStateFlow>>? = null - - private val settingsFlow: Flow = combine( - flow = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - flow3 = isAccountsModeEnabledUseCase(), - transform = ::SettingsBox, - ).shareIn( - replay = 1, - started = SharingStarted.Eagerly, - scope = scope, - ).distinctUntilChanged() - - private val availableNetworks = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - fun setTokenNetworks(networks: List) { - availableNetworks.tryEmit(networks) - } - - fun combineData(): Flow { - return availableNetworks.transformLatest { availableNetworks -> - when { - availableNetworks.isEmpty() -> emit(MyPortfolioUM.Unavailable) - else -> emitAll(onAvailableNetworksFlow().distinctUntilChanged()) - } - }.distinctUntilChanged() - } - - private fun onAvailableNetworksFlow(): Flow = - portfolioWithThisCurrencyFLow().transformLatest { portfolioWithCurrency -> - when (portfolioWithCurrency.flattenAddedCurrency.isEmpty()) { - false -> emitAll(contentFlow(portfolioWithCurrency).distinctUntilChanged()) - true -> when (portfolioWithCurrency.hasMultiWallets) { - true -> emitAll(addFirstTokenFlow()) - false -> emit(MyPortfolioUM.UnavailableForWallet) - } - } - } - - private fun addFirstTokenFlow(): Flow = buttonState.map { state -> - when (state) { - AddButtonState.Loading -> MyPortfolioUM.Loading - AddButtonState.Available -> MyPortfolioUM.AddFirstToken( - onAddClick = onAddClick, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - ) - AddButtonState.Unavailable -> MyPortfolioUM.Unavailable - } - } - - private fun contentFlow(portfolio: PortfoliosWithThisCurrency): Flow { - fun Portfolio.actionsFoAccountCurrencies(): List>> = - accountsWithAdded.map { account -> - fun CryptoCurrencyStatus.actionsFlow(): Flow> = flow { - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(this@actionsFlow.currency) - .getOrElse { YieldSupplyAvailability.Unavailable } - emitAll( - getCryptoCurrencyActionsUseCase( - accountId = account.accountStatus.account.accountId, - currency = this@actionsFlow.currency, - yieldSupplyAvailability = yieldSupplyAvailability, - ).map { actionsState -> actionsState.cryptoCurrencyStatus.currency to actionsState }, - ) - } - account.addedCurrency.map { it.actionsFlow() } - }.flatten() - - val allAddedTokenActions = - portfolio.portfolios.map { portfolioItem -> portfolioItem.actionsFoAccountCurrencies() }.flatten() - - return combine( - flow = combine(allAddedTokenActions) { it.toMap() }.distinctUntilChanged(), - flow2 = buttonState.distinctUntilChanged(), - flow3 = getExpandedHolder(portfolio), - flow4 = settingsFlow.distinctUntilChanged(), - transform = { actions, addButtonState, expanded, settings -> - buildContentState( - portfolio = portfolio, - allActions = actions, - addButtonState = addButtonState, - expanded = expanded, - settings = settings, - ) - }, - ) - } - - private fun getExpandedHolder( - portfolio: PortfoliosWithThisCurrency, - ): StateFlow>> { - val expandedHolder = this.expandedHolder - if (expandedHolder != null) return expandedHolder - val allAddedCurrency = portfolio.flattenAddedCurrency - val shouldForceExpand = allAddedCurrency.size == 1 && - allAddedCurrency.first().value.amount?.isZero() == true - - val initValue = when { - shouldForceExpand -> { - val currency = allAddedCurrency.first() - // find userWallet than have this single added token - portfolio.portfolios - .find { it.accountsWithAdded.any { account -> account.addedCurrency.isNotEmpty() } } - ?.userWallet - ?.let { setOf(it.walletId to currency.currency.id) } - .orEmpty() - } - else -> emptySet() - } - return MutableStateFlow(initValue) - .also { this.expandedHolder = it } - } - - private fun portfolioWithThisCurrencyFLow(): Flow = - allAccountSupplier().map { list -> list.map { it.addedAccountsFlow() } }.flatMapLatest { flows -> - combine(flows) { portfolios -> - PortfoliosWithThisCurrency( - currencyRawId = currencyRawId, - portfolios = portfolios.toList(), - ) - } - }.distinctUntilChanged() - - private fun AccountStatusList.addedAccountsFlow(): Flow = - getUserWalletUseCase.invokeFlow(this.userWalletId).mapNotNull { it.getOrNull() }.map { wallet -> - Portfolio( - userWallet = wallet, - accountStatusList = this, - accountsWithAdded = this.filterByRawID(), - ) - }.distinctUntilChanged() - - private fun AccountStatusList.filterByRawID(): List { - fun AccountStatus.filterByRawID(): List = when (this) { - is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies() - .filter { status -> - val currencyId = status.currency.id.rawCurrencyId ?: return@filter false - getTokenIdIfL2Network(currencyId.value) == currencyRawId.value - } - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return accountStatuses.map { accountStatus -> - AccountWithAdded( - accountStatus = accountStatus, - addedCurrency = accountStatus.filterByRawID(), - ) - } - } - - private fun buildContentState( - portfolio: PortfoliosWithThisCurrency, - allActions: Map, - addButtonState: AddButtonState, - expanded: Set>, - settings: SettingsBox, - ): MyPortfolioUM.Content { - val appCurrency = settings.appCurrency - val isBalanceHidden = settings.isBalanceHidden - val isAccountMode = settings.isAccountMode - val uiItems: MutableList = mutableListOf() - - fun toggleQuickActions(key: Pair) = expandedHolder?.update { expanded -> - val isExpand = expanded.contains(key) - if (isExpand) expanded.minus(key) else expanded.plus(key) - } - - val tokenUMConverter = PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { }, - tokenActionsHandler = tokenActionsHandler, - ) - - portfolio.portfolios.forEach { portfolioItem -> - if (portfolioItem.flattenAddedCurrency.isEmpty()) return@forEach - val userWallet = portfolioItem.userWallet - if (isAccountMode) { - uiItems.add(portfolioItem.userWallet.toWalletHeader()) - } else { - uiItems.add(portfolioItem.userWallet.toWalletPortfolioHeader()) - } - - portfolioItem.accountsWithAdded.forEach { accountWithAdded -> - if (accountWithAdded.addedCurrency.isEmpty()) return@forEach - if (isAccountMode) { - val account = accountWithAdded.accountStatus.account - uiItems.add(account.toAccountPortfolioHeader()) - } - - accountWithAdded.addedCurrency.forEach { currencyStatus -> - val actions = allActions[currencyStatus.currency]?.states.orEmpty() - val value = PortfolioData.CryptoCurrencyData( - userWallet = userWallet, - status = currencyStatus, - actions = actions, - ) - val expandedKey = portfolioItem.userWallet.walletId to currencyStatus.currency.id - val isExpand = expanded.contains(expandedKey) - - val tokenItem = tokenUMConverter.convertV2( - onTokenItemClick = { wallet, status -> - toggleQuickActions(wallet.walletId to status.currency.id) - }, - value = value, - isQuickActionsShown = isExpand, - ) - uiItems.add(tokenItem) - } - } - } - - return MyPortfolioUM.Content( - items = uiItems.toImmutableList(), - buttonState = addButtonState, - onAddClick = onAddClick, - ) - } - - private fun Account.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader( - id = this.accountId.value, - state = AccountTitleUM.Account( - prefixText = TextReference.EMPTY, - name = this.accountName.toUM().value, - icon = when (this) { - is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon) - is Account.Payment -> TODO("[REDACTED_JIRA]") - }, - ), - ) - - private fun UserWallet.toWalletPortfolioHeader(): PortfolioHeader = PortfolioHeader( - id = this.walletId.stringValue, - state = AccountTitleUM.Text( - title = stringReference(this.name), - ), - ) - - private fun UserWallet.toWalletHeader(): WalletHeader = WalletHeader( - id = this.walletId.stringValue, - name = stringReference(this.name), - ) - - @Suppress("LongParameterList") - @AssistedFactory - interface Factory { - fun create( - scope: CoroutineScope, - token: TokenMarketParams, - tokenActionsHandler: TokenActionsHandler, - buttonState: Flow, - onAddClick: () -> Unit, - ): NewMarketsPortfolioDelegate - } -} - -private data class PortfoliosWithThisCurrency( - val currencyRawId: CryptoCurrency.RawID, - val portfolios: List, -) { - - val hasMultiWallets: Boolean = portfolios.any { it.userWallet.isMultiCurrency } - - val flattenAddedCurrency: List = - portfolios.map { portfolio -> portfolio.flattenAddedCurrency }.flatten() -} - -private data class Portfolio( - val userWallet: UserWallet, - val accountStatusList: AccountStatusList, - val accountsWithAdded: List, -) { - val flattenAddedCurrency: List = - accountsWithAdded.map { it.addedCurrency }.flatten() -} - -private data class AccountWithAdded( - val addedCurrency: List, - val accountStatus: AccountStatus, -) - -private data class SettingsBox( - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, - val isAccountMode: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt deleted file mode 100644 index acf1b7934c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -/** - * Model for portfolio bottom sheet visibility - * - * @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet - * @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioBSVisibilityModel( - val isAddToPortfolioBSVisible: Boolean = false, - val isWalletSelectorBSVisible: Boolean = false, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt deleted file mode 100644 index 17b0a6cdf5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.tokens.TokenItemStateConverter -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] - * -[REDACTED_AUTHOR] - */ -internal class PortfolioTokenUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, - private val tokenActionsHandler: TokenActionsHandler, -) : Converter { - - fun convertV2( - value: PortfolioData.CryptoCurrencyData, - isQuickActionsShown: Boolean, - onTokenItemClick: (UserWallet, CryptoCurrencyStatus) -> Unit, - ): PortfolioTokenUM { - val tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - onItemClick = { _, status -> onTokenItemClick(value.userWallet, status) }, - ) - return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = value.status), - walletId = value.userWallet.walletId, - isBalanceHidden = isBalanceHidden, - isQuickActionsShown = isQuickActionsShown, - quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler), - ) - } - - override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { - val tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) }, - subtitleStateProvider = { - TokenItemState.SubtitleState.TextContent(value = stringReference(value.status.currency.name)) - }, - onItemClick = { _, status -> onTokenItemClick(status) }, - ) - - return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = value.status), - walletId = value.userWallet.walletId, - isBalanceHidden = isBalanceHidden, - isQuickActionsShown = false, - quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler), - ) - } - - companion object { - fun quickActions( - cryptoData: PortfolioData.CryptoCurrencyData, - tokenActionsHandler: TokenActionsHandler, - ): PortfolioTokenUM.QuickActions { - return PortfolioTokenUM.QuickActions( - actions = toQuickActions(cryptoData.actions), - onQuickActionClick = { quickActionUM -> - when (quickActionUM) { - QuickActionUM.Buy -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Buy, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.Exchange -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Exchange, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.Receive -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Receive, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.Stake -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Stake, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.YieldMode -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.YieldMode, - cryptoCurrencyData = cryptoData, - ) - } - }, - onQuickActionLongClick = { quickAction -> - if (quickAction == QuickActionUM.Receive) { - tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.CopyAddress, - cryptoCurrencyData = cryptoData, - ) - } - }, - ) - } - - fun toQuickActions(actions: List) = buildList { - actions.forEach { action -> - if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) { - when (action) { - is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy - is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange( - shouldShowBadge = action.showBadge, - ) - is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive - is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake - is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(apy = action.apy) - else -> null - }?.let(::add) - } - } - }.toImmutableList() - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt deleted file mode 100644 index 855e596731..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Portfolio UI data. Combined data from all UI flows that required to setup portfolio - * - * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @property selectedWalletId selected wallet id - * @property addToPortfolioData add to portfolio data - * @property shouldRequireColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioUIData( - val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - val selectedWalletId: UserWalletId?, - val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - val shouldRequireColdWalletInteraction: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt deleted file mode 100644 index 8599ad97d5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [TokenMarketParams] to [SelectNetworkUM] - * - * @property networksWithToggle map of networks with toggles - * @property alreadyAddedNetworks already added networks - * @property onNetworkSwitchClick callback is called when network switch is clicked - * -[REDACTED_AUTHOR] - */ -internal class SelectNetworkUMConverter( - private val networksWithToggle: Map, - private val alreadyAddedNetworks: Set, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketParams): SelectNetworkUM { - return SelectNetworkUM( - tokenId = value.id.value, - iconUrl = value.imageUrl, - tokenName = value.name, - tokenCurrencySymbol = value.symbol, - networks = BlockchainRowUMConverter(alreadyAddedNetworks) - .convertList(networksWithToggle.toList()) - .toImmutableList(), - onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt deleted file mode 100644 index 66ff85116c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ /dev/null @@ -1,173 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.routing.AppRoute -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.Provider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.toImmutableList - -@Suppress("LongParameterList") -internal class TokenActionsHandler @AssistedInject constructor( - private val router: Router, - private val clipboardManager: ClipboardManager, - private val uiMessageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, - @Assisted private val currentAppCurrency: Provider, - @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, - private val isDemoCardUseCase: IsDemoCardUseCase, - private val messageSender: UiMessageSender, -) { - - private val disabledActionsInDemoMode = buildSet { - add(TokenActionsBSContentUM.Action.Sell) - } - - fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - onHandleQuickAction( - HandledQuickAction( - action = action, - cryptoCurrencyData = cryptoCurrencyData, - ), - ) - val userWallet = cryptoCurrencyData.userWallet - if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return - - when (action) { - TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Receive -> Unit - TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData) - } - } - - private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean { - val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId) - val shouldShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action) - - if (shouldShowDemoWarning) { - showDemoModeWarning() - } - - return shouldShowDemoWarning - } - - private fun showDemoModeWarning() { - val message = DialogMessage( - message = resourceReference(R.string.alert_demo_feature_disabled), - ) - messageSender.send(message) - } - - private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val cryptoCurrencyStatus = cryptoCurrencyData.status - val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return - val addresses = networkAddress.availableAddresses - .mapToAddressModels(cryptoCurrencyStatus.currency) - .toImmutableList() - val defaultAddress = addresses.firstOrNull()?.value ?: return - - clipboardManager.setText(text = defaultAddress, isSensitive = true) - uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) - } - - private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - router.push( - AppRoute.Onramp( - userWalletId = cryptoCurrencyData.userWallet.walletId, - currency = cryptoCurrencyData.status.currency, - source = OnrampSource.MARKETS, - ), - ) - } - - private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ), - ) - } - - private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - router.push( - AppRoute.Swap( - currencyFrom = cryptoCurrencyData.status.currency, - userWalletId = cryptoCurrencyData.userWallet.walletId, - isInitialReverseOrder = true, - screenSource = AnalyticsParam.ScreensSources.Markets.value, - ), - ) - } - - private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val route = AppRoute.SendEntryPoint( - userWalletId = cryptoCurrencyData.userWallet.walletId, - currency = cryptoCurrencyData.status.currency, - ) - router.push(route) - } - - private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } - ?.let { it as TokenActionsState.ActionState.Stake } - ?.option ?: return - - router.push( - AppRoute.Staking( - userWalletId = cryptoCurrencyData.userWallet.walletId, - cryptoCurrency = cryptoCurrencyData.status.currency, - integrationId = option.integrationId, - ), - ) - } - - private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() - .firstOrNull()?.apy ?: return - - router.push( - AppRoute.YieldSupplyEntry( - userWalletId = cryptoCurrencyData.userWallet.walletId, - cryptoCurrency = cryptoCurrencyData.status.currency, - apy = yieldSupplyApy, - ), - ) - } - - @AssistedFactory - interface Factory { - fun create( - currentAppCurrency: Provider, - onHandleQuickAction: (HandledQuickAction) -> Unit, - ): TokenActionsHandler - } - - data class HandledQuickAction( - val action: TokenActionsBSContentUM.Action, - val cryptoCurrencyData: PortfolioData.CryptoCurrencyData, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt deleted file mode 100644 index 16f7f6554a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isZero -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class TokensPortfolioUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val addButtonState: AddButtonState, - private val bsConfig: TangemBottomSheetConfig, - private val onAddClick: () -> Unit, - private val quickActionsIntents: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { - - override fun convert(value: Map>): MyPortfolioUM.Tokens { - val currentTokensState = currentState() as? MyPortfolioUM.Tokens - - return MyPortfolioUM.Tokens( - tokens = value - .flatMap { entry -> entry.value } - .map { cryptoData -> - PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { toggleQuickActions(cryptoData) }, - tokenActionsHandler = quickActionsIntents, - ).convert(value = cryptoData) to cryptoData - } - .setQuickActionsVisibility(currentState = currentTokensState) - .toImmutableList(), - buttonState = addButtonState, - addToPortfolioBSConfig = bsConfig, - onAddClick = onAddClick, - ) - } - - private fun List>.setQuickActionsVisibility( - currentState: MyPortfolioUM.Tokens?, - ): List { - return when { - // if there is only one token and it has empty balance, show quick actions for it - currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = true) - } - } - // if there is no previous state, hide quick actions for all tokens - currentState == null -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = false) - } - } - else -> { - val previousList = currentState.tokens - - // otherwise, keep previous state - this.map { (token, _) -> - token.copy( - isQuickActionsShown = previousList - .firstOrNull { it.matchWith(token) } - ?.isQuickActionsShown == true, - ) - } - } - } - } - - private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return cryptoData.status.value.amount?.isZero() == true - } - - private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) { - updateTokens { tokenList -> - tokenList.map { token -> - token.copy( - isQuickActionsShown = if (token.matchWith(cryptoData)) { - !token.isQuickActionsShown - } else { - false - }, - ) - }.toImmutableList() - } - } - - private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean { - return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id - } - - private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return this.walletId == cryptoData.userWallet.walletId && - this.tokenItemState.id == cryptoData.status.currency.id.value - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt deleted file mode 100644 index f2d2d22dd2..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ /dev/null @@ -1,383 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.rows.ArrowRow -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import kotlinx.coroutines.delay - -@Composable -internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - titleText = resourceReference(R.string.common_add_to_portfolio), - ) { contentState -> - Content( - modifier = Modifier.fillMaxWidth(), - state = contentState, - ) - - WalletSelectorBottomSheet(contentState.walletSelectorConfig) - } -} - -@Composable -private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - var continueButtonAreaHeight by remember { mutableIntStateOf(0) } - val density = LocalDensity.current - val scrollState = rememberScrollState() - - Box(modifier = modifier) { - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - if (state.isWalletBlockVisible) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - SpacerH12() - } - - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - SpacerH12() - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) - SpacerH12() - } - - // Scroll to the bottom when the notification appears and the scroll is at the bottom - LaunchedEffect(Unit) { - if (scrollState.canScrollForward.not()) { - delay(timeMillis = 500) - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - - SpacerH(with(density) { continueButtonAreaHeight.toDp() }) - } - - AnimatedVisibility( - visible = scrollState.canScrollForward, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter), - ) { - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - - ContinueButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - continueButtonAreaHeight = it.size.height - }, - enabled = state.isContinueButtonEnabled, - isTangemIconVisible = state.isScanCardNotificationVisible, - onClick = state.onContinueButtonClick, - ) - } -} - -@Composable -private fun ContinueButton( - enabled: Boolean, - isTangemIconVisible: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TangemButton( - enabled = enabled, - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ) - .navigationBarsPadding() - .fillMaxWidth(), - text = stringResourceSafe(R.string.common_continue), - icon = if (enabled && isTangemIconVisible) { - TangemButtonIconPosition.End(R.drawable.ic_tangem_24) - } else { - TangemButtonIconPosition.None - }, - showProgress = false, - size = TangemButtonSize.Default, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - onClick = onClick, - animateContentChange = true, - ) -} - -@Suppress("LongMethod") -@Composable -private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(R.string.markets_select_network), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing14), - verticalAlignment = Alignment.CenterVertically, - ) { - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - SpacerW12() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f, fill = false) - .alignByBaseline(), - text = state.tokenName, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW6() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .alignByBaseline(), - text = state.tokenCurrencySymbol, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Visible, - maxLines = 1, - ) - } - - state.networks.fastForEachIndexed { index, network -> - ArrowRow( - isLastItem = index == state.networks.lastIndex, - content = { - BlockchainRow( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - model = network, - action = { - TangemSwitch( - checked = network.isSelected, - checkedColor = if (network.isEnabled) { - TangemTheme.colors.control.checked - } else { - TangemTheme.colors.icon.inactive - }, - onCheckedChange = { checked -> - if (checked) { - hapticManager.perform(TangemHapticEffect.View.ToggleOn) - } else { - hapticManager.perform(TangemHapticEffect.View.ToggleOff) - } - - state.onNetworkSwitchClick(network, checked) - }, - enabled = network.isEnabled, - ) - }, - ) - }, - ) - } - } - } -} - -@Composable -private fun ScanWalletWarning(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .background( - color = TangemTheme.colors.button.disabled, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Text( - text = stringResourceSafe(R.string.markets_generate_addresses_notification), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - content = content, - onDismissRequest = {}, - ), - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContent( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContentRtl( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview(rtl = true) { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -// For on device testing -@Composable -@Preview -private fun PreviewContentTestOnDevice( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview( - alwaysShowBottomSheets = false, - ) { - var isShow by remember { mutableStateOf(false) } - - var contentState by remember { - mutableStateOf(content) - } - - LaunchedEffect(Unit) { - contentState = content.copy( - onContinueButtonClick = { - contentState = contentState.copy( - isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, - ) - }, - isContinueButtonEnabled = true, - selectedWallet = content.selectedWallet.copy( - onClick = { - contentState = contentState.copy( - isContinueButtonEnabled = !contentState.isContinueButtonEnabled, - ) - }, - ), - ) - } - - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = isShow, - content = contentState, - onDismissRequest = { isShow = false }, - ), - ) - - Button( - onClick = { isShow = !isShow }, - ) { - Text(text = "Toggle") - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt deleted file mode 100644 index 1ea37db49c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ /dev/null @@ -1,339 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.common.ui.account.AccountTitle -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SmallButtonShimmer -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.* -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState - -@Composable -internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { - if (state is MyPortfolioUM.Content) { - val contentModifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing32, - ) - PortfolioList(state, contentModifier) - return - } - InformationBlock( - modifier = modifier, - contentHorizontalPadding = TangemTheme.dimens.spacing0, - title = { Title() }, - action = { - if (state !is MyPortfolioUM.Tokens) return@InformationBlock - - AddButton(state = state.buttonState, onClick = state.onAddClick) - }, - ) { - val contentModifier = Modifier.padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ) - - when (state) { - is MyPortfolioUM.Tokens -> TokenList(state = state) - is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier) - MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier) - MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier) - MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier) - is MyPortfolioUM.Content -> PortfolioList(state = state) - } - } - - val bsConfig = state.addToPortfolioBSConfig - if (bsConfig != null) { - AddToPortfolioBottomSheet(config = bsConfig) - } -} - -@Composable -private fun Title() { - Text( - text = stringResourceSafe(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun AddButton(state: AddButtonState, onClick: () -> Unit) { - when (state) { - AddButtonState.Loading -> { - Box { - SmallButtonShimmer( - modifier = Modifier.width(width = TangemTheme.dimens.size63), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - withIcon = true, - ) - - Box( - Modifier - .matchParentSize() - .background(TangemTheme.colors.background.action), - ) - - RectangleShimmer( - modifier = Modifier - .align(Alignment.Center) - .size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), - radius = TangemTheme.dimens.radius3, - ) - } - } - AddButtonState.Available, - AddButtonState.Unavailable, - -> { - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.markets_add_token), - icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), - onClick = onClick, - isEnabled = state == AddButtonState.Available, - ), - ) - } - } -} - -@Composable -private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) { - Column(modifier) { - state.tokens.fastForEachIndexed { index, token -> - key(token.tokenItemState.id) { - PortfolioItem( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - state = token, - lastInList = index == state.tokens.size - 1, - ) - } - } - } -} - -@Composable -private fun PortfolioList(state: MyPortfolioUM.Content, modifier: Modifier = Modifier) { - Column(modifier) { - key("PortfolioListHeader") { - Row( - modifier = Modifier.padding(horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResourceSafe(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - AddButton(state = state.buttonState, onClick = state.onAddClick) - } - } - - state.items.fastForEachIndexed { index, item -> - val previousItem = state.items.getOrNull(index.dec()) - val nextItem = state.items.getOrNull(index.inc()) - val itemModifier = Modifier - .fillMaxWidth() - .getOffsetModifier(item, previousItem) - .getBackgroundModifier(item, previousItem, nextItem) - - key(item.id) { - PortfolioItem( - item = item, - modifier = itemModifier, - lastInList = index == state.items.size - 1, - ) - } - } - } -} - -@Composable -private fun Modifier.getBackgroundModifier( - item: PortfolioListItem, - previousItem: PortfolioListItem?, - nextItem: PortfolioListItem?, -): Modifier { - val color = TangemTheme.colors.background.action - val radius = 14.dp - val topRound = RoundedCornerShape(topStart = radius, topEnd = radius) - val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - val allRound = RoundedCornerShape(size = radius) - val backgroundModifier = when (item) { - is WalletHeader -> this - is PortfolioHeader -> this - .clip(topRound) - .background(color = color) - is PortfolioTokenUM -> when { - previousItem is PortfolioHeader && nextItem !is PortfolioTokenUM -> this - .clip(bottomRound) - .background(color = color) - previousItem is WalletHeader && nextItem !is PortfolioTokenUM -> this - .clip(allRound) - .background(color = color) - previousItem is PortfolioTokenUM && nextItem !is PortfolioTokenUM -> this - .clip(bottomRound) - .background(color = color) - else -> this.background(color = color) - } - } - return backgroundModifier -} - -private fun Modifier.getOffsetModifier(item: PortfolioListItem, previousItem: PortfolioListItem?): Modifier = when { - item is WalletHeader -> this.padding(top = 20.dp, start = 4.dp, end = 4.dp) - item is PortfolioHeader && previousItem is PortfolioTokenUM -> this.padding(top = 12.dp) - item is PortfolioHeader && previousItem == null -> this.padding(top = 20.dp) - previousItem is WalletHeader -> this.padding(top = 12.dp) - previousItem is PortfolioTokenUM -> this - else -> this -} - -@Composable -private fun PortfolioItem(item: PortfolioListItem, lastInList: Boolean, modifier: Modifier = Modifier) { - when (item) { - is PortfolioHeader -> AccountTitle( - modifier = modifier.padding( - start = 12.dp, - top = 12.dp, - bottom = 8.dp, - ), - accountTitleUM = item.state, - textStyle = TangemTheme.typography.caption1, - textColor = TangemTheme.colors.text.primary1, - ) - is PortfolioTokenUM -> PortfolioItem( - state = item, - modifier = modifier, - lastInList = lastInList, - ) - is WalletHeader -> Text( - modifier = modifier, - text = item.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - } -} - -@Composable -fun UnavailableAsset(modifier: Modifier = Modifier) { - UnavailableContent( - textId = R.string.markets_add_to_my_portfolio_unavailable_description, - modifier = modifier, - ) -} - -@Composable -fun UnavailableAssetForWallet(modifier: Modifier = Modifier) { - UnavailableContent( - textId = R.string.markets_add_to_my_portfolio_unavailable_for_wallet_description, - modifier = modifier, - ) -} - -@Composable -private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = stringResourceSafe(textId), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Text( - text = stringResourceSafe(R.string.markets_add_to_my_portfolio_description), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_add_to_portfolio), - onClick = state.onAddClick, - ) - } -} - -@Composable -private fun LoadingPlaceholder(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.7f), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { - TangemThemePreview { - Box( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary), - ) { - MyPortfolio(state) - } - } -} - -@Preview -@Composable -private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { - TangemThemePreview(rtl = true) { - Box( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing8), - ) { - MyPortfolio(state) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt deleted file mode 100644 index d7800514fc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.icons.IconTint -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.StringsSigns.DASH_SIGN -import kotlinx.collections.immutable.persistentListOf -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState - -@Composable -internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { - Column(modifier) { - val hapticManager = LocalHapticManager.current - val tokenItemState = remember(state.tokenItemState) { - when (state.tokenItemState) { - is TokenItemState.Content -> state.tokenItemState.copy( - onItemClick = { cryptoCurrency -> - val onClick = state.tokenItemState.onItemClick - if (onClick != null) { - hapticManager.perform(TangemHapticEffect.View.ContextClick) - onClick.invoke(cryptoCurrency) - } - }, - ) - else -> state.tokenItemState - } - } - TokenItem( - state = tokenItemState, - isBalanceHidden = state.isBalanceHidden, - itemPaddingValues = PaddingValues( - start = TangemTheme.dimens.spacing10, - end = TangemTheme.dimens.spacing12, - ), - ) - - PortfolioQuickActions( - modifier = Modifier - .padding( - bottom = if (lastInList) { - TangemTheme.dimens.spacing12 - } else { - TangemTheme.dimens.spacing24 - }, - ), - actions = state.quickActions.actions, - isVisible = state.isQuickActionsShown, - onActionClick = state.quickActions.onQuickActionClick, - onActionLongClick = state.quickActions.onQuickActionLongClick, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { - TangemThemePreview { - var areQuickActionsShown by remember { mutableStateOf(value = false) } - - val onItemClick = { - areQuickActionsShown = areQuickActionsShown.not() - } - - PortfolioItem( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - state = tokenUM.copy( - tokenItemState = when (tokenUM.tokenItemState) { - is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) - is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) - else -> tokenUM.tokenItemState - }, - isQuickActionsShown = areQuickActionsShown, - ), - lastInList = true, - ) - } -} - -private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( - collection = listOf( - tokenUM.copy( - tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( - fiatAmountState = contentFiatAmount.copy( - icons = persistentListOf( - TokenFiatAmountState.Content.IconUM( - iconRes = R.drawable.ic_staking_24, - tint = IconTint.Accent, - ), - ), - ), - ), - ), - tokenUM.copy( - tokenItemState = tokenUM.tokenItemState.copy( - fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), - subtitle2State = (tokenUM.tokenItemState.subtitle2State as? TokenItemState.Subtitle2State.TextContent - ?: error("subtitle2State must be TextContent for preview")) - .copy(text = DASH_SIGN), - ), - ), - tokenUM.copy(isBalanceHidden = true), - tokenUM.copy( - tokenItemState = TokenItemState.Unreachable( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState, - subtitleState = tokenUM.tokenItemState.subtitleState, - onItemClick = {}, - onItemLongClick = {}, - ), - ), - tokenUM.copy( - tokenItemState = TokenItemState.NoAddress( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState, - subtitleState = tokenUM.tokenItemState.subtitleState, - onItemLongClick = {}, - ), - ), - tokenUM.copy( - tokenItemState = TokenItemState.Loading( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, - subtitleState = tokenUM.tokenItemState.subtitleState, - ), - ), - ), -) { - - companion object { - val tokenUM = PreviewMyPortfolioUMProvider().sampleToken - val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as? TokenFiatAmountState.Content - ?: error("fiatAmountState must be Content for preview") - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt deleted file mode 100644 index 22a377bfd2..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.icons.badge.drawBadge -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun PortfolioQuickActions( - actions: ImmutableList, - isVisible: Boolean, - onActionClick: (QuickActionUM) -> Unit, - onActionLongClick: (QuickActionUM) -> Unit, - modifier: Modifier = Modifier, -) { - if (actions.isEmpty()) return - - AnimatedVisibility( - visible = isVisible, - enter = expandVertically(expandFrom = Alignment.Top), - exit = shrinkVertically(shrinkTowards = Alignment.Top), - modifier = modifier, - ) { - Column(modifier = Modifier) { - actions.fastForEach { action -> - LineSeparator() - QuickActionItem( - state = action, - onClick = { onActionClick(action) }, - onLongClick = { onActionLongClick(action) }.takeIf { action.isLongClickAvailable }, - ) - } - } - } -} - -@Composable -private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { - val lineColor = TangemTheme.colors.stroke.primary - val strokeWidth = TangemTheme.dimens.size1 - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val startPadding = TangemTheme.dimens.spacing30 - - val height = TangemTheme.dimens.size16 - - Canvas( - modifier = modifier - .animateEnterExit( - enter = expandVertically( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - ), - expandFrom = Alignment.Top, - ) + fadeIn(), - exit = shrinkVertically( - spring( - stiffness = Spring.StiffnessLow, - ), - shrinkTowards = Alignment.Top, - ) + fadeOut(), - ) - .fillMaxWidth() - .height(height), - ) { - val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx() - - drawLine( - color = lineColor, - start = Offset(x, 0f), - end = Offset(x, size.height), - strokeWidth = strokeWidth.toPx(), - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun AnimatedVisibilityScope.QuickActionItem( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit)?, - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) { - { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - } else { - null - } - - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable( - onLongClick = onLongClickInternal, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), - ) { - QuickActionIcon(state) - Column( - modifier = Modifier - .animateEnterExit( - enter = fadeIn(), - exit = fadeOut(), - ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) { - val containerColor = TangemTheme.colors.background.action - Box( - Modifier - .animateEnterExit( - enter = scaleIn(), - exit = scaleOut(), - ) - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ) - .size(TangemTheme.dimens.size32) - .drawWithContent { - drawContent() - if (state is QuickActionUM.Exchange && state.shouldShowBadge) { - drawBadge(containerColor = containerColor, offset = 4.dp) - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.button.primary, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - var isVisible by remember { mutableStateOf(true) } - - Column( - modifier = Modifier - .fillMaxWidth() - .height(680.dp), - ) { - Button( - onClick = { isVisible = !isVisible }, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) { - Text(text = "Toggle") - } - SpacerH4() - Box( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - ) { - PortfolioQuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - isVisible = isVisible, - onActionClick = {}, - onActionLongClick = {}, - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewRtl() { - TangemThemePreview(rtl = true) { - Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { - PortfolioQuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - isVisible = true, - onActionClick = {}, - onActionLongClick = {}, - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt deleted file mode 100644 index 8cb81049e5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import kotlinx.collections.immutable.toImmutableList - -@Composable -fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - title = { content -> - TangemBottomSheetTitle(content.title) - }, - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: TokenActionsBSContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - content.actions.forEachIndexed { index, action -> - Box( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.actions.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - ) { - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconRes, - redesign = true, - onItemsClick = { content.onActionClick(action) }, - ) - } - } - } -} - -@Preview(widthDp = 360, heightDp = 640) -@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview( - alwaysShowBottomSheets = true, - ) { - Box(Modifier.background(TangemTheme.colors.background.secondary)) { - TokenActionsBottomSheet( - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TokenActionsBSContentUM( - title = "Wallet 1", - actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), - onActionClick = {}, - ), - ), - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt deleted file mode 100644 index 23e63c41f4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - title = { content -> - TangemTopAppBar( - title = resourceReference(R.string.common_choose_wallet), - titleAlignment = Alignment.CenterHorizontally, - startButton = TopAppBarButtonUM.Back(content.onBack), - height = TangemTopAppBarHeight.BOTTOM_SHEET, - ) - }, - ) { content -> - Content( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing8, - ), - state = content, - ) - } -} - -@Composable -private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Column( - modifier = modifier - .verticalScroll(rememberScrollState()), - ) { - BlockCard( - modifier = Modifier.fillMaxSize(), - colors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( - modifier = Modifier.fillMaxWidth(), - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - state = state, - ) - } - } - } - SpacerH(bottomBarHeight) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - WalletSelectorBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewContent() { - TangemThemePreview { - Content( - state = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt deleted file mode 100644 index bdc1b092e3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import kotlinx.collections.immutable.persistentListOf - -internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = "1", - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ) - - val userWallet = UserWalletItemUM( - id = "1", - name = stringReference("Wallet 1"), - information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), - balance = UserWalletItemUM.Balance.Loading, - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ), - blockchainRow, - blockchainRow, - ), - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = true, - isContinueButtonEnabled = true, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium Etherium Etherium Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ).copy(name = "Etherium Etherium Etherium Etherium"), - *Array(25) { blockchainRow }, - ), - - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = false, - isContinueButtonEnabled = false, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt deleted file mode 100644 index e6deaad138..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.account.AccountIconPreviewData -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.markets.portfolio.impl.ui.state.* -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { - - val sampleToken - get() = PortfolioTokenUM( - tokenItemState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"), - subtitleState = TokenItemState.SubtitleState.TextContent( - value = stringReference(value = "XRP Ledger token"), - ), - onItemClick = {}, - onItemLongClick = {}, - ), - isQuickActionsShown = false, - quickActions = PortfolioTokenUM.QuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - onQuickActionClick = {}, - onQuickActionLongClick = {}, - ), - isBalanceHidden = false, - walletId = UserWalletId(""), - ) - - val walletHeader - get() = WalletHeader( - id = UUID.randomUUID().toString(), - name = stringReference("Wallet 1"), - ) - - val walletPortfolioHeader - get() = PortfolioHeader( - state = AccountTitleUM.Text(title = stringReference("Wallet 1")), - id = UUID.randomUUID().toString(), - ) - - val accountHeader - get() = PortfolioHeader( - state = AccountTitleUM.Account( - icon = AccountIconPreviewData.randomAccountIcon(), - name = stringReference("Main Account"), - prefixText = TextReference.EMPTY, - ), - id = UUID.randomUUID().toString(), - ) - val coinIconState - get() = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22, - isGrayscale = false, - shouldShowCustomBadge = false, - ) - val accountToken - get() = sampleToken.copy( - tokenItemState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = coinIconState, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - fiatAmountState = FiatAmountState.Content(text = "321 $"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"), - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(value = "Token")), - onItemClick = {}, - onItemLongClick = {}, - ), - ) - - override val values: Sequence - get() = sequenceOf( - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken, sampleToken), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Available, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletPortfolioHeader, - accountToken, - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletHeader, - accountHeader, - accountToken, - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletHeader, - accountHeader, - accountToken.copy(isQuickActionsShown = true), - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Loading, - MyPortfolioUM.Unavailable, - MyPortfolioUM.UnavailableForWallet, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt deleted file mode 100644 index ff5c1567e6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent - -internal data class AddToPortfolioBSContentUM( - val selectedWallet: UserWalletItemUM, - val selectNetworkUM: SelectNetworkUM, - val isWalletBlockVisible: Boolean, - val isScanCardNotificationVisible: Boolean, - val isContinueButtonEnabled: Boolean, - val onContinueButtonClick: () -> Unit, - val walletSelectorConfig: TangemBottomSheetConfig, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt deleted file mode 100644 index 0d1cd7c700..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed class MyPortfolioUM { - - abstract val addToPortfolioBSConfig: TangemBottomSheetConfig? - - data class Tokens( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, - val tokens: ImmutableList, - val buttonState: AddButtonState, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - enum class AddButtonState { - Loading, - Available, - Unavailable, - } - } - - data class Content( - val items: ImmutableList, - val buttonState: Tokens.AddButtonState, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty - } - - data class AddFirstToken( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() - - data object Loading : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } - - data object Unavailable : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } - - data object UnavailableForWallet : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt deleted file mode 100644 index b6dd772abc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed interface PortfolioListItem { - val id: String -} - -internal data class WalletHeader( - override val id: String, - val name: TextReference, -) : PortfolioListItem - -internal data class PortfolioHeader( - override val id: String, - val state: AccountTitleUM, -) : PortfolioListItem - -internal data class PortfolioTokenUM( - val tokenItemState: TokenItemState, - val walletId: UserWalletId, - val isBalanceHidden: Boolean, - val isQuickActionsShown: Boolean, - val quickActions: QuickActions, -) : PortfolioListItem { - override val id: String = tokenItemState.id - - data class QuickActions( - val actions: ImmutableList, - val onQuickActionClick: (QuickActionUM) -> Unit, - val onQuickActionLongClick: (QuickActionUM) -> Unit, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt deleted file mode 100644 index 040f3d243e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.markets.impl.R - -@Immutable -internal sealed class QuickActionUM( - val title: TextReference, - val description: TextReference, - @DrawableRes val icon: Int, - val isLongClickAvailable: Boolean = false, -) { - data object Buy : QuickActionUM( - title = resourceReference(R.string.common_buy), - description = resourceReference(R.string.buy_token_description), - icon = R.drawable.ic_plus_24, - ) - - data class Exchange( - val shouldShowBadge: Boolean, - ) : QuickActionUM( - title = resourceReference(R.string.common_exchange), - description = resourceReference(R.string.exсhange_token_description), - icon = R.drawable.ic_exchange_vertical_24, - ) - - data object Receive : QuickActionUM( - title = resourceReference(R.string.common_receive), - description = resourceReference(R.string.receive_token_description), - icon = R.drawable.ic_arrow_down_24, - isLongClickAvailable = true, - ) - - data object Stake : QuickActionUM( - title = resourceReference(R.string.common_stake), - description = resourceReference(R.string.stake_token_description), - icon = R.drawable.ic_staking_24, - ) - - data class YieldMode( - private val apy: String, - ) : QuickActionUM( - title = resourceReference(R.string.common_yield_mode), - description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), - icon = R.drawable.ic_analytics_up_mini_24, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt deleted file mode 100644 index 90830679ca..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -internal data class SelectNetworkUM( - val tokenId: String, - val iconUrl: String?, - val tokenName: String, - val tokenCurrencySymbol: String, - val networks: ImmutableList, - val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt deleted file mode 100644 index 20db6ec796..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.ImmutableList - -internal data class TokenActionsBSContentUM( - val title: String, - val actions: ImmutableList, - val onActionClick: (Action) -> Unit, -) : TangemBottomSheetConfigContent { - - @Immutable - enum class Action( - val text: TextReference, - @DrawableRes val iconRes: Int, - ) { - CopyAddress( - text = resourceReference(R.string.common_copy_address), - iconRes = R.drawable.ic_copy_24, - ), - Send( - text = resourceReference(R.string.common_send), - iconRes = R.drawable.ic_arrow_up_24, - ), - Receive( - text = resourceReference(R.string.common_receive), - iconRes = R.drawable.ic_arrow_down_24, - ), - Buy( - text = resourceReference(R.string.common_buy), - iconRes = R.drawable.ic_plus_24, - ), - Sell( - text = resourceReference(R.string.common_sell), - iconRes = R.drawable.ic_currency_24, - ), - Exchange( - text = resourceReference(R.string.common_exchange), - iconRes = R.drawable.ic_exchange_horizontal_24, - ), - Stake( - text = resourceReference(R.string.common_stake), - iconRes = R.drawable.ic_staking_24, - ), - YieldMode( - text = resourceReference(R.string.common_yield_mode), - iconRes = R.drawable.ic_analytics_up_mini_24, - ), - ; - - val order: Int = ordinal - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt deleted file mode 100644 index fddc12c25e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -internal data class WalletSelectorBSContentUM( - val userWallets: ImmutableList, - val onBack: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt new file mode 100644 index 0000000000..2db32b29db --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.token.block.impl.model.formatter + +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.marketprice.PriceChangeType + +internal fun PriceChangeType.toChartType(): MarketChartLook.Type { + return when (this) { + PriceChangeType.UP -> MarketChartLook.Type.Growing + PriceChangeType.DOWN -> MarketChartLook.Type.Falling + PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 8f233b53ab..362075a2e2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -23,8 +23,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType 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.markets.details.impl.model.formatter.toChartType import com.tangem.features.markets.impl.R +import com.tangem.features.markets.token.block.impl.model.formatter.toChartType import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import kotlinx.collections.immutable.toImmutableList import kotlin.random.Random diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt index 53e6ba8d82..f6f073d569 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt @@ -1,15 +1,14 @@ package com.tangem.features.onboarding.v2.common.ui -import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference internal data class OnboardingDialogUM( - override val title: TextReference, - override val message: TextReference, + val title: TextReference, + val message: TextReference, val dismissButtonText: TextReference, - override val confirmButtonText: TextReference, + val confirmButtonText: TextReference, val dismissWarningColor: Boolean = false, - override val onConfirmClick: () -> Unit, + val onConfirmClick: () -> Unit, val onDismissButtonClick: () -> Unit, val onDismiss: () -> Unit, -) : AlertUM \ No newline at end of file +) \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index f0a6cb16c5..fcab39bc50 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -42,8 +42,8 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt index 2f3739e450..039af4534d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt @@ -1,7 +1,7 @@ package com.tangem.features.onramp.alloffers.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM internal interface AllOffersIntents { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 3dcc789481..ee7af4a393 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -10,9 +10,9 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.MINUS import kotlinx.collections.immutable.toImmutableList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt index 8e1847afc5..4807f6e61a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.PaymentMethodStatus -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM import kotlinx.collections.immutable.ImmutableList internal sealed interface AllOffersStateUM { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index b29663f8cc..5b0c5748d3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -12,7 +12,7 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersIntents import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.Job diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt index e59a517b99..beaf222881 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt @@ -32,10 +32,10 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM -import com.tangem.features.onramp.mainv2.ui.Offer +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM +import com.tangem.features.onramp.main.ui.Offer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt index 46ca3d4017..5e40a22e41 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt @@ -33,10 +33,10 @@ import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM -import com.tangem.features.onramp.mainv2.ui.TimingBlock +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM +import com.tangem.features.onramp.main.ui.TimingBlock import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index 40ac56570b..fadde70778 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -26,7 +26,6 @@ import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -277,9 +276,9 @@ internal class HotCryptoModel @Inject constructor( private fun updateCryptoCurrency( cryptoCurrency: CryptoCurrency, userWallet: UserWallet, - account: AccountStatus, + account: AccountStatus.CryptoPortfolio, ): CryptoCurrency? { - val derivationIndex = account.account.derivationIndex ?: return null + val derivationIndex = account.account.derivationIndex val blockchain = cryptoCurrency.network.toBlockchain() val network = networkFactory.create( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index 42967bcc47..bfcf43a112 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -8,15 +8,16 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.alloffers.AllOffersComponent import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.model.OnrampMainComponentModel -import com.tangem.features.onramp.main.ui.OnrampMainComponentContent -import com.tangem.features.onramp.providers.SelectProviderComponent +import com.tangem.features.onramp.main.ui.OnrampMainScreen import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -27,10 +28,15 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( @Assisted private val params: OnrampMainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, - private val selectProviderComponentFactory: SelectProviderComponent.Factory, + private val allOffersComponentFactory: AllOffersComponent.Factory, ) : OnrampMainComponent, AppComponentContext by appComponentContext { private val model: OnrampMainComponentModel = getOrCreateModel(params) + + init { + lifecycle.subscribe(onStop = model::onStop) + } + private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, serializer = null, @@ -43,7 +49,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( val state by model.state.collectAsState() val bottomSheet by bottomSheetSlot.subscribeAsState() - OnrampMainComponentContent(modifier = modifier, state = state) + OnrampMainScreen(modifier = modifier, state = state) bottomSheet.child?.instance?.BottomSheet() } @@ -57,7 +63,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( userWalletId = params.userWalletId, cryptoCurrency = params.cryptoCurrency, country = config.country, - isLaunchSepa = params.isLaunchSepa, + isLaunchSepa = false, onDismiss = { model.bottomSheetNavigation.dismiss() model.handleOnrampAvailable() @@ -72,14 +78,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( onDismiss = model.bottomSheetNavigation::dismiss, ), ) - is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create( + is OnrampMainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( context = childByContext(componentContext), - params = SelectProviderComponent.Params( - onProviderClick = model::onProviderSelected, - onDismiss = model.bottomSheetNavigation::dismiss, - selectedProviderId = config.selectedProviderId, - selectedPaymentMethod = config.selectedPaymentMethod, + params = AllOffersComponent.Params( + userWallet = model.userWallet, cryptoCurrency = params.cryptoCurrency, + onDismiss = model.bottomSheetNavigation::dismiss, + openRedirectPage = params.openRedirectPage, + amountCurrencyCode = config.amountCurrencyCode, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index d4858314cf..98df5c2a8e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -15,7 +15,6 @@ internal interface OnrampMainComponent : ComposableContentComponent { val source: OnrampSource, val openSettings: () -> Unit, val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, - val isLaunchSepa: Boolean, ) interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt deleted file mode 100644 index f3bbdba606..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.main.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.main.model.OnrampMainComponentModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface OnrampMainComponentModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampMainComponentModel::class) - fun bindOnrampSelectCountryModel(model: OnrampMainComponentModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt index 78140491e5..440b6f0c7f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt @@ -1,11 +1,15 @@ package com.tangem.features.onramp.main.di +import com.tangem.core.decompose.model.Model import com.tangem.features.onramp.main.DefaultOnrampMainComponent import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.main.model.OnrampMainComponentModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Singleton @Module @@ -15,4 +19,9 @@ internal interface OnrampMainComponentModule { @Binds @Singleton fun bindOnrampMainComponentFactory(factory: DefaultOnrampMainComponent.Factory): OnrampMainComponent.Factory + + @Binds + @IntoMap + @ClassKey(OnrampMainComponentModel::class) + fun bindOnrampMainComponentModel(model: OnrampMainComponentModel): Model } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt deleted file mode 100644 index c56ecedb67..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.extensions.TextReference - -internal data class OnrampAmountBlockUM( - val currencyUM: OnrampCurrencyUM, - val amountFieldModel: AmountFieldModel, - val secondaryFieldModel: OnrampAmountSecondaryFieldUM, -) - -internal data class OnrampCurrencyUM( - val code: String, - val iconUrl: String?, - val precision: Int, - val onClick: () -> Unit, -) - -@Immutable -internal sealed interface OnrampAmountSecondaryFieldUM { - data object Loading : OnrampAmountSecondaryFieldUM - data class Content(val amount: TextReference) : OnrampAmountSecondaryFieldUM - data class Error(val error: TextReference) : OnrampAmountSecondaryFieldUM -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt similarity index 71% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt index 9ae8528504..729d275e25 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt @@ -1,17 +1,17 @@ -package com.tangem.features.onramp.mainv2.entity +package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList -internal data class OnrampNewAmountBlockUM( - val currencyUM: OnrampNewCurrencyUM, +internal data class OnrampAmountBlockUM( + val currencyUM: OnrampCurrencyUM, val amountFieldModel: AmountFieldModel, val secondaryFieldModel: OnrampSecondaryFieldErrorUM, ) -internal data class OnrampNewCurrencyUM( +internal data class OnrampCurrencyUM( val unit: String, val code: String, val iconUrl: String?, @@ -25,9 +25,9 @@ internal sealed interface OnrampSecondaryFieldErrorUM { data class Error(val error: TextReference) : OnrampSecondaryFieldErrorUM } -internal sealed interface OnrampV2AmountButtonUMState { - data class Loaded(val amountButtons: ImmutableList) : OnrampV2AmountButtonUMState - data object None : OnrampV2AmountButtonUMState +internal sealed interface OnrampAmountButtonUMState { + data class Loaded(val amountButtons: ImmutableList) : OnrampAmountButtonUMState + data object None : OnrampAmountButtonUMState } internal data class OnrampAmountButtonUM( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index 0115a77152..574c819d6a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -2,12 +2,15 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote -interface OnrampIntents { - fun onAmountValueChanged(value: String, isValuePasted: Boolean) +internal interface OnrampIntents { + fun onAmountValueChanged(value: String) fun openSettings() fun openCurrenciesList() - fun onBuyClick(quote: OnrampProviderWithQuote.Data) + fun onBuyClick( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) fun openProviders() fun onRefresh() - fun onLinkClick(link: String) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt deleted file mode 100644 index 9bcc516c6b..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.domain.onramp.model.OnrampAmount -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -data class OnrampLastUpdate( - val fromAmount: OnrampAmount, - val countryCode: String, - val paymentMethod: OnrampPaymentMethod, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt index 2b0cff2f34..a317b2a287 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -1,11 +1,10 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampCountry -import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.serialization.Serializable @Serializable -internal sealed interface OnrampMainBottomSheetConfig { +sealed interface OnrampMainBottomSheetConfig { @Serializable data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig @@ -13,8 +12,5 @@ internal sealed interface OnrampMainBottomSheetConfig { data object CurrenciesList : OnrampMainBottomSheetConfig @Serializable - data class ProvidersList( - val selectedProviderId: String, - val selectedPaymentMethod: OnrampPaymentMethod, - ) : OnrampMainBottomSheetConfig + data class AllOffers(val amountCurrencyCode: String) : OnrampMainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index 72fa50f884..b9e86bee09 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -4,55 +4,29 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.onramp.impl.R @Immutable internal sealed interface OnrampMainComponentUM { val topBarConfig: OnrampMainTopBarUM - val buyButtonConfig: BuyButtonConfig val errorNotification: NotificationUM? data class InitialLoading( - val currency: String, - val onClose: () -> Unit, - val openSettings: () -> Unit, - override val errorNotification: NotificationUM? = null, - ) : OnrampMainComponentUM { - override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( - title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM.Back( - onBackClicked = onClose, - enabled = true, - ), - endButtonUM = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_more_vertical_24, - onClicked = openSettings, - isEnabled = false, - ), - ) - - override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig( - text = resourceReference(R.string.common_buy), - onClick = {}, - isEnabled = false, - ) - } + override val topBarConfig: OnrampMainTopBarUM, + override val errorNotification: NotificationUM?, + ) : OnrampMainComponentUM data class Content( override val topBarConfig: OnrampMainTopBarUM, - override val buyButtonConfig: BuyButtonConfig, override val errorNotification: NotificationUM?, val amountBlockState: OnrampAmountBlockUM, - val providerBlockState: OnrampProviderBlockUM, + val offersBlockState: OnrampOffersBlockUM, + val onrampAmountButtonUMState: OnrampAmountButtonUMState, ) : OnrampMainComponentUM } -internal data class BuyButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - val isEnabled: Boolean, +internal data class OnrampMainTopBarUM( + val title: TextReference, + val startButtonUM: TopAppBarButtonUM, + val endButtonUM: TopAppBarButtonUM, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt deleted file mode 100644 index 5cd08fe2fa..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference - -internal data class OnrampMainTopBarUM( - val title: TextReference, - val startButtonUM: TopAppBarButtonUM, - val endButtonUM: TopAppBarButtonUM, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt similarity index 97% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt index aecbc43ee9..978aed9131 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.entity +package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt deleted file mode 100644 index 5434116a79..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -sealed class OnrampProviderBlockUM { - data object Empty : OnrampProviderBlockUM() - data object Loading : OnrampProviderBlockUM() - data class Content( - val providerId: String, - val paymentMethod: OnrampPaymentMethod, - val providerName: String, - val termsOfUseLink: String?, - val privacyPolicyLink: String?, - val isBestRate: Boolean, - val onLinkClick: (String) -> Unit, - val onClick: () -> Unit, - ) : OnrampProviderBlockUM() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt new file mode 100644 index 0000000000..dcbc368282 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.onramp.main.entity + +import com.tangem.domain.onramp.model.OnrampPaymentMethod + +sealed interface OnrampProvidersUM { + + data object Empty : OnrampProvidersUM + + data object Loading : OnrampProvidersUM + + data class Content( + val providerId: String, + val paymentMethod: OnrampPaymentMethod, + ) : OnrampProvidersUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt similarity index 77% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt index 34902a7683..ce0cba166b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt @@ -1,24 +1,24 @@ -package com.tangem.features.onramp.mainv2.entity.converter +package com.tangem.features.onramp.main.entity.converter import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import java.math.BigDecimal -internal class OnrampV2AmountFieldChangeConverter( - private val currentStateProvider: Provider, +internal class OnrampAmountFieldChangeConverter( + private val currentStateProvider: Provider, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val onrampIntents: OnrampV2Intents, -) : Converter { + private val onrampIntents: OnrampIntents, +) : Converter { - override fun convert(value: String): OnrampV2MainComponentUM { + override fun convert(value: String): OnrampMainComponentUM { val state = currentStateProvider() - if (state !is OnrampV2MainComponentUM.Content) return state + if (state !is OnrampMainComponentUM.Content) return state if (value.isEmpty()) return state.emptyState() @@ -36,13 +36,13 @@ internal class OnrampV2AmountFieldChangeConverter( amountFieldModel = amountFieldModel, secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, offersBlockState = OnrampOffersBlockUM.Loading, errorNotification = null, ) } - private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content { + private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { val amountFieldModel = amountBlockState.amountFieldModel.copy( value = "", fiatValue = "", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt similarity index 71% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt index 4bf1a7285a..27b5cdabd5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt @@ -1,7 +1,7 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory -import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState import kotlinx.collections.immutable.toPersistentList internal class OnrampAmountButtonUMStateFactory { @@ -12,7 +12,7 @@ internal class OnrampAmountButtonUMStateFactory { currencyCode: String, currencySymbol: String, onAmountValueChanged: (String) -> Unit, - ): OnrampV2AmountButtonUMState { + ): OnrampAmountButtonUMState { return when (currencyCode) { USD_CODE, EUR_CODE -> { val buttons = defaultPreselectedAmount.map { value -> @@ -22,9 +22,9 @@ internal class OnrampAmountButtonUMStateFactory { onClick = { onAmountValueChanged(value.toString()) }, ) }.toPersistentList() - OnrampV2AmountButtonUMState.Loaded(buttons) + OnrampAmountButtonUMState.Loaded(buttons) } - else -> OnrampV2AmountButtonUMState.None + else -> OnrampAmountButtonUMState.None } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt similarity index 83% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt index a91f463964..462c27438a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.resourceReference @@ -11,34 +11,34 @@ import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.model.AmountType import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.converter.OnrampAmountFieldChangeConverter import com.tangem.utils.Provider -internal class OnrampV2AmountStateFactory( - private val currentStateProvider: Provider, +internal class OnrampAmountStateFactory( + private val currentStateProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampV2Intents, + private val onrampIntents: OnrampIntents, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, ) { - private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy( + private val onrampAmountFieldChangeConverter: OnrampAmountFieldChangeConverter by lazy( mode = LazyThreadSafetyMode.NONE, ) { - OnrampV2AmountFieldChangeConverter( + OnrampAmountFieldChangeConverter( currentStateProvider = currentStateProvider, onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, onrampIntents = onrampIntents, ) } - fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM { + fun getOnAmountValueChange(value: String): OnrampMainComponentUM { return onrampAmountFieldChangeConverter.convert(value) } - fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM { + fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState @@ -72,9 +72,9 @@ internal class OnrampV2AmountStateFactory( ) } - fun getSecondaryFieldAmountErrorState(quotes: List): OnrampV2MainComponentUM { + fun getSecondaryFieldAmountErrorState(quotes: List): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState @@ -91,23 +91,23 @@ internal class OnrampV2AmountStateFactory( ) } - fun getAmountSecondaryFieldResetState(): OnrampV2MainComponentUM { + fun getAmountSecondaryFieldResetState(): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState if (amountState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Empty) return currentState return currentState.copy( amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, errorNotification = null, offersBlockState = currentState.offersBlockState, ) } private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( - amountState: OnrampNewAmountBlockUM, + amountState: OnrampAmountBlockUM, ): OnrampSecondaryFieldErrorUM.Error { val amount = error.requiredAmount.format { fiat( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt similarity index 89% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt index c16f800f59..889f62feca 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt @@ -1,24 +1,23 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.onramp.model.* import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.main.entity.* import com.tangem.utils.Provider import kotlinx.collections.immutable.toPersistentList internal class OnrampOffersStateFactory( - private val currentStateProvider: Provider, - private val onrampIntents: OnrampV2Intents, + private val currentStateProvider: Provider, + private val onrampIntents: OnrampIntents, ) { - fun getOffersState(offers: List): OnrampV2MainComponentUM { - val currentState = currentStateProvider.invoke() - return when (currentState) { - is OnrampV2MainComponentUM.InitialLoading -> currentState - is OnrampV2MainComponentUM.Content -> { + fun getOffersState(offers: List): OnrampMainComponentUM { + return when (val currentState = currentStateProvider.invoke()) { + is OnrampMainComponentUM.InitialLoading -> currentState + is OnrampMainComponentUM.Content -> { if (currentState.offersBlockState is OnrampOffersBlockUM.Loading) { return currentState } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 8faf435657..1d19b99737 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -8,7 +8,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError @@ -22,16 +24,30 @@ import java.math.BigDecimal internal class OnrampStateFactory( private val currentStateProvider: Provider, + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, private val cryptoCurrency: CryptoCurrency, private val onrampIntents: OnrampIntents, ) { - fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading { + fun getInitialState( + currency: String, + onClose: () -> Unit, + openSettings: () -> Unit, + ): OnrampMainComponentUM.InitialLoading { return OnrampMainComponentUM.InitialLoading( - currency = currency, - onClose = onClose, - openSettings = onrampIntents::openSettings, errorNotification = null, + topBarConfig = OnrampMainTopBarUM( + title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), + startButtonUM = TopAppBarButtonUM.Close( + onCloseClick = onClose, + enabled = true, + ), + endButtonUM = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_more_vertical_24, + onClicked = openSettings, + isEnabled = false, + ), + ), ) } @@ -42,12 +58,19 @@ internal class OnrampStateFactory( is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) } + + val initialAmountBlockState = getInitialAmountBlockState(currency) + return OnrampMainComponentUM.Content( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = state.buyButtonConfig, - amountBlockState = getInitialAmountBlockState(currency), - providerBlockState = OnrampProviderBlockUM.Empty, + amountBlockState = initialAmountBlockState, + offersBlockState = OnrampOffersBlockUM.Empty, errorNotification = null, + onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( + currencyCode = currency.code, + currencySymbol = currency.unit, + onAmountValueChanged = onrampIntents::onAmountValueChanged, + ), ) } @@ -68,21 +91,6 @@ internal class OnrampStateFactory( } } - private fun getNoPairsErrorState(): OnrampMainComponentUM { - val state = currentStateProvider() - val contentState = state as? OnrampMainComponentUM.Content ?: return state - - return contentState.copy( - buyButtonConfig = contentState.buyButtonConfig.copy(isEnabled = false), - amountBlockState = contentState.amountBlockState.copy( - amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error( - error = resourceReference(R.string.onramp_no_available_providers), - ), - ), - ) - } - fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM { val state = currentStateProvider() val endButton = when (val button = state.topBarConfig.endButtonUM) { @@ -93,15 +101,15 @@ internal class OnrampStateFactory( return when (state) { is OnrampMainComponentUM.Content -> state.copy( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = state.buyButtonConfig.copy(isEnabled = false), - amountBlockState = state.amountBlockState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), - ), - providerBlockState = OnrampProviderBlockUM.Empty, + offersBlockState = OnrampOffersBlockUM.Empty, errorNotification = NotificationUM.Warning.OnrampErrorNotification( errorCode = errorCode, onRefresh = onRefresh, ), + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + amountBlockState = state.amountBlockState.copy( + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, + ), ) is OnrampMainComponentUM.InitialLoading -> state.copy( errorNotification = NotificationUM.Warning.OnrampErrorNotification( @@ -112,6 +120,22 @@ internal class OnrampStateFactory( } } + private fun getNoPairsErrorState(): OnrampMainComponentUM { + val state = currentStateProvider() + val contentState = state as? OnrampMainComponentUM.Content ?: return state + + return contentState.copy( + amountBlockState = contentState.amountBlockState.copy( + amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error( + error = resourceReference(R.string.onramp_no_available_providers), + ), + ), + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + offersBlockState = OnrampOffersBlockUM.Empty, + ) + } + private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM { return OnrampAmountBlockUM( currencyUM = OnrampCurrencyUM( @@ -119,11 +143,12 @@ internal class OnrampStateFactory( iconUrl = currency.image, precision = currency.precision, onClick = onrampIntents::openCurrenciesList, + unit = currency.unit, ), amountFieldModel = AmountFieldModel( value = "", fiatValue = "", - onValueChange = { onrampIntents.onAmountValueChanged(value = it, isValuePasted = false) }, + onValueChange = onrampIntents::onAmountValueChanged, keyboardOptions = KeyboardOptions( imeAction = ImeAction.None, keyboardType = KeyboardType.Number, @@ -139,7 +164,7 @@ internal class OnrampStateFactory( isValuePasted = false, onValuePastedTriggerDismiss = {}, ), - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ) } @@ -149,8 +174,4 @@ internal class OnrampStateFactory( decimals = currency.precision, type = AmountType.FiatType(currency.code), ) - - companion object { - const val PREDEFINED_SEPA_AMOUNT = "100" - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt deleted file mode 100644 index ef9cff01ac..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.features.onramp.main.entity.factory.amount - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal - -internal class OnrampAmountFieldChangeConverter( - private val currentStateProvider: Provider, -) : Converter { - - override fun convert(input: Input): OnrampMainComponentUM { - val value = input.value - val isValuePasted = input.isValuePasted - - val state = currentStateProvider() - if (state !is OnrampMainComponentUM.Content) return state - - if (value.isEmpty()) return state.emptyState() - - val amountState = state.amountBlockState - val amountTextField = amountState.amountFieldModel - val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO - val isDoneActionEnabled = !fiatDecimal.isNullOrZero() - val amountFieldModel = amountState.amountFieldModel.copy( - fiatValue = value, - fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal), - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - isValuePasted = isValuePasted, - ) - - return state.copy( - amountBlockState = amountState.copy( - amountFieldModel = amountFieldModel, - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, - ), - providerBlockState = OnrampProviderBlockUM.Loading, - ) - } - - private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { - val amountFieldModel = amountBlockState.amountFieldModel.copy( - value = "", - fiatValue = "", - cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO), - fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO), - isError = false, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ) - return copy( - amountBlockState = amountBlockState.copy( - amountFieldModel = amountFieldModel, - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), - ), - buyButtonConfig = buyButtonConfig.copy(isEnabled = false), - providerBlockState = OnrampProviderBlockUM.Empty, - ) - } - - data class Input(val value: String, val isValuePasted: Boolean) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt deleted file mode 100644 index 24010b69e9..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.tangem.features.onramp.main.entity.factory.amount - -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.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampQuote -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.AmountType -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.* -import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.utils.Provider -import com.tangem.utils.extensions.isSingleItem - -internal class OnrampAmountStateFactory( - private val currentStateProvider: Provider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampIntents, - private val cryptoCurrency: CryptoCurrency, - private val needApplyFCARestrictions: Provider, -) { - - private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( - currentStateProvider = currentStateProvider, - ) - - fun getOnAmountValueChange(value: String, isValuePasted: Boolean) = - onrampAmountFieldChangeConverter.convert(OnrampAmountFieldChangeConverter.Input(value, isValuePasted)) - - fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - return currentState.copy( - amountBlockState = amountState.copy( - currencyUM = amountState.currencyUM.copy( - code = currency.code, - iconUrl = currency.image, - precision = currency.precision, - ), - amountFieldModel = amountState.amountFieldModel.copy( - isError = false, - fiatAmount = amountState.amountFieldModel.fiatAmount.copy( - currencySymbol = currency.unit, - decimals = currency.precision, - type = AmountType.FiatType(currency.code), - ), - ), - ), - ) - } - - fun getAmountSecondaryLoadingState(): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - - return currentState.copy( - amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), - providerBlockState = OnrampProviderBlockUM.Loading, - buyButtonConfig = currentState.buyButtonConfig.copy(isEnabled = false), - errorNotification = null, - ) - } - - fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState - - return currentState.copy( - amountBlockState = amountState.copy( - amountFieldModel = amountState.amountFieldModel.copy(isError = false), - secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel, - ), - buyButtonConfig = currentState.buyButtonConfig.copy( - isEnabled = quote is OnrampQuote.Data, - onClick = { - if (quote is OnrampQuote.Data) { - onrampIntents.onBuyClick( - OnrampProviderWithQuote.Data( - provider = quote.provider, - paymentMethod = quote.paymentMethod, - toAmount = quote.toAmount, - fromAmount = quote.fromAmount, - ), - ) - } - }, - ), - errorNotification = null, - ) - } - - fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - analyticsEventHandler.send( - OnrampAnalyticsEvent.ProviderCalculated( - providerName = selectedQuote.provider.info.name, - tokenSymbol = cryptoCurrency.symbol, - paymentMethod = selectedQuote.paymentMethod.name, - ), - ) - - val bestProvider = quotes.firstOrNull() - val isMultipleQuotes = !quotes.isSingleItem() - val isOtherQuotesHasData = quotes - .filter { it.paymentMethod == selectedQuote.paymentMethod } - .filterNot { it == bestProvider } - .any { it is OnrampQuote.Data } - - val isBestProvider = selectedQuote == bestProvider && - isMultipleQuotes && - isOtherQuotesHasData && - !needApplyFCARestrictions() - - return currentState.copy( - providerBlockState = selectedQuote.toProviderBlockState(isBestProvider), - ) - } - - fun getAmountSecondaryUpdatedState( - providerResult: SelectProviderResult, - isBestRate: Boolean, - ): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - val secondaryField = when (providerResult) { - is SelectProviderResult.ProviderWithError -> { - providerResult.quoteError.toSecondaryFieldUiModel(amountState) - } - is SelectProviderResult.ProviderWithQuote -> { - val amount = providerResult.toAmount.value.format { - crypto(symbol = providerResult.toAmount.symbol, decimals = providerResult.toAmount.decimals) - } - OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) - } - } - return currentState.copy( - amountBlockState = amountState.copy(secondaryFieldModel = secondaryField), - providerBlockState = OnrampProviderBlockUM.Content( - paymentMethod = providerResult.paymentMethod, - providerId = providerResult.provider.id, - providerName = providerResult.provider.info.name, - isBestRate = isBestRate && !needApplyFCARestrictions(), - onClick = onrampIntents::openProviders, - termsOfUseLink = providerResult.provider.info.termsOfUseLink, - privacyPolicyLink = providerResult.provider.info.privacyPolicyLink, - onLinkClick = onrampIntents::onLinkClick, - ), - buyButtonConfig = currentState.buyButtonConfig.copy( - isEnabled = providerResult is SelectProviderResult.ProviderWithQuote, - onClick = { - if (providerResult is SelectProviderResult.ProviderWithQuote) { - onrampIntents.onBuyClick( - OnrampProviderWithQuote.Data( - provider = providerResult.provider, - paymentMethod = providerResult.paymentMethod, - toAmount = providerResult.toAmount, - fromAmount = providerResult.fromAmount, - ), - ) - } - }, - ), - errorNotification = null, - ) - } - - fun getAmountSecondaryResetState(): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - - if (amountState.secondaryFieldModel is OnrampAmountSecondaryFieldUM.Content) return currentState - - return currentState.copy( - amountBlockState = amountState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content( - amount = TextReference.EMPTY, - ), - ), - errorNotification = null, - ) - } - - private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM { - return OnrampProviderBlockUM.Content( - paymentMethod = paymentMethod, - providerId = provider.id, - providerName = provider.info.name, - isBestRate = isBestRate, - onClick = onrampIntents::openProviders, - termsOfUseLink = provider.info.termsOfUseLink, - privacyPolicyLink = provider.info.privacyPolicyLink, - onLinkClick = onrampIntents::onLinkClick, - ) - } - - private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? { - return when (this) { - is OnrampQuote.Error -> null - is OnrampQuote.Data -> { - val amount = toAmount.value.format { - crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) - } - OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) - } - is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState) - } - } - - private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( - amountState: OnrampAmountBlockUM, - ): OnrampAmountSecondaryFieldUM.Error { - val amount = error.requiredAmount.format { - fiat( - fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol, - fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol, - ) - } - - val errorTextRes = when (error) { - is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) - R.string.onramp_max_amount_restriction - } - is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) - R.string.onramp_min_amount_restriction - } - } - - return OnrampAmountSecondaryFieldUM.Error( - resourceReference( - errorTextRes, - wrappedList(amount), - ), - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 7c501be535..b72f54919d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -1,38 +1,25 @@ package com.tangem.features.onramp.main.model -import androidx.compose.runtime.mutableStateOf import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.core.analytics.api.AnalyticsEventHandler 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.decompose.ui.UiMessageSender -import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.fields.InputManager -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.EventMessageAction -import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory -import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT -import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory -import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.features.onramp.utils.model.EUR_CURRENCY import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,7 +30,6 @@ import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.util.Locale import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -51,85 +37,141 @@ internal class OnrampMainComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val router: Router, - private val isDemoCardUseCase: IsDemoCardUseCase, private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, private val getOnrampCountryUseCase: GetOnrampCountryUseCase, private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, - private val onrampSaveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase, - private val onrampGetDefaultCurrencyUseCase: OnrampGetDefaultCurrencyUseCase, private val amountInputManager: InputManager, - private val messageSender: UiMessageSender, - private val urlOpener: UrlOpener, - getWalletsUseCase: GetWalletsUseCase, - getUserCountryUseCase: GetUserCountryUseCase, + private val getOnrampOffersUseCase: GetOnrampOffersUseCase, paramsContainer: ParamsContainer, + getWalletsUseCase: GetWalletsUseCase, ) : Model(), OnrampIntents { - private val params: OnrampMainComponent.Params = paramsContainer.require() + val params = paramsContainer.require() - private var shouldForceChooseSepa = params.isLaunchSepa - private var currencyToRestore: OnrampCurrency? = null - - val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - private val lastUpdateState = mutableStateOf(null) - private var userCountry: UserCountry? = null + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountButtonUMStateFactory() + } @Suppress("PropertyUsedBeforeDeclaration") - private val stateFactory = OnrampStateFactory( - currentStateProvider = Provider { state.value }, - cryptoCurrency = params.cryptoCurrency, - onrampIntents = this, - ) + private val stateFactory: OnrampStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampStateFactory( + currentStateProvider = Provider { state.value }, + cryptoCurrency = params.cryptoCurrency, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + } val state: StateFlow field = MutableStateFlow( value = stateFactory.getInitialState( currency = params.cryptoCurrency.name, onClose = ::onCloseClick, + openSettings = ::openSettings, ), ) - private val amountStateFactory = OnrampAmountStateFactory( - currentStateProvider = Provider { state.value }, - analyticsEventHandler = analyticsEventHandler, - onrampIntents = this, - cryptoCurrency = params.cryptoCurrency, - needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() }, - ) + private val amountStateFactory: OnrampAmountStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountStateFactory( + currentStateProvider = Provider { state.value }, + analyticsEventHandler = analyticsEventHandler, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + } + + private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampOffersStateFactory( + currentStateProvider = Provider { state.value }, + onrampIntents = this, + ) + } private val quotesTaskScheduler = SingleTaskScheduler() - init { - userCountry = getUserCountryUseCase.invokeSync().getOrNull() - ?: UserCountry.Other(Locale.getDefault().country) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + init { modelScope.launch { clearOnrampCacheUseCase() - - if (params.isLaunchSepa) { - currencyToRestore = onrampGetDefaultCurrencyUseCase.invoke().getOrNull() - onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY) - } } - + startLoadingQuotes() sendScreenOpenAnalytics() checkResidenceCountry() subscribeToAmountChanges() + subscribeToCountryAndCurrencyUpdates() + subscribeToQuotesUpdate() + subscribeOnOffers() } - private fun sendScreenOpenAnalytics() { + override fun onDestroy() { + modelScope.launch { clearOnrampCacheUseCase.invoke() } + quotesTaskScheduler.cancelTask() + super.onDestroy() + } + + override fun onAmountValueChanged(value: String) { + state.update { amountStateFactory.getOnAmountValueChange(value) } + modelScope.launch { amountInputManager.update(value) } + } + + override fun openSettings() { + params.openSettings.invoke() + } + + override fun openCurrenciesList() { + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) + } + + override fun onBuyClick( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) { + val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return analyticsEventHandler.send( - OnrampAnalyticsEvent.ScreenOpened( - source = params.source, + OnrampAnalyticsEvent.OnBuyClick( + providerName = quote.provider.info.name, + currency = currentContentState.amountBlockState.currencyUM.code, tokenSymbol = params.cryptoCurrency.symbol, ), ) + sendOfferClickEvent( + quote = quote, + onrampOfferAdvantagesUM = onrampOfferAdvantagesUM, + categoryUM = categoryUM, + ) + params.openRedirectPage(quote) + } + + override fun openProviders() { + val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return + val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.AllOffers(amountCurrentCode)) + } + + override fun onRefresh() { + state.update { + stateFactory.getInitialState( + currency = params.cryptoCurrency.name, + onClose = router::pop, + openSettings = ::openSettings, + ) + } + modelScope.launch { + clearOnrampCacheUseCase.invoke() + checkResidenceCountry() + handleOnrampAvailable() + } + } + + fun onStop() { + quotesTaskScheduler.cancelTask() } fun handleOnrampAvailable() { @@ -137,92 +179,6 @@ internal class OnrampMainComponentModel @Inject constructor( subscribeToQuotesUpdate() } - fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) { - state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) } - - if (result.paymentMethod.id != SEPA_METHOD_ID) { - shouldForceChooseSepa = false - } - } - - private fun checkResidenceCountry() { - modelScope.launch { - checkOnrampAvailabilityUseCase(userWallet) - .onRight(::handleOnrampAvailability) - .onLeft(::handleOnrampError) - } - } - - private fun handleOnrampAvailability(availability: OnrampAvailability) { - when (availability) { - is OnrampAvailability.Available -> handleOnrampAvailable() - is OnrampAvailability.ConfirmResidency, - is OnrampAvailability.NotSupported, - -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) - } - } - - private fun subscribeToCountryAndCurrencyUpdates() { - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.fold( - ifLeft = ::handleOnrampError, - ifRight = { country -> - if (country == null) return@onEach - - val wasInitialLoading = state.value is OnrampMainComponentUM.InitialLoading - state.update { prevState -> - if (prevState is OnrampMainComponentUM.InitialLoading) { - stateFactory.getReadyState(country.defaultCurrency) - } else { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - } - - updatePairsAndQuotes() - - if (wasInitialLoading && params.isLaunchSepa) { - onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true) - } - }, - ) - } - .launchIn(modelScope) - } - - private fun subscribeToAmountChanges() = modelScope.launch { - amountInputManager.query - .filter(String::isNotEmpty) - .collectLatest { _ -> - state.update { amountStateFactory.getAmountSecondaryLoadingState() } - startLoadingQuotes() - } - } - - private suspend fun updatePairsAndQuotes() { - state.update { prevState -> - val contentState = state.value as? OnrampMainComponentUM.Content ?: return@update prevState - - if (contentState.amountBlockState.amountFieldModel.fiatValue.isNotEmpty()) { - amountStateFactory.getAmountSecondaryLoadingState() - } else { - prevState - } - } - - fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( - ifLeft = ::handleOnrampError, - ifRight = { state.update { amountStateFactory.getAmountSecondaryResetState() } }, - ) - startLoadingQuotes() - } - - private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) - sendOnrampErrorAnalytic(onrampError) - state.update { stateFactory.getOnrampErrorState(onrampError) } - } - private fun startLoadingQuotes() { quotesTaskScheduler.cancelTask() quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask()) @@ -233,11 +189,12 @@ internal class OnrampMainComponentModel @Inject constructor( delay = UPDATE_DELAY, task = { runSuspendCatching { - val content = state.value as? OnrampMainComponentUM.Content ?: return@runSuspendCatching - val amountBlockState = content.amountBlockState - if (amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) { - return@runSuspendCatching - } + val amountBlockState = (state.value as? OnrampMainComponentUM.Content)?.amountBlockState + ?: return@runSuspendCatching + + val fiatAmount = amountBlockState.amountFieldModel.fiatAmount + if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching + fetchQuotesUseCase.invoke( userWallet = userWallet, amount = amountBlockState.amountFieldModel.fiatAmount, @@ -250,6 +207,84 @@ internal class OnrampMainComponentModel @Inject constructor( ) } + private fun checkResidenceCountry() { + modelScope.launch { + checkOnrampAvailabilityUseCase(userWallet) + .onRight(::handleOnrampAvailability) + .onLeft(::handleOnrampError) + } + } + + private fun handleOnrampAvailability(availability: OnrampAvailability) { + when (availability) { + is OnrampAvailability.Available -> Unit + is OnrampAvailability.ConfirmResidency, + is OnrampAvailability.NotSupported, + -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) + } + } + + private fun onCloseClick() { + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) + router.pop() + } + + private fun subscribeOnOffers() = modelScope.launch { + getOnrampOffersUseCase + .invoke() + .collectLatest { maybeOffers -> + maybeOffers.fold( + ifLeft = ::handleOnrampError, + ifRight = { offers -> + val currentState = state.value + if (currentState is OnrampMainComponentUM.Content) { + if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) { + state.update { + currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty) + } + return@fold + } + state.update { + onrampOffersStateFactory.getOffersState(offers) + } + } + }, + ) + } + } + + private fun subscribeToAmountChanges() = modelScope.launch { + amountInputManager.query + .filter(String::isNotEmpty) + .collectLatest { _ -> + startLoadingQuotes() + } + } + + private fun subscribeToCountryAndCurrencyUpdates() { + getOnrampCountryUseCase.invoke() + .onEach { maybeCountry -> + maybeCountry.fold( + ifLeft = ::handleOnrampError, + ifRight = { country -> + if (country == null) return@onEach + state.update { prevState -> + when (prevState) { + is OnrampMainComponentUM.Content -> { + amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + } + is OnrampMainComponentUM.InitialLoading -> { + stateFactory.getReadyState(country.defaultCurrency) + } + } + } + updatePairsAndQuotes() + }, + ) + } + .launchIn(modelScope) + } + private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() .conflate() @@ -262,152 +297,31 @@ internal class OnrampMainComponentModel @Inject constructor( .launchIn(modelScope) } - override fun onAmountValueChanged(value: String, isValuePasted: Boolean) { - state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) } - modelScope.launch { amountInputManager.update(value) } - } - - override fun openSettings() { - params.openSettings() - } - - override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { - if (userWallet is UserWallet.Cold && isDemoCardUseCase.invoke(userWallet.cardId)) { - showDemoWarning() - } else { - val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return - analyticsEventHandler.send( - OnrampAnalyticsEvent.OnBuyClick( - providerName = quote.provider.info.name, - currency = currentContentState.amountBlockState.currencyUM.code, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - params.openRedirectPage(quote) - } - } - - override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) - bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) - } - - override fun openProviders() { - val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return - val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return - bottomSheetNavigation.activate( - OnrampMainBottomSheetConfig.ProvidersList( - selectedPaymentMethod = providerContentState.paymentMethod, - selectedProviderId = providerContentState.providerId, - ), - ) - } - - override fun onRefresh() { - state.update { - stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = router::pop, - ) - } - quotesTaskScheduler.cancelTask() - modelScope.launch { - clearOnrampCacheUseCase.invoke() - checkResidenceCountry() - } - } - - override fun onLinkClick(link: String) = urlOpener.openUrl(link) - - override fun onDestroy() { - modelScope.launch { clearOnrampCacheUseCase.invoke() } - quotesTaskScheduler.cancelTask() - - modelScope.launch { - if (params.isLaunchSepa) { - currencyToRestore?.let { onrampSaveDefaultCurrencyUseCase.invoke(it) } - } - } - - super.onDestroy() - } - - private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) - router.pop() - } - private fun handleQuoteResult(quotes: List) { sendOnrampQuotesErrorAnalytic(quotes) - - val quote = selectOrUpdateQuote(quotes) - - if (quote == null) { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - lastUpdateState.value = null - return - } - state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) } - } - - /** - * !!! Important quote selection logic !!! - * Selects or updated quote based on input data (amount, country, currency). - * If input data has changed select new best quote, otherwise last selected quote. - * If last selected quote on same input data is in an error state, select next best quote - * If new best quote or next best quote does not exist (i.e. Error state) select nothing. - */ - private fun selectOrUpdateQuote(quotes: List): OnrampQuote? { - val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } - - val bestSepaQuote = if (params.isLaunchSepa && shouldForceChooseSepa) { - quotes.filterIsInstance() - .filter { it.paymentMethod.id == SEPA_METHOD_ID } - .maxByOrNull { it.toAmount.value } - } else { - null - } - - // Check if amount, country or currency has changed - val newQuote = bestSepaQuote ?: if (isAmountOrCountryChanged(quoteToCheck)) { - quoteToCheck - } else { - val state = state.value as? OnrampMainComponentUM.Content - val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content - - // Get current selected quote to update - val lastSelectedQuote = quotes.firstOrNull { quote -> - quote.provider.id == providerState?.providerId && - quote.paymentMethod.id == providerState.paymentMethod.id + when { + quotes.isEmpty() -> { + state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } } - - // Check if selected updated quote is not error - if (lastSelectedQuote is OnrampQuote.Error) { - quoteToCheck - } else { - lastSelectedQuote + quotes.all { it is OnrampQuote.AmountError } -> { + state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } + } + quotes.none { it is OnrampQuote.Data } -> { + state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } + } + else -> { + state.update { prevState -> + val resetState = amountStateFactory.getAmountSecondaryFieldResetState() + if (prevState is OnrampMainComponentUM.Content && + resetState is OnrampMainComponentUM.Content && + prevState.offersBlockState is OnrampOffersBlockUM.Loading + ) { + resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty) + } else { + resetState + } + } } - } - if (newQuote != null) { - updateProvider(newQuote, quotes) - } - - return newQuote - } - - private fun updateProvider(quote: OnrampQuote, quotes: List) { - lastUpdateState.value = OnrampLastUpdate( - quote.fromAmount, - quote.countryCode, - quote.paymentMethod, - ) - - if (quote.paymentMethod.id != SEPA_METHOD_ID) { - shouldForceChooseSepa = false - } - - state.update { - amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes) } } @@ -415,41 +329,30 @@ internal class OnrampMainComponentModel @Inject constructor( state.update { prevState -> (prevState as? OnrampMainComponentUM.Content)?.copy( errorNotification = null, - providerBlockState = OnrampProviderBlockUM.Loading, + offersBlockState = OnrampOffersBlockUM.Loading, amountBlockState = prevState.amountBlockState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ), ) ?: prevState } startLoadingQuotes() } - private fun showDemoWarning() { - val alertUM = AlertDemoModeUM(onConfirmClick = {}) - val message = DialogMessage( - title = alertUM.title, - message = alertUM.message, - firstActionBuilder = { - EventMessageAction( - title = alertUM.confirmButtonText, - onClick = alertUM.onConfirmClick, - ) + private suspend fun updatePairsAndQuotes() { + fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( + ifLeft = ::handleOnrampError, + ifRight = { + state.update { + amountStateFactory.getAmountSecondaryFieldResetState() + } + startLoadingQuotes() }, - secondActionBuilder = { cancelAction() }, ) - - messageSender.send(message) } - private fun sendOnrampErrorAnalytic(error: OnrampError) { - val content = state.value as? OnrampMainComponentUM.Content - val providerContent = content?.providerBlockState as? OnrampProviderBlockUM.Content - analyticsEventHandler.sendOnrampErrorEvent( - error = error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = providerContent?.providerName, - paymentMethod = providerContent?.paymentMethod?.name, - ) + private fun handleOnrampError(onrampError: OnrampError) { + Timber.e(onrampError.toString()) + state.update { stateFactory.getOnrampErrorState(onrampError) } } private fun sendOnrampQuotesErrorAnalytic(quotes: List) { @@ -467,20 +370,48 @@ internal class OnrampMainComponentModel @Inject constructor( providerName = errorState.provider.info.name, paymentMethod = errorState.paymentMethod.name, ) - else -> { /* no-op */ - } + else -> Unit } } } - private fun isAmountOrCountryChanged(quote: OnrampQuote?): Boolean { - return lastUpdateState.value?.fromAmount != quote?.fromAmount || - lastUpdateState.value?.countryCode != quote?.countryCode + private fun sendScreenOpenAnalytics() { + analyticsEventHandler.send( + OnrampAnalyticsEvent.ScreenOpened( + source = params.source, + tokenSymbol = params.cryptoCurrency.symbol, + ), + ) + } + + private fun sendOfferClickEvent( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) { + val event = when (categoryUM) { + OnrampOfferCategoryUM.RecentlyUsed -> { + OnrampAnalyticsEvent.RecentlyBuyClicked( + tokenSymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethod = quote.paymentMethod.name, + ) + } + OnrampOfferCategoryUM.Recommended -> { + onrampOfferAdvantagesUM.toAnalyticsEvent( + cryptoCurrencySymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethodName = quote.paymentMethod.name, + ) + } + } + + if (event != null) { + analyticsEventHandler.send(event) + } } private companion object { const val UPDATE_DELAY = 10_000L - - const val SEPA_METHOD_ID = "sepa" } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index 0bdfa02446..c2293646bd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.main.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,61 +19,89 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM -import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM import com.tangem.features.onramp.main.entity.OnrampCurrencyUM +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampSecondaryFieldErrorUM @Composable -internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) { +internal fun OnrampAmountContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { Column( modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(vertical = TangemTheme.dimens.spacing28), + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + ) + .padding(vertical = 24.dp, horizontal = 16.dp) + .animateContentSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - OnrampCurrencyIcon(currencyUM = state.currencyUM) - OnrampAmountField(amountField = state.amountFieldModel) - OnrampAmountSecondary(state = state.secondaryFieldModel) + OnrampHeaderTitle() + + OnrampAmountField( + amountField = state.amountBlockState.amountFieldModel, + currencyCode = state.amountBlockState.currencyUM.code, + ) + + AnimatedVisibility( + visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty, + ) { + if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) { + OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) + } + } + + SpacerH(20.dp) + + OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) } } @Composable -private fun OnrampAmountField(amountField: AmountFieldModel) { +private fun OnrampHeaderTitle() { + Text( + text = stringResourceSafe(R.string.onramp_you_will_pay_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { val decimalFormat = rememberDecimalFormat() val requester = remember { FocusRequester() } - val symbolColor = if (amountField.fiatValue.isBlank()) { - TangemTheme.colors.text.disabled - } else { - TangemTheme.colors.text.primary1 - } AmountTextField( value = amountField.fiatValue, decimals = amountField.fiatAmount.decimals, visualTransformation = AmountVisualTransformation( decimals = amountField.fiatAmount.decimals, - symbol = amountField.fiatAmount.currencySymbol, - currencyCode = amountField.fiatAmount.currencySymbol, + symbol = currencyCode, + currencyCode = currencyCode, decimalFormat = decimalFormat, - symbolColor = symbolColor, + symbolColor = if (amountField.fiatValue.isBlank()) { + TangemTheme.colors.text.disabled + } else { + TangemTheme.colors.text.primary1 + }, ), onValueChange = amountField.onValueChange, keyboardOptions = amountField.keyboardOptions, keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( + textStyle = TangemTheme.typography.head.copy( color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), @@ -82,7 +112,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { modifier = Modifier .focusRequester(requester) .padding( - top = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ) @@ -96,7 +127,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { } @Composable -private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { +private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) { Box( modifier = Modifier .fillMaxWidth() @@ -107,24 +138,12 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { ), contentAlignment = Alignment.Center, ) { - when (state) { - is OnrampAmountSecondaryFieldUM.Content -> Text( - text = state.amount.resolveReference(), - style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) - is OnrampAmountSecondaryFieldUM.Error -> Text( - text = state.error.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) - is OnrampAmountSecondaryFieldUM.Loading -> TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(TangemTheme.dimens.size62), - ) - } + Text( + text = state.error.resolveReference(), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) } } @@ -132,20 +151,27 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) { Row( modifier = modifier - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.button.secondary) .clickable(onClick = currencyUM.onClick) - .padding(start = TangemTheme.dimens.spacing24), + .padding(horizontal = 6.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { AsyncImage( modifier = Modifier - .size(TangemTheme.dimens.size40) + .size(20.dp) .clip(CircleShape) .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), model = currencyUM.iconUrl, contentDescription = null, ) + Text( + text = currencyUM.code, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), + textAlign = TextAlign.Center, + ) Icon( modifier = Modifier .size(TangemTheme.dimens.size16) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt deleted file mode 100644 index 11e356cfef..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.tangem.features.onramp.main.ui - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.ClickableText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.appendColored -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM - -private const val TERMS_OF_USE_KEY = "termsOfUse" -private const val PRIVACY_POLICY_KEY = "privacyPolicy" - -@Composable -internal fun OnrampButtonComponent(state: OnrampMainComponentUM) { - val content = state as? OnrampMainComponentUM.Content - val providerState = content?.providerBlockState as? OnrampProviderBlockUM.Content - Column( - modifier = Modifier - .navigationBarsPadding() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - OnrampTosText(providerState) - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_buy), - onClick = state.buyButtonConfig.onClick, - enabled = state.buyButtonConfig.isEnabled, - ) - } -} - -@Composable -private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { - val termsOfUse = stringResourceSafe(R.string.common_terms_of_use) - val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy) - val tosText = stringResourceSafe(R.string.onramp_legal, termsOfUse, privacyPolicy) - - val clickableAnnotation = buildAnnotatedString { - append(tosText.substringBefore(termsOfUse)) - - pushStringAnnotation(TERMS_OF_USE_KEY, "") - appendColored(termsOfUse, TangemTheme.colors.text.accent) - pop() - - append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy)) - - pushStringAnnotation(PRIVACY_POLICY_KEY, "") - appendColored(privacyPolicy, TangemTheme.colors.text.accent) - pop() - } - - AnimatedContent( - targetState = provider, - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Onramp Legal Info Animation", - ) { state -> - val termsOfUseLink = provider?.termsOfUseLink - val privacyPolicyLink = provider?.privacyPolicyLink - - if (state != null && termsOfUseLink != null && privacyPolicyLink != null) { - ClickableText( - text = clickableAnnotation, - style = TangemTheme.typography.caption2.copy( - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ), - onClick = { offset -> - val tosAnnotations = clickableAnnotation.getStringAnnotations( - tag = TERMS_OF_USE_KEY, - start = offset, - end = offset, - ) - - if (tosAnnotations.any()) { - state.onLinkClick(termsOfUseLink) - } - - val privacyPolicyAnnotations = clickableAnnotation.getStringAnnotations( - tag = PRIVACY_POLICY_KEY, - start = offset, - end = offset, - ) - - if (privacyPolicyAnnotations.any()) { - state.onLinkClick(privacyPolicyLink) - } - }, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt similarity index 83% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt index f2ecc446d7..6b79790e18 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.ui +package com.tangem.features.onramp.main.ui import androidx.compose.animation.* import androidx.compose.animation.core.tween @@ -20,13 +20,13 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM -import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM @Composable -internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { +internal fun BoxScope.OnrampFooterContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { AnimatedVisibility( modifier = modifier .imePadding() @@ -53,16 +53,16 @@ internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content } @Composable -private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { +private fun OnrampAmountButtons(state: OnrampAmountButtonUMState) { val keyboard by keyboardAsState() AnimatedVisibility( - visible = state is OnrampV2AmountButtonUMState.Loaded, + visible = state is OnrampAmountButtonUMState.Loaded, enter = fadeIn(), exit = fadeOut(), ) { when (state) { - is OnrampV2AmountButtonUMState.Loaded -> { + is OnrampAmountButtonUMState.Loaded -> { if (keyboard is Keyboard.Opened) { LazyRow( modifier = Modifier.background(color = TangemTheme.colors.button.secondary), @@ -82,7 +82,7 @@ private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { } } } - OnrampV2AmountButtonUMState.None -> Unit + OnrampAmountButtonUMState.None -> Unit } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index fa0797f476..2b24ad2fb9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -5,13 +5,12 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import com.tangem.core.ui.components.CircleShimmer +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.notifications.Notification @@ -21,41 +20,58 @@ import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.onramp.main.entity.OnrampMainComponentUM @Composable -internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { +internal fun OnrampMainScreen(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { Scaffold( - modifier = modifier.imePadding(), - contentWindowInsets = WindowInsetsZero, - containerColor = TangemTheme.colors.background.secondary, + modifier = modifier.systemBarsPadding(), topBar = { TangemTopAppBar( - modifier = Modifier.statusBarsPadding(), startButton = state.topBarConfig.startButtonUM, endButton = state.topBarConfig.endButtonUM, title = state.topBarConfig.title.resolveReference(), ) }, - content = { innerPadding -> - val contentModifier = Modifier - .padding(innerPadding) - .padding(horizontal = TangemTheme.dimens.spacing16) + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + OnrampMainComponentContent( + state = state, + modifier = Modifier.padding(scaffoldPaddings), + ) + } +} + +@Composable +internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) { + Column( + modifier = Modifier .fillMaxWidth() - .wrapContentHeight() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { when (state) { - is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state) - is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state) + is OnrampMainComponentUM.InitialLoading -> InitialLoading(state = state) + is OnrampMainComponentUM.Content -> Content(state = state) } - }, - floatingActionButton = { - OnrampButtonComponent(state) - }, - floatingActionButtonPosition = FabPosition.Center, - ) + } + + if (state is OnrampMainComponentUM.Content) { + OnrampFooterContent(state = state) + } + } } @Composable private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) { Column( - modifier = modifier, + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { OnrampAmountContentLoading() @@ -64,27 +80,38 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier } @Composable -private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) { +private fun OnrampAmountContentLoading() { Column( - modifier = modifier + modifier = Modifier .fillMaxWidth() .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) .background(TangemTheme.colors.background.action) .padding(vertical = TangemTheme.dimens.spacing28), horizontalAlignment = Alignment.CenterHorizontally, ) { - CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size40)) RectangleShimmer( modifier = Modifier .padding(top = TangemTheme.dimens.spacing16) - .size(width = TangemTheme.dimens.size96, height = TangemTheme.dimens.size24), - radius = TangemTheme.dimens.radius3, + .size(width = 76.dp, height = 20.dp), + radius = TangemTheme.dimens.radius4, ) RectangleShimmer( modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16) - .size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12), - radius = TangemTheme.dimens.radius3, + .padding(top = TangemTheme.dimens.spacing12) + .size(width = 136.dp, height = 44.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(width = 52.dp, height = 16.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing20) + .size(width = 84.dp, height = 28.dp), + radius = TangemTheme.dimens.radius14, ) } } @@ -93,13 +120,20 @@ private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) { private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .wrapContentHeight() .navigationBarsPadding() - .padding(bottom = TangemTheme.dimens.spacing76), + .padding( + bottom = 76.dp, + start = 16.dp, + end = 16.dp, + ), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - OnrampAmountContent(state = state.amountBlockState) - OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth()) + OnrampAmountContent(state = state) + + OnrampOffersContent(state = state.offersBlockState) + if (state.errorNotification != null) Notification(config = state.errorNotification.config) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt similarity index 99% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt index dba9eac25a..4c161835e9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.ui +package com.tangem.features.onramp.main.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility @@ -33,7 +33,7 @@ import com.tangem.core.ui.test.OnrampOffersBlockTestTags import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.main.entity.* import kotlinx.collections.immutable.persistentListOf @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt deleted file mode 100644 index e13fd96310..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.features.onramp.main.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CircularProgressIndicator -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.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import com.tangem.core.ui.extensions.appendSpace -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM -import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon - -@Composable -internal fun OnrampProviderContent(state: OnrampProviderBlockUM, modifier: Modifier = Modifier) { - when (state) { - is OnrampProviderBlockUM.Empty -> Unit - is OnrampProviderBlockUM.Loading -> OnrampProviderLoading(modifier) - is OnrampProviderBlockUM.Content -> OnrampProviderBlock(modifier = modifier, state = state) - } -} - -@Composable -private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .clickable(onClick = state.onClick) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - PaymentMethodIcon(imageUrl = state.paymentMethod.imageUrl) - Column(modifier = Modifier.weight(1F)) { - Text( - text = buildAnnotatedString { - append(stringResourceSafe(id = R.string.onramp_pay_with)) - appendSpace() - withStyle( - style = SpanStyle( - fontWeight = TangemTheme.typography.subtitle2.fontWeight, - color = TangemTheme.colors.text.primary1, - ), - ) { - append(state.paymentMethod.name) - } - }, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - Text( - text = buildAnnotatedString { - append(stringResourceSafe(id = R.string.onramp_via)) - appendSpace() - append(state.providerName) - }, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - AnimatedVisibility( - visible = state.isBestRate, - enter = fadeIn(), - exit = fadeOut(), - label = "Best Rate visibility animation", - ) { - Text( - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent, - shape = RoundedCornerShape(TangemTheme.dimens.radius4), - ) - .padding( - horizontal = TangemTheme.dimens.spacing6, - vertical = TangemTheme.dimens.spacing1, - ), - text = stringResourceSafe(id = R.string.express_provider_best_rate), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary2, - ) - } - } -} - -@Composable -private fun OnrampProviderLoading(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - text = stringResourceSafe(id = R.string.express_provider), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - CircularProgressIndicator( - color = TangemTheme.colors.icon.informative, - strokeWidth = TangemTheme.dimens.size2, - modifier = Modifier.size(TangemTheme.dimens.size16), - ) - Text( - text = stringResourceSafe(id = R.string.express_fetch_best_rates), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt deleted file mode 100644 index 4d417ad448..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.onramp.alloffers.AllOffersComponent -import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig -import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel -import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen -import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultOnrampV2MainComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted private val params: OnrampV2MainComponent.Params, - private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, - private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, - private val allOffersComponentFactory: AllOffersComponent.Factory, -) : OnrampV2MainComponent, AppComponentContext by appComponentContext { - - private val model: OnrampV2MainComponentModel = getOrCreateModel(params) - - init { - lifecycle.subscribe(onStop = model::onStop) - } - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = null, - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsState() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - OnrampNewMainScreen(modifier = modifier, state = state) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: OnrampV2MainBottomSheetConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( - context = childByContext(componentContext), - params = ConfirmResidencyComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - country = config.country, - isLaunchSepa = false, - onDismiss = { - model.bottomSheetNavigation.dismiss() - model.handleOnrampAvailable() - }, - ), - ) - is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( - context = childByContext(componentContext), - params = SelectCurrencyComponent.Params( - userWallet = model.userWallet, - cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( - context = childByContext(componentContext), - params = AllOffersComponent.Params( - userWallet = model.userWallet, - cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, - openRedirectPage = params.openRedirectPage, - amountCurrencyCode = config.amountCurrencyCode, - ), - ) - } - - @AssistedFactory - interface Factory : OnrampV2MainComponent.Factory { - override fun create( - context: AppComponentContext, - params: OnrampV2MainComponent.Params, - ): DefaultOnrampV2MainComponent - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt deleted file mode 100644 index 815fa5060b..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -class DefaultOnrampV2MainFeatureToggle( - private val featureTogglesManager: FeatureTogglesManager, -) : OnrampV2MainFeatureToggle { - override val isOnrampNewMainEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED") -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt deleted file mode 100644 index 9767cf4496..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampSource - -internal interface OnrampV2MainComponent : ComposableContentComponent { - - data class Params( - val userWalletId: UserWalletId, - val cryptoCurrency: CryptoCurrency, - val source: OnrampSource, - val openSettings: () -> Unit, - val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt deleted file mode 100644 index 54595ff8d7..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -internal interface OnrampV2MainFeatureToggle { - val isOnrampNewMainEnabled: Boolean -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt deleted file mode 100644 index 84fb039ffd..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.mainv2.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface OnrampMainV2ComponentModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampV2MainComponentModel::class) - fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt deleted file mode 100644 index 08817d31ac..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.onramp.mainv2.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent -import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface OnrampNewMainComponentModule { - - @Binds - @Singleton - fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory -} - -@Module -@InstallIn(SingletonComponent::class) -internal object FeatureToggleModule { - - @Provides - @Singleton - fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle { - return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt deleted file mode 100644 index 49041e269a..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampProviderWithQuote - -internal interface OnrampV2Intents { - fun onAmountValueChanged(value: String) - fun openSettings() - fun openCurrenciesList() - fun onBuyClick( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) - fun openProviders() - fun onRefresh() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt deleted file mode 100644 index afc654a422..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampCountry -import kotlinx.serialization.Serializable - -@Serializable -sealed interface OnrampV2MainBottomSheetConfig { - @Serializable - data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig - - @Serializable - data object CurrenciesList : OnrampV2MainBottomSheetConfig - - @Serializable - data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt deleted file mode 100644 index 256aeadd72..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference - -@Immutable -internal sealed interface OnrampV2MainComponentUM { - - val topBarConfig: OnrampV2MainTopBarUM - val errorNotification: NotificationUM? - - data class InitialLoading( - override val topBarConfig: OnrampV2MainTopBarUM, - override val errorNotification: NotificationUM?, - ) : OnrampV2MainComponentUM - - data class Content( - override val topBarConfig: OnrampV2MainTopBarUM, - override val errorNotification: NotificationUM?, - val amountBlockState: OnrampNewAmountBlockUM, - val offersBlockState: OnrampOffersBlockUM, - val onrampAmountButtonUMState: OnrampV2AmountButtonUMState, - ) : OnrampV2MainComponentUM -} - -internal data class OnrampV2MainTopBarUM( - val title: TextReference, - val startButtonUM: TopAppBarButtonUM, - val endButtonUM: TopAppBarButtonUM, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt deleted file mode 100644 index 750e9ba8f3..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -sealed interface OnrampV2ProvidersUM { - - data object Empty : OnrampV2ProvidersUM - - data object Loading : OnrampV2ProvidersUM - - data class Content( - val providerId: String, - val paymentMethod: OnrampPaymentMethod, - ) : OnrampV2ProvidersUM -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt deleted file mode 100644 index 06ec867696..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity.factory - -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.Amount -import com.tangem.domain.tokens.model.AmountType -import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.utils.Provider -import java.math.BigDecimal - -internal class OnrampV2StateFactory( - private val currentStateProvider: Provider, - private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val cryptoCurrency: CryptoCurrency, - private val onrampIntents: OnrampV2Intents, -) { - - fun getInitialState( - currency: String, - onClose: () -> Unit, - openSettings: () -> Unit, - ): OnrampV2MainComponentUM.InitialLoading { - return OnrampV2MainComponentUM.InitialLoading( - errorNotification = null, - topBarConfig = OnrampV2MainTopBarUM( - title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM.Close( - onCloseClick = onClose, - enabled = true, - ), - endButtonUM = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_more_vertical_24, - onClicked = openSettings, - isEnabled = false, - ), - ), - ) - } - - fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content { - val state = currentStateProvider() - - val endButton = when (val button = state.topBarConfig.endButtonUM) { - is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) - is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) - } - - val initialAmountBlockState = getInitialAmountBlockState(currency) - - return OnrampV2MainComponentUM.Content( - topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - amountBlockState = initialAmountBlockState, - offersBlockState = OnrampOffersBlockUM.Empty, - errorNotification = null, - onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( - currencyCode = currency.code, - currencySymbol = currency.unit, - onAmountValueChanged = onrampIntents::onAmountValueChanged, - ), - ) - } - - fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM { - return when (onrampError) { - OnrampError.PairsNotFound -> getNoPairsErrorState() - is OnrampError.DataError -> getErrorState( - errorCode = onrampError.code, - onRefresh = onrampIntents::onRefresh, - ) - is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh) - is OnrampError.AmountError.TooBigError, - is OnrampError.AmountError.TooSmallError, - OnrampError.RedirectError.VerificationFailed, - OnrampError.RedirectError.WrongRequestId, - OnrampError.AlreadyHandledTransaction, - -> currentStateProvider() // ignore error state - } - } - - fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM { - val state = currentStateProvider() - val endButton = when (val button = state.topBarConfig.endButtonUM) { - is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) - is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) - } - - return when (state) { - is OnrampV2MainComponentUM.Content -> state.copy( - topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - offersBlockState = OnrampOffersBlockUM.Empty, - errorNotification = NotificationUM.Warning.OnrampErrorNotification( - errorCode = errorCode, - onRefresh = onRefresh, - ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, - amountBlockState = state.amountBlockState.copy( - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ), - ) - is OnrampV2MainComponentUM.InitialLoading -> state.copy( - errorNotification = NotificationUM.Warning.OnrampErrorNotification( - errorCode = errorCode, - onRefresh = onRefresh, - ), - ) - } - } - - private fun getNoPairsErrorState(): OnrampV2MainComponentUM { - val state = currentStateProvider() - val contentState = state as? OnrampV2MainComponentUM.Content ?: return state - - return contentState.copy( - amountBlockState = contentState.amountBlockState.copy( - amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error( - error = resourceReference(R.string.onramp_no_available_providers), - ), - ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, - offersBlockState = OnrampOffersBlockUM.Empty, - ) - } - - private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM { - return OnrampNewAmountBlockUM( - currencyUM = OnrampNewCurrencyUM( - code = currency.code, - iconUrl = currency.image, - precision = currency.precision, - onClick = onrampIntents::openCurrenciesList, - unit = currency.unit, - ), - amountFieldModel = AmountFieldModel( - value = "", - fiatValue = "", - onValueChange = onrampIntents::onAmountValueChanged, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.None, - keyboardType = KeyboardType.Number, - ), - keyboardActions = KeyboardActions(), - isFiatValue = true, - cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency), - fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency), - isError = false, - isWarning = false, - error = TextReference.EMPTY, - isFiatUnavailable = false, - isValuePasted = false, - onValuePastedTriggerDismiss = {}, - ), - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ) - } - - private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount( - currencySymbol = currency.unit, - value = this, - decimals = currency.precision, - type = AmountType.FiatType(currency.code), - ) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt deleted file mode 100644 index 517c304fd6..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ /dev/null @@ -1,417 +0,0 @@ -package com.tangem.features.onramp.mainv2.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.tangem.core.analytics.api.AnalyticsEventHandler -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.components.fields.InputManager -import com.tangem.domain.onramp.* -import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent -import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampQuote -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampOffersStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory -import com.tangem.features.onramp.utils.sendOnrampErrorEvent -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.PeriodicTask -import com.tangem.utils.coroutines.SingleTaskScheduler -import com.tangem.utils.coroutines.runSuspendCatching -import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList", "LargeClass") -internal class OnrampV2MainComponentModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val router: Router, - private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, - private val getOnrampCountryUseCase: GetOnrampCountryUseCase, - private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, - private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, - private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, - private val fetchPairsUseCase: OnrampFetchPairsUseCase, - private val amountInputManager: InputManager, - private val getOnrampOffersUseCase: GetOnrampOffersUseCase, - paramsContainer: ParamsContainer, - getWalletsUseCase: GetWalletsUseCase, -) : Model(), OnrampV2Intents { - - val params = paramsContainer.require() - - private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampAmountButtonUMStateFactory() - } - - @Suppress("PropertyUsedBeforeDeclaration") - private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampV2StateFactory( - currentStateProvider = Provider { state.value }, - cryptoCurrency = params.cryptoCurrency, - onrampIntents = this, - onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, - ) - } - - val state: StateFlow - field = MutableStateFlow( - value = stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = ::onCloseClick, - openSettings = ::openSettings, - ), - ) - - private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampV2AmountStateFactory( - currentStateProvider = Provider { state.value }, - analyticsEventHandler = analyticsEventHandler, - onrampIntents = this, - onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, - ) - } - - private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampOffersStateFactory( - currentStateProvider = Provider { state.value }, - onrampIntents = this, - ) - } - - private val quotesTaskScheduler = SingleTaskScheduler() - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - - init { - modelScope.launch { - clearOnrampCacheUseCase() - } - startLoadingQuotes() - sendScreenOpenAnalytics() - checkResidenceCountry() - subscribeToAmountChanges() - subscribeToCountryAndCurrencyUpdates() - subscribeToQuotesUpdate() - subscribeOnOffers() - } - - override fun onDestroy() { - modelScope.launch { clearOnrampCacheUseCase.invoke() } - quotesTaskScheduler.cancelTask() - super.onDestroy() - } - - override fun onAmountValueChanged(value: String) { - state.update { amountStateFactory.getOnAmountValueChange(value) } - modelScope.launch { amountInputManager.update(value) } - } - - override fun openSettings() { - params.openSettings.invoke() - } - - override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) - } - - override fun onBuyClick( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) { - val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return - analyticsEventHandler.send( - OnrampAnalyticsEvent.OnBuyClick( - providerName = quote.provider.info.name, - currency = currentContentState.amountBlockState.currencyUM.code, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - sendOfferClickEvent( - quote = quote, - onrampOfferAdvantagesUM = onrampOfferAdvantagesUM, - categoryUM = categoryUM, - ) - params.openRedirectPage(quote) - } - - override fun openProviders() { - val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return - val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode)) - } - - override fun onRefresh() { - state.update { - stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = router::pop, - openSettings = ::openSettings, - ) - } - modelScope.launch { - clearOnrampCacheUseCase.invoke() - checkResidenceCountry() - handleOnrampAvailable() - } - } - - fun onStop() { - quotesTaskScheduler.cancelTask() - } - - fun handleOnrampAvailable() { - subscribeToCountryAndCurrencyUpdates() - subscribeToQuotesUpdate() - } - - private fun startLoadingQuotes() { - quotesTaskScheduler.cancelTask() - quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask()) - } - - private fun loadQuotesTask(): PeriodicTask { - return PeriodicTask( - delay = UPDATE_DELAY, - task = { - runSuspendCatching { - val amountBlockState = (state.value as? OnrampV2MainComponentUM.Content)?.amountBlockState - ?: return@runSuspendCatching - - val fiatAmount = amountBlockState.amountFieldModel.fiatAmount - if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching - - fetchQuotesUseCase.invoke( - userWallet = userWallet, - amount = amountBlockState.amountFieldModel.fiatAmount, - cryptoCurrency = params.cryptoCurrency, - ).onLeft(::handleOnrampError) - } - }, - onSuccess = {}, - onError = {}, - ) - } - - private fun checkResidenceCountry() { - modelScope.launch { - checkOnrampAvailabilityUseCase(userWallet) - .onRight(::handleOnrampAvailability) - .onLeft(::handleOnrampError) - } - } - - private fun handleOnrampAvailability(availability: OnrampAvailability) { - when (availability) { - is OnrampAvailability.Available -> Unit - is OnrampAvailability.ConfirmResidency, - is OnrampAvailability.NotSupported, - -> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country)) - } - } - - private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) - router.pop() - } - - private fun subscribeOnOffers() = modelScope.launch { - getOnrampOffersUseCase - .invoke() - .collectLatest { maybeOffers -> - maybeOffers.fold( - ifLeft = ::handleOnrampError, - ifRight = { offers -> - val currentState = state.value - if (currentState is OnrampV2MainComponentUM.Content) { - if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) { - state.update { - currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty) - } - return@fold - } - state.update { - onrampOffersStateFactory.getOffersState(offers) - } - } - }, - ) - } - } - - private fun subscribeToAmountChanges() = modelScope.launch { - amountInputManager.query - .filter(String::isNotEmpty) - .collectLatest { _ -> - startLoadingQuotes() - } - } - - private fun subscribeToCountryAndCurrencyUpdates() { - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.fold( - ifLeft = ::handleOnrampError, - ifRight = { country -> - if (country == null) return@onEach - state.update { prevState -> - when (prevState) { - is OnrampV2MainComponentUM.Content -> { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - is OnrampV2MainComponentUM.InitialLoading -> { - stateFactory.getReadyState(country.defaultCurrency) - } - } - } - updatePairsAndQuotes() - }, - ) - } - .launchIn(modelScope) - } - - private fun subscribeToQuotesUpdate() { - getOnrampQuotesUseCase.invoke() - .conflate() - .onEach { maybeQuotes -> - maybeQuotes.fold( - ifLeft = ::handleOnrampError, - ifRight = ::handleQuoteResult, - ) - } - .launchIn(modelScope) - } - - private fun handleQuoteResult(quotes: List) { - sendOnrampQuotesErrorAnalytic(quotes) - when { - quotes.isEmpty() -> { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } - quotes.all { it is OnrampQuote.AmountError } -> { - state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } - } - quotes.none { it is OnrampQuote.Data } -> { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } - else -> { - state.update { prevState -> - val resetState = amountStateFactory.getAmountSecondaryFieldResetState() - if (prevState is OnrampV2MainComponentUM.Content && - resetState is OnrampV2MainComponentUM.Content && - prevState.offersBlockState is OnrampOffersBlockUM.Loading - ) { - resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty) - } else { - resetState - } - } - } - } - } - - private fun onRetryQuotes() { - state.update { prevState -> - (prevState as? OnrampV2MainComponentUM.Content)?.copy( - errorNotification = null, - offersBlockState = OnrampOffersBlockUM.Loading, - amountBlockState = prevState.amountBlockState.copy( - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ), - ) ?: prevState - } - startLoadingQuotes() - } - - private suspend fun updatePairsAndQuotes() { - fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( - ifLeft = ::handleOnrampError, - ifRight = { - state.update { - amountStateFactory.getAmountSecondaryFieldResetState() - } - startLoadingQuotes() - }, - ) - } - - private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) - state.update { stateFactory.getOnrampErrorState(onrampError) } - } - - private fun sendOnrampQuotesErrorAnalytic(quotes: List) { - quotes.forEach { errorState -> - when (errorState) { - is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent( - error = errorState.error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = errorState.provider.info.name, - paymentMethod = errorState.paymentMethod.name, - ) - is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent( - error = errorState.error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = errorState.provider.info.name, - paymentMethod = errorState.paymentMethod.name, - ) - else -> Unit - } - } - } - - private fun sendScreenOpenAnalytics() { - analyticsEventHandler.send( - OnrampAnalyticsEvent.ScreenOpened( - source = params.source, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - } - - private fun sendOfferClickEvent( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) { - val event = when (categoryUM) { - OnrampOfferCategoryUM.RecentlyUsed -> { - OnrampAnalyticsEvent.RecentlyBuyClicked( - tokenSymbol = params.cryptoCurrency.symbol, - providerName = quote.provider.info.name, - paymentMethod = quote.paymentMethod.name, - ) - } - OnrampOfferCategoryUM.Recommended -> { - onrampOfferAdvantagesUM.toAnalyticsEvent( - cryptoCurrencySymbol = params.cryptoCurrency.symbol, - providerName = quote.provider.info.name, - paymentMethodName = quote.paymentMethod.name, - ) - } - } - - if (event != null) { - analyticsEventHandler.send(event) - } - } - - private companion object { - const val UPDATE_DELAY = 10_000L - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt deleted file mode 100644 index 7e9499e37d..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.tangem.features.onramp.mainv2.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -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.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.WindowInsetsZero -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM - -@Composable -internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { - Scaffold( - modifier = modifier.systemBarsPadding(), - topBar = { - TangemTopAppBar( - startButton = state.topBarConfig.startButtonUM, - endButton = state.topBarConfig.endButtonUM, - title = state.topBarConfig.title.resolveReference(), - ) - }, - contentWindowInsets = WindowInsetsZero, - containerColor = TangemTheme.colors.background.secondary, - ) { scaffoldPaddings -> - OnrampNewMainComponentContent( - state = state, - modifier = Modifier.padding(scaffoldPaddings), - ) - } -} - -@Composable -internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxSize() - .background(TangemTheme.colors.background.secondary), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - when (state) { - is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state) - is OnrampV2MainComponentUM.Content -> Content(state = state) - } - } - - if (state is OnrampV2MainComponentUM.Content) { - OnrampFooterContent(state = state) - } - } -} - -@Composable -private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - OnrampAmountContentLoading() - if (state.errorNotification != null) Notification(config = state.errorNotification.config) - } -} - -@Composable -private fun OnrampAmountContentLoading() { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(vertical = TangemTheme.dimens.spacing28), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16) - .size(width = 76.dp, height = 20.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12) - .size(width = 136.dp, height = 44.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing8) - .size(width = 52.dp, height = 16.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing20) - .size(width = 84.dp, height = 28.dp), - radius = TangemTheme.dimens.radius14, - ) - } -} - -@Composable -private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .navigationBarsPadding() - .padding( - bottom = 76.dp, - start = 16.dp, - end = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - OnrampV2AmountContent(state = state) - - OnrampOffersContent(state = state.offersBlockState) - - if (state.errorNotification != null) Notification(config = state.errorNotification.config) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt deleted file mode 100644 index 8cc5c7aca6..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.tangem.features.onramp.mainv2.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.fields.AmountTextField -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.BuyTokenDetailsScreenTestTags -import com.tangem.core.ui.utils.rememberDecimalFormat -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM -import com.tangem.features.onramp.mainv2.entity.OnrampSecondaryFieldErrorUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM - -@Composable -internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), - ) - .padding(vertical = 24.dp, horizontal = 16.dp) - .animateContentSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnrampHeaderTitle() - - OnrampAmountField( - amountField = state.amountBlockState.amountFieldModel, - currencyCode = state.amountBlockState.currencyUM.code, - ) - - AnimatedVisibility( - visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty, - ) { - if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) { - OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) - } - } - - SpacerH(20.dp) - - OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) - } -} - -@Composable -private fun OnrampHeaderTitle() { - Text( - text = stringResourceSafe(R.string.onramp_you_will_pay_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { - val decimalFormat = rememberDecimalFormat() - val requester = remember { FocusRequester() } - AmountTextField( - value = amountField.fiatValue, - decimals = amountField.fiatAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = amountField.fiatAmount.decimals, - symbol = currencyCode, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - symbolColor = if (amountField.fiatValue.isBlank()) { - TangemTheme.colors.text.disabled - } else { - TangemTheme.colors.text.primary1 - }, - ), - onValueChange = amountField.onValueChange, - keyboardOptions = amountField.keyboardOptions, - keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.head.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isEnabled = !amountField.isError, - isAutoResize = true, - isValuePasted = amountField.isValuePasted, - onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing4, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32) - .testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD), - ) - - LaunchedEffect(key1 = Unit) { - requester.requestFocus() - } -} - -@Composable -private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - contentAlignment = Alignment.Center, - ) { - Text( - text = state.error.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(RoundedCornerShape(14.dp)) - .background(TangemTheme.colors.button.secondary) - .clickable(onClick = currencyUM.onClick) - .padding(horizontal = 6.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - AsyncImage( - modifier = Modifier - .size(20.dp) - .clip(CircleShape) - .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), - model = currencyUM.iconUrl, - contentDescription = null, - ) - Text( - text = currencyUM.code, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), - textAlign = TextAlign.Center, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON), - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 1133a72cfc..daa379e850 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -17,8 +17,6 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.main.OnrampMainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.settings.OnrampSettingsComponent @@ -32,9 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( @Assisted private val params: OnrampComponent.Params, private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory, - private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory, private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, - private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle, ) : OnrampComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -71,44 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor( onBack = navigation::pop, ), ) - OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) { - onrampMainV2ComponentFactory.create( - context = childByContext(componentContext), - params = OnrampV2MainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { quote -> - navigation.push( - OnrampChild.RedirectPage( - quote = quote, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - ), - ) - } else { - onrampMainComponentFactory.create( - context = childByContext(componentContext), - params = OnrampMainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { onrampProviderWithQuoteData -> - navigation.push( - OnrampChild.RedirectPage( - quote = onrampProviderWithQuoteData, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - isLaunchSepa = params.shouldLaunchSepa, - ), - ) - } + OnrampChild.Main -> onrampMainComponentFactory.create( + context = childByContext(componentContext), + params = OnrampMainComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + openSettings = { navigation.push(OnrampChild.Settings) }, + source = params.source, + openRedirectPage = { quote -> + navigation.push( + OnrampChild.RedirectPage( + quote = quote, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + ), + ) is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( context = childByContext(componentContext), params = OnrampRedirectComponent.Params( 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 1c618de087..a9ac694ab9 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 @@ -3,25 +3,25 @@ package com.tangem.features.onramp.selecttoken.model import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM 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.analytics.models.event.OfframpAnalyticsEvent 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.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R @@ -43,7 +43,8 @@ internal class OnrampOperationModel @Inject constructor( private val router: AppRouter, private val analyticsEventHandler: AnalyticsEventHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val rampStateManager: RampStateManager, @@ -119,9 +120,13 @@ internal class OnrampOperationModel @Inject constructor( val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync() .getOrElse { AppCurrency.Default }.code - reduxStateHolder.dispatch( - action = TradeCryptoAction.Sell(status, appCurrencyCode), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = status, + appCurrencyCode = appCurrencyCode, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } } @@ -153,18 +158,9 @@ internal class OnrampOperationModel @Inject constructor( private fun showErrorIfDemoModeOrElse(action: () -> Unit) { if (selectedUserWallet is UserWallet.Cold && isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { - val alertUM = AlertDemoModeUM(onConfirmClick = {}) - val message = DialogMessage( - title = alertUM.title, - message = alertUM.message, - firstActionBuilder = { - EventMessageAction( - title = alertUM.confirmButtonText, - onClick = alertUM.onConfirmClick, - ) - }, - secondActionBuilder = { cancelAction() }, + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), ) messageSender.send(message) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 597de0c5d3..855c07c8e2 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -211,6 +211,7 @@ sealed class CommonSendAnalyticEvents( const val SEND_CATEGORY = "Token / Send" const val SWAP_CATEGORY = "Swap" const val NFT_SEND_CATEGORY = "NFT" + const val APPROVE_CATEGORY = "Approve" } enum class SendScreenSource { @@ -226,5 +227,6 @@ sealed class CommonSendAnalyticEvents( SendWithSwap("Send&Swap"), WalletConnect("WalletConnect"), NFT("NFT"), + Approve("Approve"), } } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt index 78b805b69a..9a9ee70d57 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt @@ -1,7 +1,6 @@ package com.tangem.features.send.v2.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -14,6 +13,7 @@ import javax.inject.Inject @ModelScoped internal class SendConfirmAlertFactory @Inject constructor( private val messageSender: UiMessageSender, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -31,34 +31,16 @@ internal class SendConfirmAlertFactory @Inject constructor( } fun getSendTransactionErrorState( - error: SendTransactionError?, + error: SendTransactionError, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - messageSender.send( - DialogMessage( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + messageSender.send(errorDialog) } } \ No newline at end of file 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 3ace74e0f1..2ef0911b14 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 @@ -67,7 +67,6 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingAlertUM -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeLoader @@ -249,7 +248,7 @@ internal class StakingModel @Inject constructor( private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( - stateController = stateController, + messageSender = messageSender, popBackStack = ::onBackClick, onFailedTxEmailClick = ::onFailedTxEmailClick, ) @@ -503,11 +502,7 @@ internal class StakingModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) - stateController.updateEvent( - StakingEvent.ShowAlert( - StakingAlertUM.FeeIncreased(stateController::dismissAlert), - ), - ) + messageSender.send(StakingAlertUM.feeIncreased {}) updateNotifications() }, onTransactionExpired = { @@ -571,9 +566,7 @@ internal class StakingModel @Inject constructor( override fun onAmountEnterClick() { if (integration.preferredTargets.isEmpty()) { - stateController.updateEvent( - StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators), - ) + messageSender.send(StakingAlertUM.noAvailableValidators()) } else { if (uiState.value.actionType is StakingActionCommonType.Enter) { stateController.updateAll( @@ -1047,11 +1040,7 @@ internal class StakingModel @Inject constructor( } override fun showPrimaryClickAlert() { - stateController.updateEvent( - StakingEvent.ShowAlert( - StakingAlertUM.StakeMoreClickUnavailable(cryptoCurrencyStatus.currency), - ), - ) + messageSender.send(StakingAlertUM.stakeMoreClickUnavailable(cryptoCurrencyStatus.currency)) } override fun onOpenLearnMoreAboutApproveClick() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 00409eed5c..7a02bd36ef 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -3,12 +3,9 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer @@ -72,16 +69,6 @@ internal class StakingStateController @Inject constructor( mutableUiState.update(function = titleTransformer::transform) } - fun updateEvent(event: StakingEvent?) { - mutableUiState.update { - it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent()) - } - } - - fun dismissAlert() { - mutableUiState.update { it.copy(event = consumedEvent()) } - } - private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, @@ -98,7 +85,6 @@ internal class StakingStateController @Inject constructor( rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), isBalanceHidden = false, - event = consumedEvent(), bottomSheetConfig = null, actionType = StakingActionCommonType.Enter(skipEnterAmount = false), buttonsState = NavigationButtonsState.Empty, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 699729948f..8624ce9a72 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -6,14 +6,12 @@ import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData -import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.StakingTarget import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -40,7 +38,6 @@ internal data class StakingUiState( val bottomSheetConfig: TangemBottomSheetConfig?, val actionType: StakingActionCommonType, val buttonsState: NavigationButtonsState, - val event: StateEvent, val balanceState: BalanceState?, val showColdWalletInteractionIcon: Boolean, val shouldShowHoldToConfirmButton: Boolean, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index 3e757a6adc..acba4a78d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -1,85 +1,74 @@ package com.tangem.features.staking.impl.presentation.state.events -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM -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.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.staking.impl.R -@Immutable -internal sealed class StakingAlertUM : AlertUM { +internal object StakingAlertUM { - data class GenericError( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.common_error) - override val message: TextReference = resourceReference(R.string.common_unknown_error) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) - } + fun genericError(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.common_unknown_error), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class StakingError( - val code: String, - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.common_error) - override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code)) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) - } + fun stakingError(code: String, onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.generic_error_code, wrappedList(code)), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data object NoAvailableValidators : StakingAlertUM() { - override val title = resourceReference(R.string.common_error) - override val message = resourceReference(R.string.staking_no_validators_error_message) - override val confirmButtonText = resourceReference(R.string.common_ok) - override val onConfirmClick = null - } + fun noAvailableValidators(): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.staking_no_validators_error_message), + ) - data class FeeIncreased( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference? = null - override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun feeIncreased(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = null, + message = resourceReference(id = R.string.send_notification_high_fee_title), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) - data object ValidatorsUnavailable : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title) - override val message: TextReference = resourceReference(id = R.string.staking_error_no_validators_message) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun validatorsUnavailable(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.staking_error_no_validators_title), + message = resourceReference(id = R.string.staking_error_no_validators_message), + ) - data class StakeMoreClickUnavailable( - val cryptoCurrency: CryptoCurrency, - ) : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference? = null - override val message: TextReference = resourceReference( + fun stakeMoreClickUnavailable(cryptoCurrency: CryptoCurrency): DialogMessage = DialogMessage( + title = null, + message = resourceReference( id = R.string.staking_stake_more_button_unavailability_reason, wrappedList(cryptoCurrency.name, cryptoCurrency.symbol), - ) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + ), + ) - data class RewardsMinimumRequirementsError( - val cryptoCurrencyName: String, - val cryptoAmountValue: String, - ) : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference? = null - override val message: TextReference = resourceReference( - id = R.string.staking_details_min_rewards_notification, - formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + fun rewardsMinimumRequirementsError(cryptoCurrencyName: String, cryptoAmountValue: String): DialogMessage = + DialogMessage( + title = null, + message = resourceReference( + id = R.string.staking_details_min_rewards_notification, + formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + ), ) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } - data class NetworkFeeUpdated( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.staking_alert_network_fee_updated_title) - override val message: TextReference = resourceReference(R.string.staking_alert_network_fee_updated_message) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun networkFeeUpdated(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.staking_alert_network_fee_updated_title), + message = resourceReference(R.string.staking_alert_network_fee_updated_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt deleted file mode 100644 index 7e7700595e..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.events - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.extensions.TextReference - -@Immutable -internal sealed class StakingEvent { - - data class ShowSnackBar(val text: TextReference) : StakingEvent() - - data class ShowAlert(val alert: AlertUM) : StakingEvent() -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 4ba7fb1dd9..fd94adadd2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -1,69 +1,63 @@ package com.tangem.features.staking.impl.presentation.state.events -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.features.staking.impl.presentation.state.StakingStateController internal class StakingEventFactory( - private val stateController: StakingStateController, + private val messageSender: UiMessageSender, private val popBackStack: () -> Unit, private val onFailedTxEmailClick: (String) -> Unit, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory = TransactionErrorDialogFactory(), ) { fun createGenericErrorAlert(error: String) { - val alert = StakingEvent.ShowAlert( - StakingAlertUM.GenericError( + messageSender.send( + StakingAlertUM.genericError( onConfirmClick = { onFailedTxEmailClick(error) }, ), ) - stateController.updateEvent(alert) } fun createSendTransactionErrorAlert(error: SendTransactionError?) { val alert = error?.let { - TransactionErrorAlertConverter( + transactionErrorDialogFactory.create( + error = error, popBackStack = popBackStack, onFailedTxEmailClick = onFailedTxEmailClick, - ).convert(error) - }?.let { - StakingEvent.ShowAlert(it) + ) } - stateController.updateEvent(alert) + alert?.let { messageSender.send(it) } } fun createStakingErrorAlert(error: StakingError) { - val alert = StakingEvent.ShowAlert( - StakingAlertUM.StakingError( + messageSender.send( + StakingAlertUM.stakingError( code = error.toString(), onConfirmClick = { onFailedTxEmailClick(error.toString()) }, ), ) - stateController.updateEvent(alert) } fun createStakingValidatorsUnavailableAlert() { - val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable) - stateController.updateEvent(alert) + messageSender.send(StakingAlertUM.validatorsUnavailable()) } fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) { - stateController.updateEvent( - StakingEvent.ShowAlert( - alert = StakingAlertUM.RewardsMinimumRequirementsError( - cryptoCurrencyName = cryptoCurrencyName, - cryptoAmountValue = cryptoAmountValue, - ), + messageSender.send( + StakingAlertUM.rewardsMinimumRequirementsError( + cryptoCurrencyName = cryptoCurrencyName, + cryptoAmountValue = cryptoAmountValue, ), ) } fun createNetworkFeeUpdatedAlert(onConfirm: () -> Unit) { - val alert = StakingEvent.ShowAlert( - alert = StakingAlertUM.NetworkFeeUpdated( + messageSender.send( + StakingAlertUM.networkFeeUpdated( onConfirmClick = onConfirm, ), ) - stateController.updateEvent(alert) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt deleted file mode 100644 index 96234a843e..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.features.staking.impl.presentation.ui - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent - -@Composable -internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { - val resources = LocalContext.current.resources - var alertConfig by remember { mutableStateOf(value = null) } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = alertConfig) { - keyboardController?.hide() - } - - alertConfig?.let { - StakingAlert(state = it, onDismiss = { alertConfig = null }) - } - - EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is StakingEvent.ShowSnackBar -> { - snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) - } - is StakingEvent.ShowAlert -> { - alertConfig = value.alert - } - } - }, - ) -} - -@Composable -internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton: DialogButtonUM - val dismissButton: DialogButtonUM? - - val onActionClick = state.onConfirmClick - if (onActionClick != null) { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - onActionClick() - onDismiss() - }, - ) - - dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - } else { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = onDismiss, - ) - dismissButton = null - } - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 0196216ea0..836d37dec7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { - val snackbarHostState = remember { SnackbarHostState() } val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data BackHandler(onBack = uiState.clickIntents::onPrevClick) @@ -71,11 +69,6 @@ internal fun StakingScreen(uiState: StakingUiState) { ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } - - StakingEventEffect( - event = uiState.event, - snackbarHostState = snackbarHostState, - ) } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 0994844d9b..79c2503262 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -1,7 +1,6 @@ package com.tangem.features.swap.v2.impl.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -25,6 +24,7 @@ internal class SwapAlertFactory @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(expressError: ExpressError, onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { uiMessageSender.send( @@ -44,36 +44,21 @@ internal class SwapAlertFactory @Inject constructor( ) } + @Suppress("CanBeNonNullable") fun getSendTransactionErrorState( error: SendTransactionError?, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + if (error == null) return + + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - uiMessageSender.send( - DialogMessage.Companion( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + uiMessageSender.send(errorDialog) } suspend fun onFailedTxEmailClick( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt index 646e3a3218..653bb2b5e3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ui.SwapTransactionState import com.tangem.feature.swap.models.SwapAlertUM @@ -11,24 +11,25 @@ import com.tangem.utils.converter.Converter internal class SwapTransactionErrorStateConverter( private val onDismiss: () -> Unit, private val onSupportClick: (String) -> Unit, -) : Converter { - override fun convert(value: SwapTransactionState.Error): AlertUM? { + private val transactionErrorDialogFactory: TransactionErrorDialogFactory = TransactionErrorDialogFactory(), +) : Converter { + override fun convert(value: SwapTransactionState.Error): DialogMessage? { return when (value) { is SwapTransactionState.Error.TransactionError -> { when (val error = value.error) { is SendTransactionError.UserCancelledError -> return null - null -> SwapAlertUM.GenericError(onDismiss) - else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error) + null -> SwapAlertUM.genericError(onDismiss) + else -> transactionErrorDialogFactory.create(error, onDismiss, onSupportClick) } } is SwapTransactionState.Error.ExpressError -> { - SwapAlertUM.ExpressErrorAlert( + SwapAlertUM.expressErrorAlert( message = getExpressErrorMessage(value.error), onConfirmClick = { onSupportClick(value.error.code.toString()) }, ) } - SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss) - is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.GenericError( + SwapTransactionState.Error.UnknownError -> SwapAlertUM.genericError(onDismiss) + is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.genericError( onConfirmClick = { onSupportClick(value.txId) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 38be62641a..1f5c403663 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -24,12 +24,22 @@ import com.tangem.core.analytics.models.event.SwapAnalyticsEvent 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.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter +import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus @@ -162,6 +172,7 @@ internal class SwapModel @Inject constructor( private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, + private val messageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -342,7 +353,8 @@ internal class SwapModel @Inject constructor( } if (fromAccountStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + showAlert() + swapRouter.back() } else { fromAccountCurrencyStatus = fromAccountStatus toAccountCurrencyStatus = toAccountStatus @@ -360,7 +372,8 @@ internal class SwapModel @Inject constructor( } if (fromStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + showAlert() + swapRouter.back() } else { initialFromStatus = fromStatus initialToStatus = toStatus @@ -1027,7 +1040,7 @@ internal class SwapModel @Inject constructor( val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { delay(SWAP_IN_PROGRESS_DELAY) startLoadingQuotesFromLastState() @@ -1053,7 +1066,7 @@ internal class SwapModel @Inject constructor( when (swapTransactionState) { is SwapTransactionState.TxSent -> { if (fee == null) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } sendSuccessSwapEvent( @@ -1097,21 +1110,11 @@ internal class SwapModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) } SwapTransactionState.DemoMode -> { - uiState = stateBuilder.createDemoModeAlert( - uiState = uiState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showDemoModeAlert() } is SwapTransactionState.Error -> { startLoadingQuotesFromLastState() - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, - error = swapTransactionState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onFailedTxEmailClick, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showTransactionErrorAlert(swapTransactionState) } is SwapTransactionState.TangemPayWithdrawalData -> { processTangemPayWithdrawal(swapTransactionState = swapTransactionState) @@ -1120,7 +1123,7 @@ internal class SwapModel @Inject constructor( }.onFailure { error -> Timber.e(error) startLoadingQuotesFromLastState() - makeDefaultAlert() + showAlert() } } } @@ -1212,7 +1215,7 @@ internal class SwapModel @Inject constructor( } val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") return@launch } @@ -1242,29 +1245,19 @@ internal class SwapModel @Inject constructor( startLoadingQuotesFromLastState(isSilent = true) } is SwapTransactionState.Error -> { - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, - error = swapTransactionState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onFailedTxEmailClick, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showTransactionErrorAlert(swapTransactionState) } SwapTransactionState.DemoMode -> { - uiState = stateBuilder.createDemoModeAlert( - uiState = uiState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showDemoModeAlert() } is SwapTransactionState.TangemPayWithdrawalData -> { processTangemPayWithdrawal(swapTransactionState = swapTransactionState) } } - }.onFailure { makeDefaultAlert() } + }.onFailure { showAlert() } }.onFailure { error -> Timber.e(error.message.orEmpty()) - makeDefaultAlert() + showAlert() } } } @@ -1654,12 +1647,94 @@ internal class SwapModel @Inject constructor( return inputNumberFormatter.getValidatedNumberWithFixedDecimals(amount, maxDecimals) } - private fun makeDefaultAlert() { - uiState = stateBuilder.addAlert(uiState) + private fun showAlert(message: TextReference = resourceReference(R.string.common_unknown_error)) { + messageSender.send(SwapAlertUM.genericError(onConfirmClick = { }, message = message)) } - private fun makeDefaultAlert(message: TextReference) { - uiState = stateBuilder.addAlert(uiState, message) + private fun showDemoModeAlert() { + messageSender.send( + DialogMessage( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = {}, + ), + ), + ) + } + + private fun showTransactionErrorAlert( + error: SwapTransactionState.Error, + onSupportClick: (String) -> Unit = ::onFailedTxEmailClick, + ) { + val errorAlert = SwapTransactionErrorStateConverter( + onDismiss = {}, + onSupportClick = onSupportClick, + ).convert(error) + errorAlert?.let { messageSender.send(it) } + } + + private fun onTangemPaySupportClick(txId: String) { + modelScope.launch { + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull().orEmpty() + val email = FeedbackEmailType.Visa.Withdrawal( + walletMetaInfo = metaInfo, + customerId = customerId, + providerName = dataState.selectedProvider?.name.orEmpty(), + txId = txId, + ) + sendFeedbackEmailUseCase(email) + } + } + + private fun showSwapInfoAlert(isPriceImpact: Boolean, token: String, provider: SwapProvider) { + messageSender.send( + SwapAlertUM.informationAlert( + message = buildSwapInfoMessage(isPriceImpact, token, provider), + onConfirmClick = {}, + ), + ) + } + + private fun buildSwapInfoMessage(isPriceImpact: Boolean, token: String, provider: SwapProvider): TextReference { + val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}%" } + val messages = buildList { + when (provider.type) { + ExchangeProviderType.CEX -> { + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_cex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) + } + } + ExchangeProviderType.DEX, + ExchangeProviderType.DEX_BRIDGE, + -> { + if (isPriceImpact) { + add(resourceReference(R.string.swapping_high_price_impact_description)) + add(stringReference("\n\n")) + } + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_dex_description_with_slippage, + formatArgs = wrappedList(slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) + } + } + } + } + return combinedReference(messages.toWrappedList()) } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1779,14 +1854,7 @@ internal class SwapModel @Inject constructor( val selectedProvider = dataState.selectedProvider ?: return@UiActions val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions val isPriceImpact = uiState.priceImpact is PriceImpact.Value - uiState = stateBuilder.createAlert( - uiState = uiState, - isPriceImpact = isPriceImpact, - token = currencySymbol, - provider = selectedProvider, - isReverseSwapPossible = isReverseSwapPossible(), - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - ) + showSwapInfoAlert(isPriceImpact, currencySymbol, selectedProvider) }, onLinkClick = urlOpener::openUrl, onSelectTokenClick = { @@ -2092,31 +2160,12 @@ internal class SwapModel @Inject constructor( } private fun onTangemPayWithdrawalError(txId: String?) { - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, + showTransactionErrorAlert( error = SwapTransactionState.Error.TangemPayWithdrawalError(txId.orEmpty()), - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = { - val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull() ?: "Unknown" - onTangemPaySupportClick(customerId = customerId, txId = txId) - }, - isReverseSwapPossible = isReverseSwapPossible(), + onSupportClick = ::onTangemPaySupportClick, ) } - private fun onTangemPaySupportClick(customerId: String, txId: String?) { - modelScope.launch { - val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch - val email = FeedbackEmailType.Visa.Withdrawal( - walletMetaInfo = metaInfo, - customerId = customerId, - providerName = dataState.selectedProvider?.name.orEmpty(), - txId = txId.orEmpty(), - ) - sendFeedbackEmailUseCase(email) - } - } - private fun onFailedTxEmailClick(errorMessage: String) { modelScope.launch { val transaction = dataState.swapDataModel?.transaction diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt index bb84996732..fcf6277b98 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt @@ -1,38 +1,43 @@ package com.tangem.feature.swap.models -import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction -sealed class SwapAlertUM : AlertUM { +internal object SwapAlertUM { - data class GenericError( - override val onConfirmClick: (() -> Unit), - override val message: TextReference = resourceReference(R.string.common_unknown_error), - ) : SwapAlertUM() { - override val title: TextReference? = null - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } + fun genericError( + onConfirmClick: () -> Unit, + message: TextReference = resourceReference(R.string.common_unknown_error), + ): DialogMessage = DialogMessage( + title = null, + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class ExpressErrorAlert( - override val message: TextReference = resourceReference(R.string.common_unknown_error), - override val onConfirmClick: (() -> Unit), - ) : SwapAlertUM() { - override val title: TextReference? = null - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } + fun expressErrorAlert( + message: TextReference = resourceReference(R.string.common_unknown_error), + onConfirmClick: () -> Unit, + ): DialogMessage = DialogMessage( + title = null, + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class InformationAlert( - override val message: TextReference, - override val onConfirmClick: (() -> Unit), - ) : SwapAlertUM() { - override val title: TextReference = resourceReference( - R.string.swapping_alert_title, - ) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_ok) - } + fun informationAlert(message: TextReference, onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.swapping_alert_title), + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 47517e2801..02b46b544c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -7,14 +7,11 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState -import com.tangem.feature.swap.models.states.events.SwapEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,7 +21,6 @@ internal data class SwapStateHolder( val blockchainId: String, // not the same as networkId, its local id in app val notifications: ImmutableList = persistentListOf(), val isInsufficientFunds: Boolean, - val event: StateEvent = consumedEvent(), val changeCardsButtonState: ChangeCardsButtonState, val providerState: ProviderState, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt deleted file mode 100644 index a00fed2610..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.feature.swap.models.states.events - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM - -@Immutable -internal sealed class SwapEvent { - data class ShowAlert(val alert: AlertUM) : SwapEvent() -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index adeed21292..a30871f920 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -5,21 +5,17 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -29,7 +25,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.converters.TokensDataConverterV2 import com.tangem.feature.swap.domain.models.ExpressDataError @@ -43,12 +38,10 @@ import com.tangem.feature.swap.model.SwapNotificationsFactory import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* -import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.PERCENT import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -972,117 +965,6 @@ internal class StateBuilder( ) } - fun createErrorTransactionAlert( - uiState: SwapStateHolder, - error: SwapTransactionState.Error, - onDismiss: () -> Unit, - onSupportClick: (String) -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - val errorAlert = SwapTransactionErrorStateConverter( - onSupportClick = onSupportClick, - onDismiss = onDismiss, - ).convert(error) - return uiState.copy( - event = errorAlert?.let { - triggeredEvent( - data = SwapEvent.ShowAlert(errorAlert), - onConsume = onDismiss, - ) - } ?: consumedEvent(), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - fun createDemoModeAlert( - uiState: SwapStateHolder, - onDismiss: () -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - return uiState.copy( - event = triggeredEvent( - data = SwapEvent.ShowAlert(AlertDemoModeUM(onDismiss)), - onConsume = onDismiss, - ), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - @Suppress("LongParameterList") - fun createAlert( - uiState: SwapStateHolder, - isPriceImpact: Boolean, - token: String, - provider: SwapProvider, - onDismiss: () -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" } - val combinedMessage = buildList { - when (provider.type) { - ExchangeProviderType.CEX -> { - if (slippage != null) { - add( - resourceReference( - id = R.string.swapping_alert_cex_description_with_slippage, - formatArgs = wrappedList(token, slippage), - ), - ) - } else { - add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) - } - } - ExchangeProviderType.DEX, - ExchangeProviderType.DEX_BRIDGE, - -> { - if (isPriceImpact) { - add(resourceReference(R.string.swapping_high_price_impact_description)) - add(stringReference("\n\n")) - } - if (slippage != null) { - add( - resourceReference( - id = R.string.swapping_alert_dex_description_with_slippage, - formatArgs = wrappedList(slippage), - ), - ) - } else { - add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) - } - } - } - } - return uiState.copy( - event = triggeredEvent( - SwapEvent.ShowAlert( - SwapAlertUM.InformationAlert( - message = combinedReference(combinedMessage.toWrappedList()), - onConfirmClick = onDismiss, - ), - ), - onConsume = onDismiss, - ), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - fun addAlert( - uiState: SwapStateHolder, - message: TextReference = resourceReference(R.string.common_unknown_error), - onDismiss: () -> Unit = { clearAlert(uiState) }, - ): SwapStateHolder { - return uiState.copy( - event = triggeredEvent( - SwapEvent.ShowAlert( - SwapAlertUM.GenericError(onDismiss, message), - ), - onConsume = onDismiss, - ), - ) - } - - fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent()) - fun addNotification(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { return uiState.copy( notifications = notificationsFactory.getGeneralErrorStateNotifications( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt deleted file mode 100644 index 3a7455b5b0..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.feature.swap.ui - -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.feature.swap.models.states.events.SwapEvent -import com.tangem.feature.swap.presentation.R - -@Composable -internal fun SwapEventEffect(event: StateEvent) { - var alertConfig by remember { mutableStateOf(value = null) } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = alertConfig) { - keyboardController?.hide() - } - - alertConfig?.let { - SwapAlert(state = it, onDismiss = { alertConfig = null }) - } - - EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is SwapEvent.ShowAlert -> { - alertConfig = value.alert - } - } - }, - ) -} - -@Composable -internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - state.onConfirmClick?.invoke() - onDismiss() - }, - ) - val dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index b4d3c15097..813e71cc11 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -117,10 +117,6 @@ internal fun SwapScreenContent( textAlign = TextAlign.Start, ) } - - SwapEventEffect( - event = state.event, - ) } } diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt index 393e589bce..0f2ae969eb 100644 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -1,5 +1,5 @@ package com.tangem.features.tangempay interface TangemPayFeatureToggles { - val isTangemPayEnabled: Boolean + val isTangemPayAccountsRefactorEnabled: Boolean } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt index a51c11a3bc..30897aff29 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -5,6 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class DefaultTangemPayFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TangemPayFeatureToggles { - override val isTangemPayEnabled - get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") + override val isTangemPayAccountsRefactorEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED") } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 2b5736147e..58f5d9ef84 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -45,7 +45,6 @@ internal class TangemPayDetailsComponent( appComponentContext = child("txHistoryComponent"), params = DefaultTangemPayTxHistoryComponent.Params( userWalletId = params.userWalletId, - customerWalletAddress = params.config.customerWalletAddress, uiActions = model, ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt index bfd618dfa5..d27e63ad51 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt @@ -25,7 +25,6 @@ internal class DefaultTangemPayTxHistoryComponent( data class Params( val userWalletId: UserWalletId, - val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions, ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt index e1433f4d17..47b229ce5d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -31,7 +31,6 @@ internal class TangemPayTxHistoryModel @Inject constructor( private val listManager = TangemPayTxHistoryListManager( repository = tangemPayTxHistoryRepository, dispatchers = dispatchers, - customerWalletAddress = params.customerWalletAddress, txHistoryUiActions = params.uiActions, ) @@ -104,7 +103,7 @@ internal class TangemPayTxHistoryModel @Inject constructor( } private fun loadMoreItems(): Boolean { - modelScope.launch { listManager.loadMore(params.customerWalletAddress) } + modelScope.launch { listManager.loadMore() } return true } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt index f1b971367c..fa798e16bc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt @@ -22,7 +22,6 @@ private typealias TangemPayTxHistoryBatchAction = BatchAction val route = when (buttonUM) { @@ -97,6 +100,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO + ButtonUM.STORY_BOOK -> TesterScreen.STORY_BOOK } innerTesterRouter.open(route) @@ -179,6 +183,15 @@ internal class TesterActivity : ComposeActivity() { AddressesInfoScreen(state) } + + composable(route = TesterScreen.STORY_BOOK.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + StoryBookScreen(state) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index 2d2f610f44..e3318e0cab 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -26,5 +26,6 @@ data class TesterMenuUM( TEST_PUSHES(R.string.test_push), ACCOUNTS(R.string.accounts), ADDRESSES_INFO(R.string.addresses_info), + STORY_BOOK(R.string.story_book), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index ccf4b0acd0..ca2ae6a0e9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -15,4 +15,5 @@ internal enum class TesterScreen { TEST_PUSHES, ACCOUNTS, ADDRESSES_INFO, + STORY_BOOK, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt new file mode 100644 index 0000000000..1c730da1d5 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal sealed interface StoryBookPage + +internal data object StoryList : StoryBookPage + +internal data class NorthernLightsStory( + val variant: Variant, + val onVariantChange: (Variant) -> Unit, +) : StoryBookPage { + enum class Variant { + Shader, + Simple, + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt new file mode 100644 index 0000000000..39d646a85a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal data class StoryBookUM( + val currentPage: StoryBookPage = StoryList, + val onBackClick: () -> Unit, + val onStoryClick: (StoryPageFactory) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt new file mode 100644 index 0000000000..acea04d55b --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal fun interface StoryPageFactory { + fun create(updatePage: ((StoryBookPage) -> StoryBookPage) -> Unit): StoryBookPage +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt new file mode 100644 index 0000000000..522c3d4ae1 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.tester.presentation.storybook.page.background + +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): NorthernLightsStory { + return NorthernLightsStory( + variant = NorthernLightsStory.Variant.Shader, + onVariantChange = { newVariant -> + updateStory { currentState -> + currentState.copy(variant = newVariant) + } + }, + ) +} + +internal val northernLightsStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt new file mode 100644 index 0000000000..a5764e2be5 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt @@ -0,0 +1,89 @@ +@file:Suppress("MagicNumber") +package com.tangem.feature.tester.presentation.storybook.page.background + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory + +@Composable +internal fun NorthernLightsStory(state: NorthernLightsStory, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize()) { + NorthernLightsBackground( + modifier = Modifier.fillMaxSize(), + forceSimpleVersion = state.variant == NorthernLightsStory.Variant.Simple, + ) + + NorthernLightsVariantToggle( + selected = state.variant, + onSelect = state.onVariantChange, + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding(bottom = 24.dp) + .padding(horizontal = 24.dp), + ) + } +} + +@Composable +private fun NorthernLightsVariantToggle( + selected: NorthernLightsStory.Variant, + onSelect: (NorthernLightsStory.Variant) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(Color.Black.copy(alpha = 0.35f)) + .border(width = 1.dp, color = Color.White.copy(alpha = 0.15f), shape = shape) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + NorthernLightsStory.Variant.entries.forEach { variant -> + VariantChip( + label = variant.label, + selected = variant == selected, + onClick = { onSelect(variant) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun VariantChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background(if (selected) Color.White.copy(alpha = 0.2f) else Color.Transparent) + .clickable(onClick = onClick) + .padding(vertical = 10.dp, horizontal = 16.dp), + ) { + Text( + text = label, + color = Color.White, + fontSize = 14.sp, + ) + } +} + +private val NorthernLightsStory.Variant.label: String + get() = when (this) { + NorthernLightsStory.Variant.Shader -> "Shader" + NorthernLightsStory.Variant.Simple -> "Simple" + } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt new file mode 100644 index 0000000000..ac74e45a74 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.tester.presentation.storybook.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory + +private data class StoryItem(val title: String, val factory: StoryPageFactory) + +private fun buildStories() = listOf( + StoryItem(title = "Northern Lights Background", factory = northernLightsStoryFactory), +) + +@Composable +internal fun StoryBookListScreen(state: StoryBookUM, modifier: Modifier = Modifier) { + val stories = remember { buildStories() } + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + stickyHeader { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = "Storybook", + containerColor = TangemTheme.colors.background.primary, + ) + } + + items(items = stories, key = { it.title }) { item -> + PrimaryButton( + text = item.title, + onClick = { state.onStoryClick(item.factory) }, + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt new file mode 100644 index 0000000000..73e643e0ac --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.tester.presentation.storybook.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory + +@Composable +internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + AnimatedContent( + targetState = state.currentPage, + modifier = modifier, + ) { storyState -> + when (storyState) { + StoryList -> StoryBookListScreen(state = state) + is NorthernLightsStory -> NorthernLightsStory(state = storyState) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt new file mode 100644 index 0000000000..851570c925 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tester.presentation.storybook.viewmodel + +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookPage +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal interface StateUpdater { + fun updateStory(update: (T) -> T) +} + +internal inline fun storyPageFactory( + crossinline build: StateUpdater.() -> T, +): StoryPageFactory = StoryPageFactory { updatePage -> + val updater = object : StateUpdater { + override fun updateStory(update: (T) -> T) { + updatePage { current -> + if (current is T) update(current) else current + } + } + } + updater.build() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt new file mode 100644 index 0000000000..e08ab8c0d7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tester.presentation.storybook.viewmodel + +import androidx.lifecycle.ViewModel +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +internal class StoryBookViewModel @Inject constructor() : ViewModel() { + + private var router: InnerTesterRouter? = null + + private val _uiState = MutableStateFlow( + StoryBookUM( + onBackClick = ::onBackClick, + onStoryClick = ::onStoryClick, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun setupNavigation(router: InnerTesterRouter) { + this.router = router + } + + private fun onBackClick() { + if (_uiState.value.currentPage !is StoryList) { + _uiState.update { it.copy(currentPage = StoryList) } + } else { + router?.back() + } + } + + private fun onStoryClick(factory: StoryPageFactory) { + _uiState.update { state -> + state.copy( + currentPage = factory.create { update -> + _uiState.update { s -> s.copy(currentPage = update(s.currentPage)) } + }, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 9aadd20368..19d8c518ee 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -22,4 +22,5 @@ News details News details (Bottom Sheet) Addresses info + Story book diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 10cb869ad1..3b4adc992f 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -69,10 +69,10 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(projects.domain.legacy) implementation(projects.domain.markets.models) implementation(projects.domain.models) implementation(projects.domain.notifications.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) implementation(projects.domain.promo) @@ -108,6 +108,7 @@ dependencies { implementation(projects.features.sendV2.api) implementation(projects.features.tokenRecieve.api) implementation(projects.features.yieldSupply.api) + implementation(projects.features.tangempay.details.api) implementation(deps.decompose.ext.compose) 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 345620dd50..4992503c8a 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 @@ -23,6 +23,7 @@ import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger import dagger.assisted.Assisted @@ -48,6 +49,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, private val accountsFeatureToggles: AccountsFeatureToggles, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) : TokenDetailsDeepLinkHandler { @@ -128,7 +130,10 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( id = cryptoCurrency.id, ) !isMultiCurrency -> walletBalanceFetcher( - params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId), + params = WalletBalanceFetcher.Params( + userWalletId = userWallet.walletId, + isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, + ), ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index a221f254ec..fa2e9d6908 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -176,7 +176,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - isYieldSupplyFeatureEnabled = false, ) val tokenDetailsState_2 = TokenDetailsState( @@ -202,7 +201,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - isYieldSupplyFeatureEnabled = true, ) val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock) 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 f06bee0ec8..a0b3afb86c 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 @@ -18,11 +18,13 @@ 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.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender 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.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -47,15 +49,14 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.ShouldShowPromoTokenUseCase import com.tangem.domain.promo.models.PromoId -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent @@ -91,7 +92,6 @@ import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -130,7 +130,8 @@ internal class TokenDetailsModel @Inject constructor( private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase, private val openTrustlineUseCase: OpenTrustlineUseCase, private val dismissIncompleteTransactionUseCase: DismissIncompleteTransactionUseCase, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val analyticsEventsHandler: AnalyticsEventHandler, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, @@ -144,7 +145,6 @@ internal class TokenDetailsModel @Inject constructor( private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val receiveAddressesFactory: ReceiveAddressesFactory, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, @@ -193,7 +193,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) @@ -423,9 +422,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun subscribeOnYieldSupplyBalanceIfActive(status: CryptoCurrencyStatus) { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - status.value.yieldSupplyStatus?.isActive == true - ) { + if (status.value.yieldSupplyStatus?.isActive == true) { if (yieldSupplyBalanceJobHolder.isActive && status.value.sources.networkSource != StatusSource.ACTUAL) { return } @@ -709,12 +706,13 @@ internal class TokenDetailsModel @Inject constructor( showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = status, - appCurrencyCode = selectedAppCurrencyFlow.value.code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventsHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } @@ -1235,13 +1233,11 @@ internal class TokenDetailsModel @Inject constructor( } private suspend fun needShowYieldSupplyWarning(): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) + return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } private fun isActiveYieldSupply(): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true + return cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true } override fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index ed8cf22b9f..8fc3962fe6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -23,5 +23,4 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val isYieldSupplyFeatureEnabled: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 0b10013609..16a861d7c9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsYieldSupplyState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter @@ -31,7 +30,6 @@ internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter, TokenDetailsState> { override fun convert(value: Either): TokenDetailsState { @@ -113,10 +111,7 @@ internal class TokenDetailsLoadedBalanceConverter( selectedBalanceType = currentState.selectedBalanceType, isBalanceSelectorEnabled = isBalanceSelectorEnabled, isBalanceFlickering = status.value.isFlickering(), - yieldSupplyState = - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - status.value.yieldSupplyStatus?.isActive == true - ) { + yieldSupplyState = if (status.value.yieldSupplyStatus?.isActive == true) { TokenDetailsYieldSupplyState.Active(clickIntents::onYieldInfoClick) } else { TokenDetailsYieldSupplyState.Empty 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 4bf97d2e4d..5580bcc639 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 @@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList @@ -31,7 +30,6 @@ internal class TokenDetailsSkeletonStateConverter( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -72,7 +70,6 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - isYieldSupplyFeatureEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled, ) } 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 323b323e72..aa14a1aa20 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 @@ -4,7 +4,6 @@ import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig import com.tangem.common.ui.tokens.getUnavailabilityReasonText -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.TextReference @@ -33,8 +32,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.features.tokendetails.impl.R -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList @@ -48,7 +47,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) { private val skeletonStateConverter by lazy { @@ -57,7 +55,6 @@ internal class TokenDetailsStateFactory( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } @@ -74,7 +71,6 @@ internal class TokenDetailsStateFactory( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = tokenDetailsClickIntents, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index abf30bd8f0..a3280e26c4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -148,10 +148,8 @@ internal fun TokenDetailsScreen( ) } - if (state.isYieldSupplyFeatureEnabled) { - item { - yieldSupplyComponent.Content(modifier = itemModifier) - } + item { + yieldSupplyComponent.Content(modifier = itemModifier) } expressTransactionsItems( diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index 6c53807de1..b6b47ea825 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -145,7 +145,7 @@ internal class AccountItemsDelegate @Inject constructor( return this.sortedBy { positionByAccountId[it.id] ?: Int.MAX_VALUE } } - private fun openAccountDetails(account: Account) { + private fun openAccountDetails(account: Account.CryptoPortfolio) { router.push(AppRoute.AccountDetails(account)) } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 5d647616a7..b7e61208c1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -9,6 +9,12 @@ plugins { android { namespace = "com.tangem.feature.wallet.impl" + packaging { + resources { + // To build and run composable preview + merges += "paymentrequest.proto" + } + } } dependencies { @@ -44,6 +50,16 @@ dependencies { exclude(group = "com.google.firebase", module = "protolite-well-known-types") exclude(group = "com.google.protobuf", module = "protobuf-javalite") } + implementation(deps.haze) { + exclude(module = "activity-compose") + exclude(module = "activity") + exclude(module = "activity-ktx") + } + implementation(deps.haze.materials) { + exclude(module = "activity-compose") + exclude(module = "activity") + exclude(module = "activity-ktx") + } /** DI */ implementation(deps.hilt.android) @@ -84,6 +100,7 @@ dependencies { implementation(projects.domain.nft) implementation(projects.domain.nft.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) implementation(projects.domain.promo) diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index 4d05f69171..44d956c114 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -5,11 +5,8 @@ BooleanPropertyNaming:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher$@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem$abstract val showShadow: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem.RoundingMode$abstract val showGap: Boolean - BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private var readyForRateAppNotification = false - BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$val userHasWalletOrWallet2 = userWallets.filterIsInstance<UserWallet.Cold>().any { val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } BooleanPropertyNaming:OrganizeTokensState.kt$OrganizeTokensState.ActionsConfig$val showApplyProgress: Boolean = false BooleanPropertyNaming:ScrollToWalletTransformer.kt$ScrollToWalletTransformer$private val withScrollAnimation: Boolean = true - BooleanPropertyNaming:SetWalletCardDropDownItemsTransformer.kt$SetWalletCardDropDownItemsTransformer$private val dropdownEnabled: Boolean BooleanPropertyNaming:TangemPayState.kt$TangemPayState.Progress$val showProgress: Boolean = false BooleanPropertyNaming:TokenActionButtonConfig.kt$TokenActionButtonConfig$val enabled: Boolean = true BooleanPropertyNaming:UpdateMultiWalletActionButtonBadgeTransformer.kt$UpdateMultiWalletActionButtonBadgeTransformer$private val showSwapBadge: Boolean @@ -18,127 +15,69 @@ BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Is click enabled */ abstract val enabled: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Whether to dim content */ abstract val dimContent: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton.Swap$val showBadge: Boolean = false - BooleanPropertyNaming:WalletModel.kt$WalletModel$private var needToRefreshWallet = false - BooleanPropertyNaming:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase$private val useNewListRepository: Boolean - BooleanPropertyNaming:WalletScreen.kt$val portfolioContent = state is WalletState.MultiCurrency.Content && state.tokensListState is WalletTokensListState.ContentState.PortfolioContent BooleanPropertyNaming:WalletScreen.kt$val showMarketsHint by remember { derivedStateOf { // Show hint only when there are items in the list // and when there a no items to scroll listState.layoutInfo.totalItemsCount > 0 && !listState.canScrollBackward && !listState.canScrollForward || listState.canScrollBackward && !listState.canScrollForward } } BooleanPropertyNaming:WalletScreen.kt$var visible by remember { mutableStateOf(value = false) } BooleanPropertyNaming:WalletScreenState.kt$WalletScreenState$val showMarketsOnboarding: Boolean BooleanPropertyNaming:WalletWithFundsChecker.kt$WalletWithFundsChecker$val prevStatus = statusByWalletId.get(userWalletId) - IgnoredReturnValue:MultiWalletTokenListStore.kt$MultiWalletTokenListStore$remove(userWalletId) MaxChainedCallsOnSameLine:HasSingleWalletSignedHashesUseCase.kt$HasSingleWalletSignedHashesUseCase$userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes - MultilineLambdaItParameter:BasicTokenListSubscriber.kt$BasicTokenListSubscriber${ it.getOrElse { e -> Timber.e("Failed to load app currency: $e") AppCurrency.Default } } MultilineLambdaItParameter:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler${ Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") showAlert(Failed) } MultilineLambdaItParameter:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher${ it.fold( ifLeft = { emit(UserWalletItemUM.ImageState.Loading) }, ifRight = { wallet -> emitAll(walletImage(wallet, size)) }, ) } - MultilineLambdaItParameter:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory${ hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) .conflate() .distinctUntilChanged() .firstOrNull() } - MultilineLambdaItParameter:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory${ val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } - MultilineLambdaItParameter:NetworkGroupToDraggableItemsConverterV2.kt$NetworkGroupToDraggableItemsConverterV2${ AccountCryptoCurrencyStatus( account = account, status = it, ) } MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ isBalanceHidden = it.isBalanceHidden stateHolder.updateHiddenState(isBalanceHidden) } - MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ stateHolder.updateStateAfterTokenListSorting(it) cachedTokenList = it } - MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) cachedAccountStatusList = it } - MultilineLambdaItParameter:OrganizedTokenListConverter.kt$OrganizedTokenListConverter${ AccountCryptoCurrencyStatus( account = cryptoAccount, status = it, ) } - MultilineLambdaItParameter:PrimaryCurrencySubscriber.kt$PrimaryCurrencySubscriber${ // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( balance = it, tokensCount = null, ), ) } - MultilineLambdaItParameter:PrimaryCurrencySubscriber.kt$PrimaryCurrencySubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } - MultilineLambdaItParameter:PrimaryCurrencySubscriberV2.kt$PrimaryCurrencySubscriberV2${ // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( balance = it, tokensCount = null, ), ) } - MultilineLambdaItParameter:ReviewManagerRequester.kt$ReviewManagerRequester${ handleOnCompleteRequestTask( reviewManager = reviewManager, activity = context.findActivity(), task = it, onDismissClick = onDismissClick, ) } MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -> when (button) { is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } } - MultilineLambdaItParameter:SetVisaInfoTransformer.kt$SetVisaInfoTransformer${ if (it is RefreshTokenExpiredException) { return getRefreshTokenExpiredState(prevState) } return prevState.copy( buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } - MultilineLambdaItParameter:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } } MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } } - MultilineLambdaItParameter:TxHistorySubscriber.kt$TxHistorySubscriber${ SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, error = it, clickIntents = clickIntents, ) } - MultilineLambdaItParameter:TxHistorySubscriberV2.kt$TxHistorySubscriberV2${ SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, error = it, clickIntents = clickIntents, ) } - MultilineLambdaItParameter:UpdateMultiWalletActionsTransformer.kt$UpdateMultiWalletActionsTransformer${ when (it) { is WalletManageButton.Buy -> { it.copy( enabled = buyStatus.isContent(), dimContent = !buyStatus.isContent(), ) } is WalletManageButton.Sell -> { it.copy( enabled = sellStatus.isContent(), dimContent = !sellStatus.isContent(), ) } is WalletManageButton.Swap -> { it.copy( enabled = swapStatus.isContent(), dimContent = !swapStatus.isContent(), ) } else -> it } } MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get primary currency status $it") null } MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get selected wallet $it") null } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e("Unable to get balances and limits: $it") return@launch } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e(it, "Failed to get transaction details") return@launch } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e(it, "Failed to get visa currency") return@launch } - MultilineLambdaItParameter:VisaWalletSubscriber.kt$VisaWalletSubscriber${ Timber.e(it, "Failed to load VISA currency") setFailedTxHistoryState(it) return@flow } - MultilineLambdaItParameter:VisaWalletSubscriber.kt$VisaWalletSubscriber${ Timber.e(it, "Failed to load tx history for wallet ${userWallet.walletId}") throw it } MultilineLambdaItParameter:WalletCard.kt${ haptic.performHapticFeedback(HapticFeedbackType.LongPress) isMenuVisible = true pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) } MultilineLambdaItParameter:WalletCard.kt${ val press = PressInteraction.Press(it) interactionSource.emit(press) tryAwaitRelease() interactionSource.emit(PressInteraction.Release(press)) } - MultilineLambdaItParameter:WalletCardClickIntents.kt$WalletCardClickIntentsImplementor${ Timber.e("Unable to delete user wallet: $it") return@launch } - MultilineLambdaItParameter:WalletClickIntents.kt$WalletClickIntents${ if (!it.isLocked) { launch { walletContentFetcher(userWalletId = it.walletId) } } walletScreenContentLoader.load( userWallet = it, clickIntents = this@WalletClickIntents, coroutineScope = modelScope, ) } MultilineLambdaItParameter:WalletContentClickIntents.kt$WalletContentClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) return@launch } - MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) clipboardManager.setText(text = it, isSensitive = true) } - MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) } - MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) } - MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } MultilineLambdaItParameter:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase${ val defaultName = it.name val suggestedWalletName = suggestedWalletName(defaultName, existingNames) if (defaultName != suggestedWalletName) { userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) } Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) } MultilineLambdaItParameter:WalletScreen.kt${ PaddingValues( bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp, ) } MultilineLambdaItParameter:WalletScreen.kt${ WalletSnackbarHost( snackbarHostState = it, event = state.event, modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing4) .navigationBarsPadding(), ) } - MultilineLambdaItParameter:WalletScreen.kt${ balancesAndLimitsBlock( modifier = itemModifier, state = it.balancesAndLimitBlockState, ) } - MultilineLambdaItParameter:WalletScreen.kt${ findPortfolioVisibleState( portfolio = it, expandedState = expandedState, collapsedState = collapsedState, ) } MultilineLambdaItParameter:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) } MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) NamedArguments:TangemSnapFlingBehavior.kt$HighVelocityApproachAnimation$animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep) NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$animateSnap( closestOffset, closestOffset, animationState, snapAnimationSpec, ) { delta -> remainingScrollOffset -= delta onRemainingScrollOffsetUpdate(remainingScrollOffset) } NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$animateSnap( remainingOffset, remainingOffset, animationState.copy(value = 0f), snapAnimationSpec, ) { delta -> remainingScrollOffset -= delta onAnimationStep(remainingScrollOffset) } NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$approach( initialTargetOffset, initialVelocity, animation, snapLayoutInfoProvider, density, onAnimationStep, ) NamedArguments:TangemSnapFlingBehavior.kt$approachAnimation( this, initialTargetOffset, initialVelocity, onAnimationStep, ) - NamedArguments:WalletContent.kt$tokensListItems(state.tokensListState, modifier, isBalanceHidden, portfolioVisibleState) NamedArguments:WalletContent.kt$txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) - NamedArguments:WalletScreenContentLoader.kt$WalletScreenContentLoader$loadInternal(userWallet, clickIntents, coroutineScope, isRefresh = true) - NamedArguments:WalletScreenContentLoader.kt$WalletScreenContentLoader$loadInternal(userWallet, clickIntents, coroutineScope, isRefresh) NestedScopeFunctions:WalletScreen.kt$let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } NestedScopeFunctions:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } NestedScopeFunctions:WalletScreen.kt$let { marketPriceBlockState -> marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) } NoNameShadowing:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher${ it.isMultiCurrency } NoNameShadowing:MultiCurrencyAccountContent.kt$modifier - NoNameShadowing:TxHistorySubscriber.kt$TxHistorySubscriber${ it.cachedIn(coroutineScope) } - NoNameShadowing:TxHistorySubscriberV2.kt$TxHistorySubscriberV2${ it.cachedIn(coroutineScope) } NoNameShadowing:WalletComponent.kt$WalletComponent$dialog NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token } NoNameShadowing:WalletNFTItem.kt$modifier NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size} - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${networkStatuses?.size} - NullableToStringCall:WalletStateController.kt$WalletStateController$${transformer::class.simpleName} - NullableToStringCall:WalletSubscriber.kt$WalletSubscriber$${this::class.simpleName} PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_modelScope PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_router PropertyUsedBeforeDeclaration:OrganizeTokensModel.kt$OrganizeTokensModel$uiState PropertyUsedBeforeDeclaration:WalletScreenPreviewData.kt$WalletScreenPreviewData$buyButton PropertyUsedBeforeDeclaration:WalletStateController.kt$WalletStateController$mutableUiState ReusedModifierInstance:DefaultWalletEntryComponent.kt$DefaultWalletEntryComponent$Content(modifier) - ReusedModifierInstance:VisaTxDetailsBottomSheet.kt$LazyColumn( modifier = modifier.background(TangemTheme.colors.background.secondary), contentPadding = PaddingValues( bottom = TangemTheme.dimens.spacing16, ), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), horizontalAlignment = Alignment.CenterHorizontally, ) { item { TransactionBlock(config.transaction) } items(config.requests) { item -> BlockchainRequestBlock(item) } item { DisputeButton(config.onDisputeClick) } } ReusedModifierInstance:WalletNFTItem.kt$Image( modifier = modifier .background(TangemTheme.colors.stroke.primary), painter = painterResource(R.drawable.ic_nft_preview_more_16), contentDescription = null, ) ReusedModifierInstance:WalletNFTItem.kt$SubcomposeAsyncImage( modifier = modifier, model = s.url, loading = { RectangleShimmer(radius = 0.dp) }, error = { Box( modifier = Modifier.background(TangemTheme.colors.field.primary), ) }, contentDescription = null, ) ReusedModifierInstance:WalletNFTItem.kt$take(modifiers.size) SuspendFunSwallowedCancellation:WalletModel.kt$WalletModel$runCatching - UnnecessaryLet:BalancesAndLimitsBottomSheetConverter.kt$BalancesAndLimitsBottomSheetConverter$let(::formatAmount) - UnnecessaryLet:MultiWalletContentLoader.kt$MultiWalletContentLoader$let(::add) - UnnecessaryLet:SingleWalletWithTokenContentLoader.kt$SingleWalletWithTokenContentLoader$let(::add) UnnecessaryLet:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$let { abs(it) * sign(initialVelocity) // ensure offset sign is correct } UnnecessaryLet:WalletClickIntents.kt$WalletClickIntents$let(::add) UnnecessaryLet:WalletScreen.kt$let { (state.tokensListState as? WalletTokensListState.ContentState)?.let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } } UnnecessaryLet:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - UnnecessarySafeCall:SetVisaInfoTransformer.kt$SetVisaInfoTransformer$visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) } UseEmptyCounterpart:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher$mapOf<String, ArtworkUM>() - UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$mapOf() UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$setOf() UseEmptyCounterpart:PortfolioOrganizeTokensAnalyticsEvent.kt$PortfolioOrganizeTokensAnalyticsEvent$mapOf() UseEmptyCounterpart:PromoActivationAnalytics.kt$PromoActivationAnalytics$mapOf() - UseEmptyCounterpart:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber$listOf() - UseEmptyCounterpart:SingleWalletExpressStatusesSubscriberV2.kt$SingleWalletExpressStatusesSubscriberV2$listOf() UseEmptyCounterpart:TokenListStateConverter.kt$TokenListStateConverter$listOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.Basic$mapOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.MainScreen$mapOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.PushBannerPromo$mapOf() - UseOrEmpty:CryptoCurrenciesIdsResolver.kt$CryptoCurrenciesIdsResolver$accountStatusList?.accountStatuses ?.filter { it.getCryptoTokenList() != TokenList.Empty } ?.associate { accountStatus -> val currencies = accountStatus.flattenCurrencies() accountStatus.account as Account.CryptoPortfolio to draggableTokens .asSequence() .filter { it.accountId == accountStatus.account.accountId.value } .mapNotNull { sortedToken -> currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency } .toList() } ?: emptyMap() UseSumOfInsteadOfFlatMapSize:TokenListStateConverter.kt$TokenListStateConverter$flatMap(NetworkGroup::currencies) VarCouldBeVal:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$private var motionScaleDuration = DefaultScrollMotionDurationScale - VarCouldBeVal:WalletModel.kt$WalletModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt index b54ed3161b..e8a18c88ec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen +import com.tangem.feature.wallet.child.organizetokens.ui.OrganizeTokensScreen import kotlinx.coroutines.launch internal class OrganizeTokensComponent( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt similarity index 92% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt index a0c96c2974..5d4948ab02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.analytics +package com.tangem.feature.wallet.child.organizetokens.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt index da7e0746a2..4d0560b517 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.token.state.TokenItemState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt similarity index 54% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt index 720edbb802..38144081c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt @@ -1,27 +1,9 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -@Deprecated("Use OrganizeTokensListUM instead, will be removed in future releases") -@Immutable -internal sealed class OrganizeTokensListState { - abstract val items: PersistentList - - data class GroupedByNetwork( - override val items: PersistentList, - ) : OrganizeTokensListState() - - data class Ungrouped( - override val items: PersistentList, - ) : OrganizeTokensListState() - - data object Empty : OrganizeTokensListState() { - override val items: PersistentList = persistentListOf() - } -} - @Immutable internal sealed interface OrganizeTokensListUM { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt similarity index 91% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt index 556f2e081d..234ab0e793 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.event.StateEvent @@ -7,7 +7,6 @@ import org.burnoutcrew.reorderable.ItemPosition @Immutable internal data class OrganizeTokensState( val onBackClick: () -> Unit, - val itemsState: OrganizeTokensListState, val tokenListUM: OrganizeTokensListUM, val header: HeaderConfig, val actions: ActionsConfig, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt new file mode 100644 index 0000000000..cb1a1b0db7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.model.AccountCryptoCurrencies +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM + +internal class CryptoCurrenciesIdsResolver { + + fun resolve(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { + if (accountStatusList == null) return emptyMap() + + val draggableTokens = when (tokensListUM) { + OrganizeTokensListUM.EmptyList -> return emptyMap() + is OrganizeTokensListUM.AccountList, + is OrganizeTokensListUM.TokensList, + -> tokensListUM.items.filterIsInstance() + } + + return accountStatusList.accountStatuses + .filterCryptoPortfolio() + .filter { it.tokenList != TokenList.Empty } + .associate { accountStatus -> + val currenciesById = accountStatus.flattenCurrencies().associateBy { it.currency.id.value } + + accountStatus.account to draggableTokens + .asSequence() + .filter { it.accountId == accountStatus.account.accountId.value } + .mapNotNull { token -> currenciesById[token.id]?.currency } + .toList() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt similarity index 76% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt index e0f4868347..5123361a66 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens +package com.tangem.feature.wallet.child.organizetokens.model -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem import org.burnoutcrew.reorderable.ItemPosition internal interface OrganizeTokensIntents { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index fb310ec8be..bd4e6ee9be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -7,35 +7,21 @@ 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.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCaseV2 -import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCaseV2 -import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCaseV2 +import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase -import com.tangem.domain.tokens.ToggleTokenListSortingUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensIntents -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder -import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 +import com.tangem.feature.wallet.child.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -49,18 +35,13 @@ internal class OrganizeTokensModel @Inject constructor( paramsContainer: ParamsContainer, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, override val dispatchers: CoroutineDispatcherProvider, - private val getTokenListUseCase: GetTokenListUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventsHandler: AnalyticsEventHandler, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase, private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventsHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val toggleTokenListGroupingUseCaseV2: ToggleTokenListGroupingUseCaseV2, - private val toggleTokenListSortingUseCaseV2: ToggleTokenListSortingUseCaseV2, - private val applyTokenListSortingUseCaseV2: ApplyTokenListSortingUseCaseV2, ) : Model(), OrganizeTokensIntents { private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() @@ -68,24 +49,17 @@ internal class OrganizeTokensModel @Inject constructor( private var isBalanceHidden = true private val dragAndDropAdapter = DragAndDropAdapter( - listStateProvider = Provider { uiState.value.itemsState }, - ) - - private val dragAndDropAdapterV2 = DragAndDropAdapterV2( tokenListUMProvider = Provider { uiState.value.tokenListUM }, ) private val stateHolder = OrganizeTokensStateHolder( intents = this, - dragAndDropIntents = dragAndDropAdapter, - dragAndDropAdapterV2 = dragAndDropAdapterV2, + dragAndDropAdapter = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - accountsFeatureToggles = accountsFeatureToggles, ) private val userWalletId = paramsContainer.require().userWalletId - private var cachedTokenList: TokenList? = null private var cachedAccountStatusList: AccountStatusList? = null private var isAccountsModeEnabled: Boolean = false @@ -113,68 +87,35 @@ internal class OrganizeTokensModel @Inject constructor( } override fun onSortClick() { - if (accountsFeatureToggles.isFeatureEnabled) { - val list = cachedAccountStatusList ?: return - if (list.sortType == TokensSortType.BALANCE) return + val list = cachedAccountStatusList ?: return + if (list.sortType == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) - modelScope.launch { - toggleTokenListSortingUseCaseV2(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) - cachedAccountStatusList = it - }, - ) - } - } else { - val list = cachedTokenList ?: return - if (list.sortedBy == TokensSortType.BALANCE) return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) - - modelScope.launch { - toggleTokenListSortingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) - } + modelScope.launch { + toggleTokenListSortingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { accountStatusList -> + stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled) + cachedAccountStatusList = accountStatusList + }, + ) } } override fun onGroupClick() { - if (accountsFeatureToggles.isFeatureEnabled) { - val list = cachedAccountStatusList ?: return + val list = cachedAccountStatusList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) - modelScope.launch { - toggleTokenListGroupingUseCaseV2(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) - cachedAccountStatusList = it - }, - ) - } - } else { - val list = cachedTokenList ?: return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) - - modelScope.launch { - toggleTokenListGroupingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) - } + modelScope.launch { + toggleTokenListGroupingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { accountStatusList -> + stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled) + cachedAccountStatusList = accountStatusList + }, + ) } } @@ -183,39 +124,20 @@ internal class OrganizeTokensModel @Inject constructor( stateHolder.updateStateToDisplayProgress() val resolver = CryptoCurrenciesIdsResolver() val isSortedByBalance = uiState.value.header.isSortedByBalance + val tokensListUM = uiState.value.tokenListUM - val result = if (accountsFeatureToggles.isFeatureEnabled) { - val tokensListUM = uiState.value.tokenListUM + val isGroupedByNetwork = tokensListUM.isGrouped - val isGroupedByNetwork = tokensListUM.isGrouped + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - - applyTokenListSortingUseCaseV2( - sortedTokensIdsByAccount = resolver.resolveV2(tokensListUM, cachedAccountStatusList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } else { - val listState = uiState.value.itemsState - - val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork - - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - - applyTokenListSortingUseCase( - userWalletId = userWalletId, - sortedTokensIds = resolver.resolve(listState, cachedTokenList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } + val result = applyTokenListSortingUseCase( + sortedTokensIdsByAccount = resolver.resolve(tokensListUM, cachedAccountStatusList), + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) result.fold( ifLeft = stateHolder::updateStateWithError, @@ -235,72 +157,35 @@ internal class OrganizeTokensModel @Inject constructor( private fun bootstrapTokenList() { modelScope.launch { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountList = singleAccountStatusListSupplier.getSyncOrNull( - SingleAccountStatusListProducer.Params(userWalletId), - ) ?: return@launch + val accountList = singleAccountStatusListSupplier.getSyncOrNull( + SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return@launch - isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() - stateHolder.updateStateWithAccountList( - accountStatusList = accountList, - isAccountsModeEnabled = isAccountsModeEnabled, - ) + stateHolder.updateStateWithAccountList( + accountStatusList = accountList, + isAccountsModeEnabled = isAccountsModeEnabled, + ) - cachedAccountStatusList = accountList - } else { - val tokenList = getTokenList() ?: return@launch - stateHolder.updateStateWithTokenList(tokenList) - cachedTokenList = tokenList - } + cachedAccountStatusList = accountList } } - private suspend fun getTokenList(): TokenList? { - val maybeTokenList = getTokenListUseCase.launch(userWalletId) - .filterNot(Lce::isLoading) - .firstOrNull() - ?: return null - - return maybeTokenList - .onError(stateHolder::updateStateWithError) - .getOrNull(isPartialContentAccepted = false) - } - private fun bootstrapDragAndDropUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - dragAndDropAdapterV2.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChangedV2(type) + dragAndDropAdapter.dragAndDropUpdates + .distinctUntilChanged() + .onEach { (type, updatedListState) -> + disableSortingByBalanceIfListChanged(type) - stateHolder.updateStateWithManualSortingV2(updatedListState) - } - .launchIn(modelScope) - } else { - dragAndDropAdapter.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChanged(type) - - stateHolder.updateStateWithManualSorting(updatedListState) - } - .launchIn(modelScope) - } + stateHolder.updateStateWithManualSorting(updatedListState) + } + .launchIn(modelScope) } private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) { if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return - if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { - cachedTokenList = cachedTokenList?.disableSortingByBalance() - stateHolder.disableSortingByBalance() - } - } - - private fun disableSortingByBalanceIfListChangedV2(dragOperationType: DragAndDropAdapterV2.DragOperation.Type) { - if (dragOperationType !is DragAndDropAdapterV2.DragOperation.Type.End) return - if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE) stateHolder.disableSortingByBalance() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt new file mode 100644 index 0000000000..638cfa480b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt @@ -0,0 +1,111 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.TokenListToStateConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.error.TokenListSortingErrorConverter +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter +import com.tangem.utils.Provider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +internal class OrganizeTokensStateHolder( + private val intents: OrganizeTokensIntents, + private val dragAndDropAdapter: DragAndDropAdapter, + private val appCurrencyProvider: Provider, +) { + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + private val inProgressStateConverter by lazy { InProgressStateConverter() } + + private val tokenListSortingErrorConverter by lazy { + TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) + } + + val stateFlow: StateFlow = stateFlowInternal + + fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverter( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this) + } + } + + fun updateStateAfterTokenListSorting(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverter( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this).copy( + scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), + ) + } + } + + fun updateStateToDisplayProgress() { + updateState { inProgressStateConverter.convert(value = this) } + } + + fun updateStateToHideProgress() { + updateState { inProgressStateConverter.convertBack(value = this) } + } + + fun updateStateWithManualSorting(tokenListUM: OrganizeTokensListUM) { + updateState { copy(tokenListUM = tokenListUM) } + } + + fun disableSortingByBalance() { + updateState { copy(header = header.copy(isSortedByBalance = false)) } + } + + fun updateHiddenState(isBalanceHidden: Boolean) { + updateState { copy(isBalanceHidden = isBalanceHidden) } + } + + fun updateStateWithError(error: TokenListSortingError) { + updateState { tokenListSortingErrorConverter.convert(error) } + } + + private fun getInitialState(): OrganizeTokensState { + return OrganizeTokensState( + onBackClick = intents::onBackClick, + tokenListUM = OrganizeTokensListUM.EmptyList, + header = OrganizeTokensState.HeaderConfig( + onSortClick = intents::onSortClick, + onGroupClick = intents::onGroupClick, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = intents::onApplyClick, + onCancelClick = intents::onCancelClick, + ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = dragAndDropAdapter::onItemDragged, + onItemDragStart = dragAndDropAdapter::onItemDraggingStart, + onItemDragEnd = dragAndDropAdapter::onItemDraggingEnd, + canDragItemOver = dragAndDropAdapter::canDragItemOver, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + ) + } + + private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { + stateFlowInternal.update(block) + } + + private fun consumeScrollListToTopEvent() { + updateState { copy(scrollListToTop = consumedEvent()) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt similarity index 59% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt index 80ea9a25b1..1133e2b1b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem internal fun getGroupPlaceholder(index: Int, accountId: String = ""): DraggableItem.Placeholder { return DraggableItem.Placeholder( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt similarity index 63% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt index 2c10ffaf82..f668be3be6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt @@ -1,36 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem -internal fun List.uniteItems(): List { - val items = prepareItems() - val lastItemIndex = items.lastIndex - - return prepareItems().mapIndexed { index, item -> - val mode = when (index) { - // 1 index is used because the first item is always a placeholder, check `prepareItems()` function - 1 -> DraggableItem.RoundingMode.Top() - lastItemIndex -> DraggableItem.RoundingMode.Bottom() - else -> when (item) { - is DraggableItem.Portfolio, - is DraggableItem.Placeholder, - -> DraggableItem.RoundingMode.None - is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) - is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) { - DraggableItem.RoundingMode.Bottom(showGap = true) - } else { - DraggableItem.RoundingMode.None - } - } - } - - item - .updateRoundingMode(mode) - .updateShadowVisibility(show = false) - } -} - -internal fun List.uniteItemsV2(isAccountsMode: Boolean): List { +internal fun List.uniteItems(isAccountsMode: Boolean): List { val items = this val lastItemIndex = items.lastIndex @@ -92,27 +64,6 @@ internal fun List.divideMovingItem(movingItem: DraggableItem): Li return mutableList } -/** - * !!! Workaround !!! - * - * We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the - * [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item. - * - * @since 07.09.2023 - * */ -private fun List.prepareItems(): List { - val firstPlaceholderId = "initial_placeholder" - val items = this - - return mutableListOf().apply { - add(DraggableItem.Placeholder(firstPlaceholderId)) - - val itemsWithoutFirstPlaceholder = items.filterNot { it.id == firstPlaceholderId } - - addAll(itemsWithoutFirstPlaceholder) - } -} - /** * Applying rounding to tokens * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt index 0225b0d6ca..b4d70c5fb0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt new file mode 100644 index 0000000000..2935a14855 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.child.organizetokens.model.common + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal inline fun OrganizeTokensListUM.updateItems( + update: (PersistentList) -> List, +): OrganizeTokensListUM { + val updatedItems = update(items).toPersistentList() + + return when (this) { + is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) + is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) + OrganizeTokensListUM.EmptyList -> this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/TokenListOperations.kt similarity index 83% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/TokenListOperations.kt index 8600d8beaf..b298e1121c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/TokenListOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.tokenlist.TokenList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt similarity index 82% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt index 936385e8ea..7da5b51e59 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter +package com.tangem.feature.wallet.child.organizetokens.model.converter -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState import com.tangem.utils.converter.TwoWayConverter internal class InProgressStateConverter : TwoWayConverter { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt similarity index 83% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt index 279d59f8ed..61bb28d854 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter +package com.tangem.feature.wallet.child.organizetokens.model.converter import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.domain.account.models.AccountStatusList @@ -8,17 +8,17 @@ import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.OrganizedTokenListConverter +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizedTokenListConverter import com.tangem.utils.converter.Converter import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList -internal class TokenListToStateConverterV2( +internal class TokenListToStateConverter( private val accountStatusList: AccountStatusList, private val isAccountsMode: Boolean, private val appCurrency: AppCurrency, @@ -82,7 +82,7 @@ internal class AccountTokenItemConverter( emptyList() } }.toList() - .uniteItemsV2(true).toPersistentList(), + .uniteItems(true).toPersistentList(), ) } else { OrganizeTokensListUM.TokensList( @@ -92,7 +92,7 @@ internal class AccountTokenItemConverter( add(getGroupPlaceholder(accountId = value.mainAccount.accountId.value, index = -1)) } addAll(organizedTokenListConverter.convert(value.mainAccount)) - }.uniteItemsV2(false) + }.uniteItems(false) .toPersistentList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListErrorConverter.kt similarity index 64% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListErrorConverter.kt index e2117bfc5f..506816829f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListErrorConverter.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error +package com.tangem.feature.wallet.child.organizetokens.model.converter.error import com.tangem.domain.tokens.error.TokenListError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt similarity index 65% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt index 7d7bcd16c5..f86c07468e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error +package com.tangem.feature.wallet.child.organizetokens.model.converter.error import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 478351d5dc..cd6c995427 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState @@ -11,14 +11,14 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance import com.tangem.common.getTotalWithRewardsStakingBalance -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId +import com.tangem.feature.wallet.child.organizetokens.model.common.getTokenItemId import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero import java.math.BigDecimal -internal class CryptoCurrencyToDraggableItemConverterV2( +internal class CryptoCurrencyToDraggableItemConverter( private val appCurrency: AppCurrency, ) : Converter { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt index 13206c8a13..9a68fca15d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -1,16 +1,16 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder import com.tangem.utils.converter.Converter -internal class NetworkGroupToDraggableItemsConverterV2( - private val itemConverter: CryptoCurrencyToDraggableItemConverterV2, +internal class NetworkGroupToDraggableItemsConverter( + private val itemConverter: CryptoCurrencyToDraggableItemConverter, ) : Converter, List> { override fun convert(value: Pair): List { @@ -42,10 +42,10 @@ internal class NetworkGroupToDraggableItemsConverterV2( private fun createTokens(account: Account.CryptoPortfolio, group: NetworkGroup): List { return itemConverter.convertList( - group.currencies.map { + group.currencies.map { currencyStatus -> AccountCryptoCurrencyStatus( account = account, - status = it, + status = currencyStatus, ) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt similarity index 85% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt index c0938d2748..c1f5ce4fef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt @@ -1,10 +1,10 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -14,9 +14,9 @@ internal class OrganizedTokenListConverter( private val appCurrency: AppCurrency, ) : Converter> { - private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) } + private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverter(appCurrency) } private val groupsConverter by lazy { - NetworkGroupToDraggableItemsConverterV2(tokensConverter) + NetworkGroupToDraggableItemsConverter(tokensConverter) } override fun convert(value: AccountStatus.CryptoPortfolio): PersistentList { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt index 6c0a7531b8..7e4e3b77fd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt @@ -1,11 +1,11 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd +package com.tangem.feature.wallet.child.organizetokens.model.dnd -import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents +import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.model.common.updateItems import com.tangem.utils.Provider import kotlinx.collections.immutable.mutate import kotlinx.coroutines.flow.Flow @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import org.burnoutcrew.reorderable.ItemPosition -internal class DragAndDropAdapterV2( +internal class DragAndDropAdapter( private val tokenListUMProvider: Provider, ) : DragAndDropIntents { @@ -81,7 +81,7 @@ internal class DragAndDropAdapterV2( is DraggableItem.Placeholder, is DraggableItem.Portfolio, -> items - is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupV2(items, item) + is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) .divideMovingItem(item) is DraggableItem.Token -> items.divideMovingItem(item) } @@ -96,11 +96,11 @@ internal class DragAndDropAdapterV2( updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { when (draggingItem) { is DraggableItem.GroupHeader -> { - draggableGroupsOperations.expandGroupsV2(items) - .uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + draggableGroupsOperations.expandGroups(items) + .uniteItems(tokenListUM is OrganizeTokensListUM.AccountList) } is DraggableItem.Token -> { - items.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + items.uniteItems(tokenListUM is OrganizeTokensListUM.AccountList) } is DraggableItem.Placeholder, is DraggableItem.Portfolio, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt similarity index 59% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt index e92d772829..7ce66f0a2e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt @@ -1,9 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd +package com.tangem.feature.wallet.child.organizetokens.model.dnd -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder internal class DraggableGroupsOperations { @@ -24,47 +23,9 @@ internal class DraggableGroupsOperations { return itemsWithoutGroupTokens.divideMovingItem(movingGroup) } - fun collapseGroupV2(items: List, movingGroup: DraggableItem.GroupHeader): List { - if (!groupIdToTokens.isNullOrEmpty()) return items - - groupIdToTokens = items - .asSequence() - .filterIsInstance() - .groupBy { it.groupId } - - val itemsWithoutGroupTokens = items.filterNot { - it is DraggableItem.Token && it.groupId == movingGroup.id - } - - return itemsWithoutGroupTokens.divideMovingItem(movingGroup) - } - fun expandGroups(items: List): List { if (groupIdToTokens.isNullOrEmpty()) return items - val currentGroups = items.filterIsInstance() - val lastGroupIndex = currentGroups.lastIndex - - val expandedGroups = currentGroups - .flatMapIndexed { index, group -> - buildList { - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - if (index != lastGroupIndex) { - add(getGroupPlaceholder(index)) - } - } - } - .uniteItems() - - groupIdToTokens = null - - return expandedGroups - } - - fun expandGroupsV2(items: List): List { - if (groupIdToTokens.isNullOrEmpty()) return items - val accountList = items.filterIsInstance() val currentGroups = items.filterIsInstance() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt similarity index 95% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt index 2996d749be..1c23378393 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens +package com.tangem.feature.wallet.child.organizetokens.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler @@ -44,12 +44,11 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.OrganizeTokensScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensPreview import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable @@ -76,7 +75,6 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier .fillMaxSize(), listState = tokensListState, tokensListUM = state.tokenListUM, - state = state.itemsState, dndConfig = state.dndConfig, isBalanceHidden = state.isBalanceHidden, ) @@ -98,18 +96,13 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier @Composable private fun TokenList( listState: LazyListState, - state: OrganizeTokensListState, tokensListUM: OrganizeTokensListUM, dndConfig: OrganizeTokensState.DragAndDropConfig, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current - val tokenList = if (tokensListUM !is OrganizeTokensListUM.EmptyList) { - tokensListUM.items - } else { - state.items - } + val tokenList = tokensListUM.items Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { { _, _ -> @@ -423,8 +416,8 @@ private fun OrganizeTokensScreenPreview( private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.organizeTokensState, - WalletPreviewData.groupedOrganizeTokensState, + OrganizeTokensPreview.stateAccounts, + OrganizeTokensPreview.state, ), ) // endregion Preview \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt new file mode 100644 index 0000000000..47869d9b69 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt @@ -0,0 +1,127 @@ +package com.tangem.feature.wallet.child.organizetokens.ui.preview + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import java.util.UUID + +internal object OrganizeTokensPreview { + + private const val networksSize = 10 + private const val tokensSize = 3 + + private val tokenItemDragState by lazy { + TokenItemState.Draggable( + id = UUID.randomUUID().toString(), + iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), + ) + } + + private val draggableItems: PersistentList by lazy { + List(networksSize) { it } + .flatMap { index -> + val lastNetworkIndex = networksSize - 1 + val lastTokenIndex = tokensSize - 1 + val networkNumber = index + 1 + + val group = DraggableItem.GroupHeader( + id = networkNumber, + networkName = "$networkNumber", + + roundingMode = when (index) { + 0 -> DraggableItem.RoundingMode.Top() + lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() + else -> DraggableItem.RoundingMode.None + }, + accountId = "account_$networkNumber", + ) + + val tokens: MutableList = mutableListOf() + repeat(times = tokensSize) { i -> + val tokenNumber = i + 1 + tokens.add( + DraggableItem.Token( + tokenItemState = tokenItemDragState.copy( + id = "${group.id}_token_$tokenNumber", + titleState = TokenItemState.TitleState.Content( + text = stringReference(value = "Token $tokenNumber from $networkNumber network"), + ), + ), + groupId = group.id, + accountId = "account_$networkNumber", + roundingMode = when { + i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() + else -> DraggableItem.RoundingMode.None + }, + ), + ) + } + + val divider = DraggableItem.Placeholder( + id = "divider_$networkNumber", + accountId = "account_$networkNumber", + ) + + buildList { + add(group) + addAll(tokens) + if (index != lastNetworkIndex) { + add(divider) + } + } + } + .toPersistentList() + } + + val stateAccounts by lazy { + OrganizeTokensState( + onBackClick = {}, + tokenListUM = OrganizeTokensListUM.AccountList( + items = draggableItems, + isGrouped = true, + ), + header = OrganizeTokensState.HeaderConfig( + onSortClick = {}, + onGroupClick = {}, + ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = { _, _ -> }, + onItemDragStart = {}, + canDragItemOver = { _, _ -> false }, + onItemDragEnd = {}, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = {}, + onCancelClick = {}, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + ) + } + + val state by lazy { + stateAccounts.copy( + tokenListUM = OrganizeTokensListUM.TokensList( + items = draggableItems, + isGrouped = true, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 8d96777c9e..6bcf590744 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss @@ -14,6 +15,7 @@ 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.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent @@ -23,12 +25,11 @@ import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen +import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2 import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle -import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -38,18 +39,18 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +@OptIn(ExperimentalDecomposeApi::class) @Suppress("LongParameterList") internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, - marketsEntryComponentFactory: MarketsEntryComponent.Factory, feedEntryComponentFactory: FeedEntryComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, - private val feedFeatureToggle: FeedFeatureToggle, + private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() @@ -60,9 +61,6 @@ internal class WalletComponent @AssistedInject constructor( entryRoute = null, ) } - private val marketsEntryComponent by lazy { - marketsEntryComponentFactory.create(child("marketsEntryComponent")) - } init { lifecycle.subscribe(model.screenLifecycleProvider) @@ -148,18 +146,33 @@ internal class WalletComponent @AssistedInject constructor( var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() - WalletScreen( - state = model.uiState.collectAsStateWithLifecycle().value, - bottomSheetContent = { - BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = modifier, - ) - }, - bottomSheetHeaderHeightProvider = { headerSize }, - onBottomSheetStateChange = { bottomSheetState.value = it }, - ) + if (designFeatureToggles.isRedesignEnabled) { + WalletScreen2( + state = model.uiState.collectAsStateWithLifecycle().value, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, + ) + } else { + WalletScreen( + state = model.uiState.collectAsStateWithLifecycle().value, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, + ) + } when (val dialog = dialog.child?.instance) { is ComposableDialogComponent -> dialog.Dialog() @@ -174,19 +187,11 @@ internal class WalletComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier, ) { - if (feedFeatureToggle.isFeedEnabled) { - feedEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } else { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } + feedEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) } @AssistedFactory 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 256d99fb51..6b19da9a9f 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 @@ -11,7 +11,6 @@ import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -31,7 +30,10 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents 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 -import com.tangem.feature.wallet.presentation.wallet.domain.* +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -43,11 +45,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSend import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedCallbacks import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks -import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import kotlinx.coroutines.* @@ -80,7 +79,6 @@ internal class WalletModel @Inject constructor( private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val walletImageResolver: WalletImageResolver, - private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, private val analyticsEventsHandler: AnalyticsEventHandler, private val walletContentFetcher: WalletContentFetcher, @@ -90,17 +88,13 @@ internal class WalletModel @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, private val userWalletsListRepository: UserWalletsListRepository, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, - private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, private val trackingContextProxy: TrackingContextProxy, private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val feedFeatureToggle: FeedFeatureToggle, private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase, private val appsFlyerStore: AppsFlyerStore, val screenLifecycleProvider: ScreenLifecycleProvider, @@ -116,13 +110,12 @@ internal class WalletModel @Inject constructor( private val refreshWalletJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder() - private var needToRefreshWallet = false - private var expressTxStatusTaskScheduler = SingleTaskScheduler() + private var shouldRefreshWallet = false + private val expressTxStatusTaskScheduler = SingleTaskScheduler() init { trackScreenOpened() - updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -159,17 +152,9 @@ internal class WalletModel @Inject constructor( } } - private fun updateMarketToggle() { - stateHolder.update { - it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled) - } - } - private fun updateYieldSupplyApy() { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { - modelScope.launch(dispatchers.default) { - yieldSupplyApyUpdateUseCase() - } + modelScope.launch(dispatchers.default) { + yieldSupplyApyUpdateUseCase() } } @@ -188,7 +173,6 @@ internal class WalletModel @Inject constructor( override fun onDestroy() { super.onDestroy() - tokenListStore.clear() stateHolder.clear() walletScreenContentLoader.cancelAll() } @@ -267,9 +251,9 @@ internal class WalletModel @Inject constructor( getWalletsUseCase() .conflate() .distinctUntilChanged() - .map { + .map { userWallets -> walletsUpdateActionResolver.resolve( - wallets = it, + wallets = userWallets, currentState = stateHolder.value, ) } @@ -361,7 +345,7 @@ internal class WalletModel @Inject constructor( refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() - needToRefreshWallet && !isBackground -> { + shouldRefreshWallet && !isBackground -> { triggerRefreshWalletQuotes() } } @@ -394,7 +378,6 @@ internal class WalletModel @Inject constructor( * Update state each time a user opens/returns to wallet screen * and every minute while user stays on the main screen */ - if (!tangemPayFeatureToggles.isTangemPayEnabled) return combine( flow = screenLifecycleProvider.isBackgroundState, @@ -433,12 +416,12 @@ internal class WalletModel @Inject constructor( private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) - needToRefreshWallet = true + shouldRefreshWallet = true }.saveIn(refreshWalletJobHolder) } private fun triggerRefreshWalletQuotes() { - needToRefreshWallet = false + shouldRefreshWallet = false val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return modelScope.launch { @@ -469,7 +452,6 @@ internal class WalletModel @Inject constructor( // refresh loader to use actual user wallet walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, isRefresh = true, coroutineScope = modelScope, ) @@ -517,10 +499,9 @@ internal class WalletModel @Inject constructor( } private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) { - action.wallets.forEach { + action.wallets.forEach { userWallet -> walletScreenContentLoader.load( - userWallet = it, - clickIntents = clickIntents, + userWallet = userWallet, coroutineScope = modelScope, isRefresh = true, ) @@ -539,7 +520,6 @@ internal class WalletModel @Inject constructor( walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -566,11 +546,9 @@ internal class WalletModel @Inject constructor( private fun reinitializeNewWallet(action: WalletsUpdateActionResolver.Action.ReinitializeNewWallet) { walletScreenContentLoader.cancel(action.prevWalletId) - tokenListStore.remove(action.prevWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -589,11 +567,9 @@ internal class WalletModel @Inject constructor( private fun reinitializeWallets(action: WalletsUpdateActionResolver.Action.ReinitializeWallets) { action.wallets.forEach { userWallet -> walletScreenContentLoader.cancel(userWallet.walletId) - tokenListStore.remove(userWallet.walletId) walletScreenContentLoader.load( userWallet = userWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -610,39 +586,20 @@ internal class WalletModel @Inject constructor( } private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { - if (accountsFeatureToggles.isFeatureEnabled) { - fetchWalletContent(userWallet = action.selectedWallet) + fetchWalletContent(userWallet = action.selectedWallet) - stateHolder.update( - AddWalletTransformer( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - walletImageResolver = walletImageResolver, - ), - ) - - walletScreenContentLoader.load( + stateHolder.update( + AddWalletTransformer( userWallet = action.selectedWallet, clickIntents = clickIntents, - coroutineScope = modelScope, - ) - } else { - walletScreenContentLoader.load( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - coroutineScope = modelScope, - ) + walletImageResolver = walletImageResolver, + ), + ) - fetchWalletContent(userWallet = action.selectedWallet) - - stateHolder.update( - AddWalletTransformer( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - walletImageResolver = walletImageResolver, - ), - ) - } + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + coroutineScope = modelScope, + ) scrollToWallet(prevIndex = action.prevWalletIndex, newIndex = action.selectedWalletIndex) { stateHolder.update { @@ -655,11 +612,9 @@ internal class WalletModel @Inject constructor( private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) - tokenListStore.remove(action.deletedWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -703,7 +658,6 @@ internal class WalletModel @Inject constructor( walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 1f63082dad..fccfccfd12 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletType import timber.log.Timber import javax.inject.Inject @@ -110,7 +111,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( when (walletState) { is WalletState.MultiCurrency -> { val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } - walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + walletState.type == WalletType.Hot && wallet is UserWallet.Cold } else -> false } @@ -212,7 +213,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId } ?: return@filter false wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency && - previousState.type == WalletState.MultiCurrency.WalletType.Hot + previousState.type == WalletType.Hot } return Action.ReinitializeWallets(selectedWallet, walletsToUpdate) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1638e3ed00..5a779f8e1d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -27,7 +27,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer -import com.tangem.features.tangempay.TangemPayFeatureToggles import kotlinx.coroutines.launch import javax.inject.Inject @@ -64,7 +63,6 @@ internal interface TangemPayIntents { @ModelScoped internal class TangemPayClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, - private val featureToggles: TangemPayFeatureToggles, private val onboardingRepository: OnboardingRepository, private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase, private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase, @@ -77,9 +75,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( override suspend fun onPullToRefresh() { val userWalletId = stateHolder.getSelectedWalletId() - if (!featureToggles.isTangemPayEnabled || - !onboardingRepository.isTangemPayInitialDataProduced(userWalletId) - ) { + if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { return } tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) 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 41161fa227..8f03d6ae84 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 @@ -30,7 +30,7 @@ internal class WalletClickIntents @Inject constructor( private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor, - private val stateHolder: WalletStateController, + private val stateController: WalletStateController, private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, @@ -62,7 +62,7 @@ internal class WalletClickIntents @Inject constructor( fun onWalletChange(index: Int, onlyState: Boolean) { if (onlyState) { - stateHolder.update { it.copy(selectedWalletIndex = index) } + stateController.update { it.copy(selectedWalletIndex = index) } return } @@ -70,27 +70,23 @@ internal class WalletClickIntents @Inject constructor( launch { neverToShowWalletsScrollPreview() } val maybeUserWallet = selectWalletUseCase( - userWalletId = stateHolder.value.wallets[index].walletCardState.id, + userWalletId = stateController.value.wallets[index].walletCardState.id, ) - stateHolder.update { it.copy(selectedWalletIndex = index) } + stateController.update { it.copy(selectedWalletIndex = index) } - maybeUserWallet.onRight { - if (!it.isLocked) { - launch { walletContentFetcher(userWalletId = it.walletId) } + maybeUserWallet.onRight { userWallet -> + if (!userWallet.isLocked) { + launch { walletContentFetcher(userWalletId = userWallet.walletId) } } - walletScreenContentLoader.load( - userWallet = it, - clickIntents = this@WalletClickIntents, - coroutineScope = modelScope, - ) + walletScreenContentLoader.load(userWallet = userWallet, coroutineScope = modelScope) } } } fun onRefreshSwipe(showRefreshState: Boolean) { - when (stateHolder.getSelectedWallet()) { + when (stateController.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { refreshMultiCurrencyContent(showRefreshState) } @@ -111,7 +107,7 @@ internal class WalletClickIntents @Inject constructor( private fun refreshMultiCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) @@ -126,7 +122,7 @@ internal class WalletClickIntents @Inject constructor( } .awaitAll() - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) } @@ -137,7 +133,7 @@ internal class WalletClickIntents @Inject constructor( private fun refreshSingleCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) @@ -147,12 +143,11 @@ internal class WalletClickIntents @Inject constructor( onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, - clickIntents = this@WalletClickIntents, isRefresh = true, coroutineScope = modelScope, ) - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index e6baa06b02..25d595780c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -11,8 +11,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference @@ -20,7 +22,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.DialogMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -36,13 +37,12 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent @@ -63,7 +63,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -136,19 +135,17 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val appRouter: AppRouter, private val rampStateManager: RampStateManager, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val receiveAddressesFactory: ReceiveAddressesFactory, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val uiMessageSender: UiMessageSender, @@ -289,23 +286,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.io) { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - currency = cryptoCurrencyStatus.currency, - ) - .map { it.account.accountId } - .getOrNull() + val accountId = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWalletId, + currency = cryptoCurrencyStatus.currency, + ) + .map { it.account.accountId } + .getOrNull() - if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}") - return@launch - } - - manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) - } else { - removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) + if (accountId == null) { + Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}") + return@launch } + + manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) .fold( ifLeft = { walletEventSender.send( @@ -335,12 +328,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { modelScope.launch(dispatchers.main) { - reduxStateHolder.dispatch( - action = TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyStatus, - appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } } @@ -472,9 +466,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return - val tokenListState = selectedWallet.tokensListState - - when (tokenListState) { + when (val tokenListState = selectedWallet.tokensListState) { is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability( tokenCount = tokenListState.items.count { it is TokensListItemUM.Token }, ) @@ -663,8 +655,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } private suspend fun needShowYieldSupplyWarning(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) + return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt index 4119478f5a..7303bfbb6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.account import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -9,7 +8,6 @@ import javax.inject.Inject @ModelScoped internal class AccountDependencies @Inject constructor( - val accountsFeatureToggles: AccountsFeatureToggles, val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, val expandedAccountsHolder: ExpandedAccountsHolder, val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index ba99940e19..1c48f40af5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,24 +1,11 @@ package com.tangem.feature.wallet.presentation.common -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.wallet.state.model.* -import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import java.util.UUID @Suppress("LargeClass") internal object WalletPreviewData { @@ -68,135 +55,6 @@ internal object WalletPreviewData { ) } - private val tokenItemDragState by lazy { - TokenItemState.Draggable( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_polygon_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), - ) - } - - private const val networksSize = 10 - private const val tokensSize = 3 - private val draggableItems: PersistentList by lazy { - List(networksSize) { it } - .flatMap { index -> - val lastNetworkIndex = networksSize - 1 - val lastTokenIndex = tokensSize - 1 - val networkNumber = index + 1 - - val group = DraggableItem.GroupHeader( - id = networkNumber, - networkName = "$networkNumber", - - roundingMode = when (index) { - 0 -> DraggableItem.RoundingMode.Top() - lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - accountId = "account_$networkNumber", - ) - - val tokens: MutableList = mutableListOf() - repeat(times = tokensSize) { i -> - val tokenNumber = i + 1 - tokens.add( - DraggableItem.Token( - tokenItemState = tokenItemDragState.copy( - id = "${group.id}_token_$tokenNumber", - titleState = TokenItemState.TitleState.Content( - text = stringReference(value = "Token $tokenNumber from $networkNumber network"), - ), - ), - groupId = group.id, - accountId = "account_$networkNumber", - roundingMode = when { - i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - ), - ) - } - - val divider = DraggableItem.Placeholder( - id = "divider_$networkNumber", - accountId = "account_$networkNumber", - ) - - buildList { - add(group) - addAll(tokens) - if (index != lastNetworkIndex) { - add(divider) - } - } - } - .toPersistentList() - } - - private val draggableTokens by lazy { - draggableItems - .filterIsInstance() - .toMutableList() - .also { - it[0] = it[0].copy(roundingMode = DraggableItem.RoundingMode.Top()) - } - .toPersistentList() - } - - val groupedOrganizeTokensState by lazy { - OrganizeTokensState( - onBackClick = {}, - itemsState = OrganizeTokensListState.GroupedByNetwork( - items = draggableItems, - ), - tokenListUM = OrganizeTokensListUM.EmptyList, - header = OrganizeTokensState.HeaderConfig( - onSortClick = {}, - onGroupClick = {}, - ), - dndConfig = OrganizeTokensState.DragAndDropConfig( - onItemDragged = { _, _ -> }, - onItemDragStart = {}, - canDragItemOver = { _, _ -> false }, - onItemDragEnd = {}, - ), - actions = OrganizeTokensState.ActionsConfig( - onApplyClick = {}, - onCancelClick = {}, - ), - scrollListToTop = consumedEvent(), - isBalanceHidden = true, - ) - } - - val organizeTokensState by lazy { - groupedOrganizeTokensState.copy( - itemsState = OrganizeTokensListState.Ungrouped( - items = draggableTokens, - ), - ) - } - - val bottomSheet by lazy { - TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = WalletBottomSheetConfig.UnlockWallets( - onUnlockClick = {}, - onScanClick = {}, - ), - ) - } - val actionsBottomSheet = ActionsBottomSheetConfig( actions = listOf( TokenActionButtonConfig( 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 59f9ca6314..fb85ffc73d 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 @@ -186,7 +186,7 @@ internal object WalletScreenPreviewData { onItemClick = { }, ), tangemPayState = TangemPayState.Empty, - type = WalletState.MultiCurrency.WalletType.Cold, + type = WalletType.Cold, ) } @@ -218,12 +218,12 @@ internal object WalletScreenPreviewData { singleWalletLockedState, multiWalletState, ), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, - isNewMarketEnabled = false, ) internal val accountScreenState = diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt deleted file mode 100644 index f1eb7915cf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens - -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverterV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 -import com.tangem.utils.Provider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -internal class OrganizeTokensStateHolder( - private val intents: OrganizeTokensIntents, - private val dragAndDropIntents: DragAndDropIntents, - private val dragAndDropAdapterV2: DragAndDropAdapterV2, - private val appCurrencyProvider: Provider, - private val accountsFeatureToggles: AccountsFeatureToggles, -) { - - private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) - - private val tokenListConverter by lazy { - val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) - val itemsConverter = TokenListToListStateConverter( - tokensConverter = tokensConverter, - groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), - ) - - TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter) - } - - private val inProgressStateConverter by lazy { InProgressStateConverter() } - - private val tokenListErrorConverter by lazy { - TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) - } - - private val tokenListSortingErrorConverter by lazy { - TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) - } - - val stateFlow: StateFlow = stateFlowInternal - - fun updateStateWithTokenList(tokenList: TokenList) { - updateState { tokenListConverter.convert(tokenList) } - } - - fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { - updateState { - TokenListToStateConverterV2( - accountStatusList = accountStatusList, - isAccountsMode = isAccountsModeEnabled, - appCurrency = appCurrencyProvider(), - ).transform(this) - } - } - - fun updateStateAfterTokenListSorting(tokenList: TokenList) { - updateState { - tokenListConverter.convert(tokenList).copy( - scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), - ) - } - } - - fun updateStateAfterTokenListSortingV2(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { - updateState { - TokenListToStateConverterV2( - accountStatusList = accountStatusList, - isAccountsMode = isAccountsModeEnabled, - appCurrency = appCurrencyProvider(), - ).transform(this).copy( - scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), - ) - } - } - - fun updateStateToDisplayProgress() { - updateState { inProgressStateConverter.convert(value = this) } - } - - fun updateStateToHideProgress() { - updateState { inProgressStateConverter.convertBack(value = this) } - } - - fun updateStateWithManualSortingV2(tokenListUM: OrganizeTokensListUM) { - updateState { copy(tokenListUM = tokenListUM) } - } - - fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { - updateState { copy(itemsState = itemsState) } - } - - fun disableSortingByBalance() { - updateState { copy(header = header.copy(isSortedByBalance = false)) } - } - - fun updateHiddenState(isBalanceHidden: Boolean) { - updateState { copy(isBalanceHidden = isBalanceHidden) } - } - - fun updateStateWithError(error: TokenListError) { - updateState { tokenListErrorConverter.convert(error) } - } - - fun updateStateWithError(error: TokenListSortingError) { - updateState { tokenListSortingErrorConverter.convert(error) } - } - - private fun getInitialState(): OrganizeTokensState { - return OrganizeTokensState( - onBackClick = intents::onBackClick, - itemsState = OrganizeTokensListState.Empty, - tokenListUM = OrganizeTokensListUM.EmptyList, - header = OrganizeTokensState.HeaderConfig( - onSortClick = intents::onSortClick, - onGroupClick = intents::onGroupClick, - ), - actions = OrganizeTokensState.ActionsConfig( - onApplyClick = intents::onApplyClick, - onCancelClick = intents::onCancelClick, - ), - dndConfig = if (accountsFeatureToggles.isFeatureEnabled) { - OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropAdapterV2::onItemDragged, - onItemDragStart = dragAndDropAdapterV2::onItemDraggingStart, - onItemDragEnd = dragAndDropAdapterV2::onItemDraggingEnd, - canDragItemOver = dragAndDropAdapterV2::canDragItemOver, - ) - } else { - OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropIntents::onItemDragged, - onItemDragStart = dragAndDropIntents::onItemDraggingStart, - onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, - canDragItemOver = dragAndDropIntents::canDragItemOver, - ) - }, - scrollListToTop = consumedEvent(), - isBalanceHidden = true, - ) - } - - private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { - stateFlowInternal.update(block) - } - - private fun consumeScrollListToTopEvent() { - updateState { copy(scrollListToTop = consumedEvent()) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt deleted file mode 100644 index 770ada3985..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils - -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.status.model.AccountCryptoCurrencies -import com.tangem.domain.models.account.filterCryptoPortfolio -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM - -internal class CryptoCurrenciesIdsResolver { - - fun resolve(listState: OrganizeTokensListState, tokenList: TokenList?): List { - val draggableTokens = when (listState) { - is OrganizeTokensListState.Empty -> return emptyList() - is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance() - is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance() - } - val currenciesStatuses = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty, - null, - -> return emptyList() - } - - return draggableTokens.mapNotNull { draggableToken -> - val currencyStatus = currenciesStatuses.firstOrNull { - it.currency.id.value == draggableToken.id - } - - currencyStatus?.currency?.id - } - } - - @Suppress("UseOrEmpty") - fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { - val draggableTokens = when (tokensListUM) { - OrganizeTokensListUM.EmptyList -> return emptyMap() - is OrganizeTokensListUM.AccountList, - is OrganizeTokensListUM.TokensList, - -> tokensListUM.items.filterIsInstance() - } - - return accountStatusList?.accountStatuses - ?.filterCryptoPortfolio() - ?.filter { it.tokenList != TokenList.Empty } - ?.associate { accountStatus -> - val currencies = accountStatus.flattenCurrencies() - accountStatus.account to draggableTokens - .asSequence() - .filter { it.accountId == accountStatus.account.accountId.value } - .mapNotNull { sortedToken -> - currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency - } - .toList() - } ?: emptyMap() - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt deleted file mode 100644 index d67847aa0a..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common - -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList - -internal inline fun OrganizeTokensListState.updateItems( - update: (PersistentList) -> List, -): OrganizeTokensListState { - val updatedItems = update(items).toPersistentList() - - return when (this) { - is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems) - is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems) - is OrganizeTokensListState.Empty -> this - } -} - -internal inline fun OrganizeTokensListUM.updateItems( - update: (PersistentList) -> List, -): OrganizeTokensListUM { - val updatedItems = update(items).toPersistentList() - - return when (this) { - is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) - is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) - OrganizeTokensListUM.EmptyList -> this - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt deleted file mode 100644 index d561e3b262..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter - -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class TokenListToStateConverter( - private val currentState: Provider, - private val itemsConverter: TokenListToListStateConverter, -) : Converter { - - override fun convert(value: TokenList): OrganizeTokensState { - val state = currentState() - val itemsState = itemsConverter.convert(value) - - return state.copy( - itemsState = itemsState, - header = state.header.copy( - isEnabled = itemsState !is OrganizeTokensListState.Empty, - isSortedByBalance = value.sortedBy == TokensSortType.BALANCE, - isGrouped = value is TokenList.GroupedByNetwork, - ), - actions = state.actions.copy( - canApply = itemsState !is OrganizeTokensListState.Empty, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt deleted file mode 100644 index 79ea585f1d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.common.getTotalWithRewardsStakingBalance -import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants -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.CryptoCurrencyStatus -import com.tangem.domain.models.staking.StakingBalance -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.orZero -import java.math.BigDecimal - -internal class CryptoCurrencyToDraggableItemConverter( - private val appCurrencyProvider: Provider, -) : Converter { - - private val iconStateConverter = CryptoCurrencyToIconStateConverter() - - override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { - return createDraggableToken(value, appCurrencyProvider()) - } - - override fun convertList(input: Collection): List { - val appCurrency = appCurrencyProvider() - - return input.map { createDraggableToken(it, appCurrency) } - } - - private fun createDraggableToken( - currencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ): DraggableItem.Token { - return DraggableItem.Token( - tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.network), - ) - } - - private fun createTokenItemState( - currencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ): TokenItemState.Draggable { - val currency = currencyStatus.currency - - return TokenItemState.Draggable( - id = getTokenItemId(currency.id), - iconState = iconStateConverter.convert(currencyStatus), - titleState = TokenItemState.TitleState.Content(text = stringReference(currency.name)), - subtitle2State = if (currencyStatus.value.isError) { - TokenItemState.Subtitle2State.Unreachable - } else { - TokenItemState.Subtitle2State.TextContent(text = getFormattedFiatAmount(currencyStatus, appCurrency)) - }, - ) - } - - private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data - val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) - ?.multiply(fiatRate).orZero() - - val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt deleted file mode 100644 index 73ecfe3f79..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.utils.converter.Converter - -internal class NetworkGroupToDraggableItemsConverter( - private val itemConverter: CryptoCurrencyToDraggableItemConverter, -) : Converter> { - - override fun convert(value: NetworkGroup): List { - return buildList { - add(createGroupHeader(value)) - addAll(createTokens(value)) - } - } - - override fun convertList(input: Collection): List> { - val lastItemIndex = input.size - 1 - - return input.mapIndexed { index, networkGroup -> - convert(networkGroup).toMutableList() - .also { mutableGroup -> - if (index != lastItemIndex) { - mutableGroup.add(getGroupPlaceholder(index)) - } - } - } - } - - private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader( - id = getGroupHeaderId(group.network), - networkName = group.network.name, - ) - - private fun createTokens(group: NetworkGroup): List { - return itemConverter.convertList(group.currencies) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt deleted file mode 100644 index 770d63c0f4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList - -internal class TokenListToListStateConverter( - private val groupsConverter: NetworkGroupToDraggableItemsConverter, - private val tokensConverter: CryptoCurrencyToDraggableItemConverter, -) : Converter { - - override fun convert(value: TokenList): OrganizeTokensListState { - return when (value) { - is TokenList.GroupedByNetwork -> createListState(value) - is TokenList.Ungrouped -> createListState(value) - is TokenList.Empty -> createEmptyListState() - } - } - - private fun createListState(tokenList: TokenList.GroupedByNetwork): OrganizeTokensListState.GroupedByNetwork { - return OrganizeTokensListState.GroupedByNetwork( - items = groupsConverter.convertList(tokenList.groups) - .flatten() - .uniteItems() - .toPersistentList(), - ) - } - - @Suppress("UNCHECKED_CAST") // Erased type - private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped { - return OrganizeTokensListState.Ungrouped( - items = tokensConverter.convertList(tokenList.currencies) - .uniteItems() - .toPersistentList() as PersistentList, - ) - } - - private fun createEmptyListState(): OrganizeTokensListState.Empty { - return OrganizeTokensListState.Empty - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt deleted file mode 100644 index 7158aa1260..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd - -import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems -import com.tangem.utils.Provider -import kotlinx.collections.immutable.mutate -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull -import org.burnoutcrew.reorderable.ItemPosition - -internal class DragAndDropAdapter( - private val listStateProvider: Provider, -) : DragAndDropIntents { - - private val draggableGroupsOperations = DraggableGroupsOperations() - - private val externalListState: OrganizeTokensListState - get() = listStateProvider.invoke() - - private val dragAndDropUpdatesInternal: MutableStateFlow = MutableStateFlow(value = null) - - private var draggingItem: DraggableItem? = null - private var draggingListState: OrganizeTokensListState? = null - - val dragAndDropUpdates: Flow - get() = dragAndDropUpdatesInternal.filterNotNull() - - override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { - val items = when (val listState = externalListState) { - is OrganizeTokensListState.GroupedByNetwork -> listState.items - is OrganizeTokensListState.Empty, - is OrganizeTokensListState.Ungrouped, - -> return true // If ungrouped then item can be moved anywhere - } - - val (dragOverItem, draggingItem) = findItemsToMove( - items = items, - moveOverItemKey = dragOver.key, - movedItemKey = dragging.key, - ) - - if (dragOverItem == null || draggingItem == null) { - return false - } - - return when (draggingItem) { - is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) - is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> false - } - } - - override fun onItemDraggingStart(item: DraggableItem) { - if (draggingItem != null) return - draggingItem = item - - updateListState(DragOperation.Type.Start) { - when (item) { - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> items - is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) - is DraggableItem.Token -> when (this) { - is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item) - is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item) - is OrganizeTokensListState.Empty -> items - } - } - } - - draggingListState = externalListState - } - - override fun onItemDraggingEnd() { - val draggingItem = draggingItem ?: return - - updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { - when (draggingItem) { - is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) - is DraggableItem.Token -> items.uniteItems() - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> items - } - } - - this.draggingItem = null - } - - override fun onItemDragged(from: ItemPosition, to: ItemPosition) { - updateListState(DragOperation.Type.Dragged) { - items.mutate { - it.add(to.index, it.removeAt(from.index)) - } - } - } - - private fun updateListState(type: DragOperation.Type, block: OrganizeTokensListState.() -> List) { - val updatedState = externalListState.updateItems { block(externalListState) } - - dragAndDropUpdatesInternal.value = DragOperation(type, updatedState) - } - - private fun findItemsToMove( - items: List, - moveOverItemKey: Any?, - movedItemKey: Any?, - ): Pair { - var moveOverItem: DraggableItem? = null - var movedItem: DraggableItem? = null - - for (item in items) { - if (item.id == moveOverItemKey) { - moveOverItem = item - } - if (item.id == movedItemKey) { - movedItem = item - } - if (moveOverItem != null && movedItem != null) { - break - } - } - - return Pair(moveOverItem, movedItem) - } - - private fun checkCanMoveHeaderOver( - moveOverItemPosition: ItemPosition, - moveOverItem: DraggableItem, - lastItemIndex: Int, - ): Boolean { - // Group item can be moved only to group divider or to ages of the items list - return when { - moveOverItemPosition.index == 0 -> true - moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.Placeholder -> true - else -> false - } - } - - private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { - // Token item can be moved only in its group - return when (moveOverItem) { - is DraggableItem.GroupHeader -> false // Token item can not be moved to group item - is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.Portfolio, - is DraggableItem.Placeholder, - -> false - } - } - - private fun checkIsItemsOrderChanged(): Boolean { - fun OrganizeTokensListState?.getItemsIds(): List? = this?.items?.mapNotNull { item -> - if (item is DraggableItem.Placeholder) { - null - } else { - item.id - } - } - - return externalListState.getItemsIds() != draggingListState.getItemsIds() - } - - data class DragOperation( - val type: Type, - val listState: OrganizeTokensListState, - ) { - - sealed class Type { - - data object Start : Type() - - data object Dragged : Type() - - data class End(val isItemsOrderChanged: Boolean) : Type() - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt new file mode 100644 index 0000000000..0d83d630b8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.preview + +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledStringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM + +internal object WalletBalancePreview { + + val content: WalletBalanceUM.Content = WalletBalanceUM.Content( + id = UserWalletId("0"), + name = "My Wallet", + balance = combinedReference( + stringReference("1,234"), + styledStringReference( + ".56", + { + TangemTheme.typography2.headingRegular28.toSpanStyle() + }, + ), + stringReference(" $"), + ), + isBalanceFlickering = false, + isZeroBalance = false, + ) + + val loading: WalletBalanceUM.Loading = WalletBalanceUM.Loading( + id = UserWalletId("1"), + name = "My Wallet", + ) + + val error: WalletBalanceUM.Error = WalletBalanceUM.Error( + id = UserWalletId("2"), + name = "My Wallet", + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt index 5e65dfb36a..a54705233c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.tangempay.TangemPayAnalyticsEvents @@ -29,7 +29,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor( // ignore cancelled state on analytics customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore kyc not approved state on analytics - customerInfo.info.kycStatus != CustomerInfo.KycStatus.APPROVED -> return + customerInfo.info.kycStatus != KycStatus.APPROVED -> return cardInfo != null && productInstance != null -> return else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed() } 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 341c2c6726..41fe676459 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 @@ -8,9 +8,8 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.* import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen.* -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.* -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import javax.inject.Inject @@ -34,10 +33,29 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( } } + fun send(displayedWalletUM: WalletUM?, newNotifications: List) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val notificationsDiff = newNotifications.filter { it !in totalNotifications } + + val eventsToSend = getEvents2(notificationsDiff) + + eventsToSend.forEach { event -> + analyticsEventHandler.send(event) + } + } + private fun getEvents(warnings: List): Set { return warnings.mapNotNullTo(mutableSetOf(), ::getEvent) } + private fun getEvents2(notifications: List): Set { + return notifications.mapNotNullTo(mutableSetOf(), ::getEvent2) + } + @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { @@ -106,4 +124,53 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.UpgradeHotWalletPromo -> null } } + + @Suppress("CyclomaticComplexMethod") + private fun getEvent2(notificationUM: WalletNotificationUM): AnalyticsEvent? { + return when (notificationUM) { + WalletNotificationUM.DevCard -> DevelopmentCard() + WalletNotificationUM.FailedCardValidation -> ProductSampleCard() + is WalletNotificationUM.MissingBackup -> BackupYourWallet() + is WalletNotificationUM.NumberOfSignedHashesIncorrect -> CardSignedTransactions() + WalletNotificationUM.TestnetCard -> TestnetCard() + WalletNotificationUM.DemoCard -> DemoCard() + is WalletNotificationUM.MissingAddresses -> MissingAddresses() + is WalletNotificationUM.RateApp -> HowDoYouLikeTangem() + is WalletNotificationUM.BackupError -> BackupError() + is WalletNotificationUM.NoteMigration -> NotePromo() + is WalletNotificationUM.OnePlusOnePromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.OnePlusOne, + ) + is WalletNotificationUM.YieldPromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + ) + is WalletNotificationUM.FinishWalletActivation -> { + val activationState = if (notificationUM.isBackupExists) { + NoticeFinishActivation.ActivationState.Unfinished + } else { + NoticeFinishActivation.ActivationState.NotStarted + } + val balanceState = when (notificationUM.type) { + WalletNotificationType.Warning -> AnalyticsParam.EmptyFull.Full + else -> AnalyticsParam.EmptyFull.Empty + } + NoticeFinishActivation( + activationState = activationState, + balanceState = balanceState, + ) + } + is WalletNotificationUM.SeedPhraseNotification -> NoticeSeedPhraseSupport() + is WalletNotificationUM.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() + is WalletNotificationUM.PushNotifications -> PushBanner() + is WalletNotificationUM.UnlockWallets, + is WalletNotificationUM.NoAccount, + is WalletNotificationUM.LowSignatures, + WalletNotificationUM.SomeNetworksUnreachable, + is WalletNotificationUM.UsedOutdatedData, + is WalletNotificationUM.CloreMigration, + -> null + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index f52290e6d2..fdcdc567b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -4,12 +4,8 @@ import com.tangem.common.routing.AppRoute.WalletBackup import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.icon -import com.tangem.core.ui.components.bottomsheets.message.infoBlock -import com.tangem.core.ui.components.bottomsheets.message.onClick -import com.tangem.core.ui.components.bottomsheets.message.primaryButton -import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet @@ -19,7 +15,9 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -71,6 +69,44 @@ internal class WalletWarningsSingleEventSender @Inject constructor( } } + suspend fun send( + userWalletId: UserWalletId, + displayedWalletUM: WalletUM?, + newNotifications: List, + ) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val events = newNotifications.filter { it !in totalNotifications } + + // We must show activation bs only for the first seen wallet when open the app (if need, see conditions below), + // so we keep this wallet id and use for future checks, ignore other wallets during the app session. + if (isActivationBottomSheetShown.isEmpty()) { + isActivationBottomSheetShown[userWalletId] = false + } + + events.forEach { event -> + when (event) { + is WalletNotificationUM.SeedPhraseNotification -> { + seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + } + is WalletNotificationUM.FinishWalletActivation -> { + // We check that map contains the first seen wallet (will return null instead false/true otherwise) + // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) + if (isActivationBottomSheetShown[userWalletId] == false) { + if (event.messageEffect == TangemMessageEffect.Warning && event.isBackupExists.not()) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown[userWalletId] = true + } + } + else -> Unit + } + } + } + private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return if (userWallet !is UserWallet.Hot) return 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 a243ddef48..d08cbc8cb1 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 @@ -11,7 +11,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -52,7 +51,6 @@ import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @ModelScoped internal class GetMultiWalletWarningsFactory @Inject constructor( - private val tokenListStore: MultiWalletTokenListStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, @@ -71,30 +69,15 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - val accountStatusList by lazy { + val accountStatusListFlow by lazy { val params = SingleAccountStatusListProducer.Params(userWallet.walletId) accountDependencies.singleAccountStatusListSupplier(params) .map { it.totalFiatBalance to it.flattenCurrencies() } .map { Lce.Content(it) } } - fun tokenListFlow(): LceFlow>> { - return if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { - accountStatusList - } else { - runCatching { tokenListStore.getOrThrow(userWallet.walletId) } - .map { result -> result.map { lce -> lce.map { it.totalFiatBalance to it.flattenCurrencies() } } } - .getOrNull() - // in case of runtime change ft in tester menu - ?: accountStatusList - } - } - - // val params = SingleAccountStatusListProducer.Params(userWallet.walletId) - // val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) return combine( - // todo account just use it, after delete accountsFeatureToggles - // accountStatusListFlow, + accountStatusListFlow, isReadyToShowRateAppUseCase().distinctUntilChanged(), isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), @@ -110,7 +93,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) .distinctUntilChanged(), ) { array -> array } - .combine(tokenListFlow()) { array, any: Any? -> arrayOf(any).plus(elements = array) } .map { array -> val lceTokens = array[0] as Lce>> val totalFiatBalance = lceTokens.map { it.first } @@ -149,9 +131,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addYieldPromoNotification(clickIntents, shouldShowYieldPromo) - addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) - addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) + addWarningNotifications( + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) addPushReminderNotification( clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index c7fe144bd9..30c506aa5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either import arrow.core.right import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.producer.SingleAccountStatusProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier import com.tangem.domain.card.CardTypesResolver @@ -15,7 +14,6 @@ import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase @@ -30,8 +28,6 @@ import javax.inject.Inject @ModelScoped @Suppress("LongParameterList") internal class GetSingleWalletWarningsFactory @Inject constructor( - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val singleAccountStatusSupplier: SingleAccountStatusSupplier, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, @@ -40,7 +36,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, ) { - private var readyForRateAppNotification = false + private var isReadyForRateAppNotification = false fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { if (userWallet !is UserWallet.Cold) { @@ -54,7 +50,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = getWalletsUseCase().conflate(), ) { maybePrimaryCurrencyStatus, isReadyToShowRating, isNeedToBackup, userWallets -> - readyForRateAppNotification = true + isReadyForRateAppNotification = true buildList { addUsedOutdatedDataNotification(maybePrimaryCurrencyStatus) @@ -120,8 +116,8 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( cardTypesResolver: CardTypesResolver, clickIntents: WalletClickIntents, ) { - val userHasWalletOrWallet2 = userWallets.filterIsInstance().any { - val typesResolver = it.scanResponse.cardTypesResolver + val hasWalletOrWallet2 = userWallets.filterIsInstance().any { coldWallet -> + val typesResolver = coldWallet.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } @@ -129,7 +125,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( element = WalletNotification.NoteMigration( onClick = { clickIntents.onNoteMigrationButtonClick(NOTE_MIGRATION_URL) }, ), - condition = cardTypesResolver.isTangemNote() && !userHasWalletOrWallet2, + condition = cardTypesResolver.isTangemNote() && !hasWalletOrWallet2, ) addIf( @@ -191,8 +187,8 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( selectedWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus?, ): Boolean { - return cryptoCurrencyStatus?.currency?.network?.let { - hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) + return cryptoCurrencyStatus?.currency?.network?.let { network -> + hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) .conflate() .distinctUntilChanged() .firstOrNull() @@ -209,7 +205,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( onDislikeClick = clickIntents::onDislikeAppClick, onCloseClick = clickIntents::onCloseRateAppWarningClick, ), - condition = isReadyToShowRating && readyForRateAppNotification, + condition = isReadyToShowRating && isReadyForRateAppNotification, ) } @@ -219,7 +215,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( element is WalletNotification.Warning || element is WalletNotification.NoteMigration ) { - readyForRateAppNotification = false + isReadyForRateAppNotification = false } element @@ -229,16 +225,12 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private fun getPrimaryCurrencyStatusFlow( userWallet: UserWallet, ): Flow> { - return if (accountsFeatureToggles.isFeatureEnabled) { - getAccountStatusFlow(userWallet).mapNotNull { accountStatus -> - accountStatus.flattenCurrencies().firstOrNull() - } - .distinctUntilChanged() - .conflate() - .map { it.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) + return getAccountStatusFlow(userWallet).mapNotNull { accountStatus -> + accountStatus.flattenCurrencies().firstOrNull() } + .distinctUntilChanged() + .conflate() + .map { it.right() } } private fun getAccountStatusFlow(userWallet: UserWallet): Flow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt new file mode 100644 index 0000000000..8e0d92d545 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt @@ -0,0 +1,131 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.TangemSiteUrlBuilder +import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.promo.ShouldShowPromoWalletUseCase +import com.tangem.domain.promo.models.PromoId +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.utils.extensions.addIf +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are not critical and can be stacked with each other. + */ +@ModelScoped +internal class GetWalletNotificationsCarouselFactory @Inject constructor( + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val notificationsRepository: NotificationsRepository, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + return combine( + flow = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) + .distinctUntilChanged(), + flow2 = notificationsRepository.getShouldShowNotification( + NotificationId.EnablePushesReminderNotification.key, + ).distinctUntilChanged(), + flow3 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne) + .distinctUntilChanged(), + flow4 = isReadyToShowRateAppUseCase().distinctUntilChanged(), + flow5 = getWalletsUseCase().conflate(), + ) { showYieldPromo, showPushesNotification, showOnePlusOnePromo, showRateAppPromo, wallets -> + + buildList { + addNoteMigrationNotification(userWallet, wallets, clickIntents) + addRateAppNotification(showRateAppPromo, clickIntents) + addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo) + addYieldPromoNotification(clickIntents, showYieldPromo) + + addPushNotification( + shouldShow = showPushesNotification, + isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addRateAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(isReadyToShowRating) { + WalletNotificationUM.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ) + } + } + + private fun MutableList.addYieldPromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.YieldPromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) }, + onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() }, + ) + } + } + + private fun MutableList.addOnePlusOnePromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.OnePlusOnePromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) }, + onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) }, + ) + } + } + + private fun MutableList.addNoteMigrationNotification( + userWallet: UserWallet, + userWallets: List, + clickIntents: WalletClickIntents, + ) { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val isUserHasWalletOrWallet2 = userWallets.filterIsInstance().any { wallet -> + val typesResolver = wallet.scanResponse.cardTypesResolver + typesResolver.isTangemWallet() || typesResolver.isWallet2() + } + + addIf(cardTypesResolver != null && cardTypesResolver.isTangemNote() && !isUserHasWalletOrWallet2) { + WalletNotificationUM.NoteMigration( + onClick = { clickIntents.onNoteMigrationButtonClick(TangemSiteUrlBuilder.NOTE_MIGRATION_URL) }, + ) + } + } + + private fun MutableList.addPushNotification( + shouldShow: Boolean, + isPushesAllowed: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(shouldShow && !isPushesAllowed) { + WalletNotificationUM.PushNotifications( + onCloseClick = clickIntents::onDenyPermissions, + onEnabledClick = clickIntents::onAllowPermissions, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt new file mode 100644 index 0000000000..c900381523 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt @@ -0,0 +1,326 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.account.AccountDependencies +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are critical and should be shown separately from each other. + */ +@Suppress("LongParameterList") +@ModelScoped +internal class GetWalletWarningsFactory @Inject constructor( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val backupValidator: BackupValidator, + private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, + private val accountDependencies: AccountDependencies, + private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) + + return combine( + flow = accountStatusListFlow, + flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), + flow3 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), + flow4 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), + ) { accountList, isNeedToBackup, seedPhraseIssueStatus, shouldAccessCodeSkipped -> + val totalFiatBalance = accountList.totalFiatBalance + val flattenCurrencies = accountList.flattenCurrencies() + + buildList { + addUsedOutdatedDataNotification(totalFiatBalance) + + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + + addFinishWalletActivationNotification( + userWallet = userWallet, + totalFiatBalance = totalFiatBalance, + clickIntents = clickIntents, + shouldAccessCodeSkipped = shouldAccessCodeSkipped, + ) + + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) + + addWarningNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { + addIf( + element = WalletNotificationUM.UsedOutdatedData, + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, + ) + } + + private fun MutableList.addCriticalNotifications( + userWallet: UserWallet, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + if (userWallet !is UserWallet.Cold) { + return + } + + addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + addIf( + element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() }, + condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, + ) + + addIf( + element = WalletNotificationUM.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotificationUM.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotificationUM.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.DemoCard, + condition = cardTypesResolver != null && isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(userWallet, flattenCurrencies, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + userWallet: UserWallet, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val currencies = flattenCurrencies.getMissingAddressCurrencies().ifEmpty { return } + + addIf( + element = WalletNotificationUM.MissingAddresses( + tangemIcon = walletInterationIcon(userWallet), + missingAddressesCount = currencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies) + }, + ), + condition = currencies.isNotEmpty(), + ) + } + + private fun List.getMissingAddressCurrencies(): List { + return this + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.MissingBackup( + onClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotificationUM.TestnetCard, + condition = cardTypesResolver?.isTestCard() == true, + ) + + addIf( + element = WalletNotificationUM.SomeNetworksUnreachable, + condition = flattenCurrencies.hasUnreachableNetworks(), + ) + + addCloreMigrationNotification(flattenCurrencies, clickIntents) + + addNoAccountWarning(cryptoCurrencyStatus = flattenCurrencies.firstOrNull()) + + addIf( + element = WalletNotificationUM.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick, + ), + condition = hasSignedHashes(userWallet, flattenCurrencies.firstOrNull()), + ) + } + + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount + if (noAccountStatus != null) { + add( + element = WalletNotificationUM.NoAccount( + network = cryptoCurrencyStatus.currency.name, + amount = noAccountStatus.amountToCreateAccount.toString(), + symbol = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + } + + private fun MutableList.addCloreMigrationNotification( + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + add( + WalletNotificationUM.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } + } + + private fun List.hasUnreachableNetworks(): Boolean { + return any { it.value is CryptoCurrencyStatus.Unreachable } + } + + private fun MutableList.addFinishWalletActivationNotification( + userWallet: UserWallet, + totalFiatBalance: TotalFiatBalance, + clickIntents: WalletClickIntents, + shouldAccessCodeSkipped: Boolean, + ) { + if (userWallet !is UserWallet.Hot) return + + val isBackupExists = userWallet.backedUp + val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && + !shouldAccessCodeSkipped + val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired + + val messageEffect = when (totalFiatBalance) { + TotalFiatBalance.Failed, + TotalFiatBalance.Loading, + -> TangemMessageEffect.None + is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) { + TangemMessageEffect.Warning + } else { + TangemMessageEffect.None + } + } + + addIf( + element = WalletNotificationUM.FinishWalletActivation( + messageEffect = messageEffect, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, + isBackupExists = isBackupExists, + ), + condition = shouldShowFinishActivation, + ) + } + + private fun MutableList.addSeedNotificationIfNeeded( + userWallet: UserWallet.Cold, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + val isNotificationAvailable = with(userWallet) { + val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) + val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported + + !isDemo && isWalletWithSeedPhrase + } + + when (seedPhraseIssueStatus) { + SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( + element = WalletNotificationUM.SeedPhraseNotification( + onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, + onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( + element = WalletNotificationUM.SeedPhraseSecondNotification( + onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, + onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.NOT_NEEDED -> Unit + } + } + + private suspend fun hasSignedHashes( + selectedWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + if (selectedWallet !is UserWallet.Cold || !selectedWallet.isMultiCurrency) return false + val network = cryptoCurrencyStatus?.currency?.network ?: return false + + return hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) + .conflate() + .distinctUntilChanged() + .firstOrNull() == true + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt deleted file mode 100644 index 1aa129675c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.error.TokenListError -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.shareIn -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject - -@ModelScoped -internal class MultiWalletTokenListStore @Inject constructor( - private val getTokenListUseCase: GetTokenListUseCase, -) { - - private val flows: ConcurrentHashMap> by lazy { - ConcurrentHashMap() - } - - fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) { - if (flows[userWalletId] != null) { - Timber.d("Flow with token list for $userWalletId already exists") - return - } - - coroutineScope.ensureActive() - - flows[userWalletId] = getTokenListUseCase - .launch(userWalletId) - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - - Timber.d("Flow with token list for $userWalletId created") - } - - fun getOrThrow(userWalletId: UserWalletId): LceFlow { - return requireNotNull(flows[userWalletId]) { - "Flow with token list for $userWalletId doesn't exist" - } - } - - fun remove(userWalletId: UserWalletId) { - flows.remove(userWalletId) - - Timber.d("Flow with token list for $userWalletId removed") - } - - fun clear() { - flows.clear() - - Timber.d("All flows with token list cleared") - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt index 46e14c7693..10de4aeeb6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -1,7 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveInAndJoin @@ -27,6 +28,7 @@ import javax.inject.Singleton internal class WalletContentFetcher @Inject constructor( private val walletBalanceFetcher: WalletBalanceFetcher, private val dispatchers: CoroutineDispatcherProvider, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, ) { private val fetchingJobMap = ConcurrentHashMap() @@ -64,8 +66,12 @@ internal class WalletContentFetcher @Inject constructor( Timber.d("Start fetching for $userWalletId") val maybeResult = launch { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) + walletBalanceFetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, + ), + ).onLeft(Timber::e) } .saveInAndJoin(jobHolder) 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 4d9bf94d7e..2b4ca41b34 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 @@ -1,52 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.loaders import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.* +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader import javax.inject.Inject @Suppress("LongParameterList") @ModelScoped internal class WalletContentLoaderFactory @Inject constructor( - private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory, - private val multiWalletContentLoaderV2Factory: MultiWalletContentLoaderV2.Factory, - private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory, - private val singleWalletWithTokenContentLoaderV2Factory: SingleWalletWithTokenContentLoaderV2.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory, - private val singleWalletContentLoaderV2Factory: SingleWalletContentLoaderV2.Factory, + private val multiWalletContentLoaderFactory: MultiWalletContentLoader.Factory, + private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoader.Factory, + private val singleWalletContentLoaderFactory: SingleWalletContentLoader.Factory, ) { - fun create( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - isRefresh: Boolean = false, - ): WalletContentLoader? { + fun create(userWallet: UserWallet, isRefresh: Boolean = false): WalletContentLoader? { return when { userWallet.isMultiCurrency -> { - if (accountsFeatureToggles.isFeatureEnabled) { - multiWalletContentLoaderV2Factory.create(userWallet) - } else { - multiWalletContentLoaderFactory.create(userWallet, clickIntents) - } + multiWalletContentLoaderFactory.create(userWallet) } userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { - if (accountsFeatureToggles.isFeatureEnabled) { - singleWalletWithTokenContentLoaderV2Factory.create(userWallet) - } else { - singleWalletWithTokenContentLoaderFactory.create(userWallet, clickIntents) - } + singleWalletWithTokenContentLoaderFactory.create(userWallet) } userWallet is UserWallet.Cold && !userWallet.isMultiCurrency -> { - if (accountsFeatureToggles.isFeatureEnabled) { - singleWalletContentLoaderV2Factory.create(userWallet, isRefresh) - } else { - singleWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh) - } + singleWalletContentLoaderFactory.create(userWallet, isRefresh) } else -> null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt index 1fa83b51e4..ee6cde3b58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt @@ -18,8 +18,8 @@ internal class WalletLoaderStorage @Inject constructor() { } fun remove(id: UserWalletId) { - loaders[id]?.let { - it.forEach(Job::cancel) + loaders[id]?.let { jobs -> + jobs.forEach(Job::cancel) loaders.remove(id) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt index dd68293363..8965195057 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -4,7 +4,6 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.newSingleThreadContext @@ -14,9 +13,8 @@ import javax.inject.Inject /** * Base wallet screen content loader. Use it to load content by [UserWallet]. * - * @property factory factory that creates loader - * @property storage storage that save loader's jobs - * @property dispatchers coroutine dispatchers provider + * @property factory factory that creates loader + * @property storage storage that save loader's jobs * [REDACTED_AUTHOR] */ @@ -33,25 +31,19 @@ internal class WalletScreenContentLoader @Inject constructor( * Load content by [UserWallet] * * @param userWallet user wallet - * @param clickIntents click intents * @param isRefresh flag that determinate if content must load again * @param coroutineScope coroutine scope */ - fun load( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - isRefresh: Boolean = false, - coroutineScope: CoroutineScope, - ) { + fun load(userWallet: UserWallet, isRefresh: Boolean = false, coroutineScope: CoroutineScope) { if (userWallet.isLocked) return val id = userWallet.walletId if (!storage.contains(id)) { - loadInternal(userWallet, clickIntents, coroutineScope, isRefresh) + loadInternal(userWallet, coroutineScope, isRefresh) } else { if (isRefresh) { storage.remove(id) - loadInternal(userWallet, clickIntents, coroutineScope, isRefresh = true) + loadInternal(userWallet, coroutineScope, isRefresh = true) } else { Timber.d("$id content loading has already started") } @@ -70,15 +62,9 @@ internal class WalletScreenContentLoader @Inject constructor( singleBackgroundDispatcher.close() } - private fun loadInternal( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - coroutineScope: CoroutineScope, - isRefresh: Boolean, - ) { + private fun loadInternal(userWallet: UserWallet, coroutineScope: CoroutineScope, isRefresh: Boolean) { val loader = factory.create( userWallet = userWallet, - clickIntents = clickIntents, isRefresh = isRefresh, ) 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 c71fb95879..98fdd2bf6d 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,96 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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 -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -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.presentation.wallet.subscribers.* -import com.tangem.features.tangempay.TangemPayFeatureToggles +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject @Suppress("LongParameterList") -@Deprecated("Use MultiWalletContentLoaderV2 instead") -@ModelScoped -internal class MultiWalletContentLoader( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val tokenListStore: MultiWalletTokenListStore, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, +internal class MultiWalletContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val accountListSubscriberFactory: AccountListSubscriber.Factory, + private val walletNFTListSubscriberFactory: WalletNFTListSubscriberV2.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, + private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return buildList { - MultiWalletTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - applyTokenListSortingUseCase = applyTokenListSortingUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ).let(::add) + override fun create(): List = listOf( + accountListSubscriberFactory.create(userWallet), + walletNFTListSubscriberFactory.create(userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet), + multiWalletWarningsSubscriberFactory.create(userWallet), + multiWalletActionButtonsSubscriberFactory.create(userWallet), + tangemPayMainSubscriberFactory.create(userWallet), + ) - WalletNFTListSubscriber( - userWallet = userWallet, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - stateHolder = stateHolder, - walletsRepository = walletsRepository, - clickIntents = clickIntents, - currenciesRepository = currenciesRepository, - ).let(::add) - - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ).let(::add) - - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - - if (tangemPayFeatureToggles.isTangemPayEnabled) { - add(tangemPayMainSubscriberFactory.create(userWallet)) - } - } + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletContentLoader } } \ No newline at end of file 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 deleted file mode 100644 index 525899d92e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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 -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -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.presentation.wallet.subscribers.TangemPayMainSubscriber -import com.tangem.features.tangempay.TangemPayFeatureToggles -import javax.inject.Inject - -@Suppress("LongParameterList") -@Deprecated("Use MultiWalletContentLoaderV2.Factory instead") -@ModelScoped -internal class MultiWalletContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val walletsRepository: WalletsRepository, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val currenciesRepository: CurrenciesRepository, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, - private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, -) { - - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { - return MultiWalletContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - stateHolder = stateHolder, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - applyTokenListSortingUseCase = applyTokenListSortingUseCase, - getStoryContentUseCase = getStoryContentUseCase, - walletsRepository = walletsRepository, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - currenciesRepository = currenciesRepository, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - tangemPayFeatureToggles = tangemPayFeatureToggles, - tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt deleted file mode 100644 index 2c52604748..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.promo.GetStoryContentUseCase -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.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.tangempay.TangemPayFeatureToggles -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class MultiWalletContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet, - private val accountListSubscriberFactory: AccountListSubscriber.Factory, - private val tokenListAnalyticsSubscriberFactory: TokenListAnalyticsSubscriber.Factory, - private val walletNFTListSubscriberV2Factory: WalletNFTListSubscriberV2.Factory, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, - private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOfNotNull( - accountListSubscriberFactory.create(userWallet = userWallet), - tokenListAnalyticsSubscriberFactory.create(userWallet = userWallet), - walletNFTListSubscriberV2Factory.create(userWallet = userWallet), - checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ), - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - getStoryContentUseCase = getStoryContentUseCase, - ), - - if (tangemPayFeatureToggles.isTangemPayEnabled) { - tangemPayMainSubscriberFactory.create(userWallet) - } else { - null - }, - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet): MultiWalletContentLoaderV2 - } -} \ No newline at end of file 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 34c986c644..ae08b51df3 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 @@ -1,83 +1,34 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -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 dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject @Suppress("LongParameterList") -internal class SingleWalletContentLoader( - private val userWallet: UserWallet.Cold, - private val clickIntents: WalletClickIntents, - private val isRefresh: Boolean, - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, +internal class SingleWalletContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, + private val primaryCurrencySubscriberFactory: PrimaryCurrencySubscriber.Factory, + private val singleWalletButtonsSubscriberFactory: SingleWalletButtonsSubscriber.Factory, + private val singleWalletNotificationsSubscriberFactory: SingleWalletNotificationsSubscriber.Factory, + private val singleWalletExpressStatusesSubscriberFactory: SingleWalletExpressStatusesSubscriber.Factory, + private val txHistorySubscriberFactory: TxHistorySubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return listOf( - PrimaryCurrencySubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - ), - SingleWalletButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, - ), - SingleWalletNotificationsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - ), - SingleWalletExpressStatusesSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - ), - TxHistorySubscriber( - userWallet = userWallet, - isRefresh = isRefresh, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - ), - ) + override fun create(): List = listOf( + primaryCurrencySubscriberFactory.create(userWallet), + singleWalletButtonsSubscriberFactory.create(userWallet), + singleWalletNotificationsSubscriberFactory.create(userWallet), + singleWalletExpressStatusesSubscriberFactory.create(userWallet), + txHistorySubscriberFactory.create(userWallet, isRefresh), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoader } } \ No newline at end of file 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 deleted file mode 100644 index 4340bf544b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -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 javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -@Deprecated("Use SingleWalletContentLoaderV2.Factory instead") -internal class SingleWalletContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, -) { - - fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { - return SingleWalletContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - isRefresh = isRefresh, - stateHolder = stateHolder, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt deleted file mode 100644 index 6c4711cb5f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.account.AccountDependencies -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.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class SingleWalletContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Cold, - @Assisted private val isRefresh: Boolean, - private val clickIntents: WalletClickIntents, - private val stateHolder: WalletStateController, - private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val accountDependencies: AccountDependencies, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val dispatchers: CoroutineDispatcherProvider, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - PrimaryCurrencySubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - stateController = stateHolder, - analyticsEventHandler = analyticsEventHandler, - ), - SingleWalletButtonsSubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - stateController = stateHolder, - clickIntents = clickIntents, - getCryptoCurrencyActionsUseCaseV2 = getCryptoCurrencyActionsUseCaseV2, - ), - SingleWalletNotificationsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - ), - SingleWalletExpressStatusesSubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - stateController = stateHolder, - clickIntents = clickIntents, - analyticsEventHandler = analyticsEventHandler, - ), - TxHistorySubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - isRefresh = isRefresh, - stateController = stateHolder, - clickIntents = clickIntents, - ), - CheckWalletWithFundsSubscriber( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - walletWithFundsChecker = walletWithFundsChecker, - dispatchers = dispatchers, - ), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoaderV2 - } -} \ No newline at end of file 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 a43c2c130e..81720d5bd6 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,70 +1,29 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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 -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -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.presentation.wallet.subscribers.MultiWalletActionButtonsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -@Deprecated("Use SingleWalletWithTokenContentLoaderV2 instead") -@Suppress("LongParameterList") -internal class SingleWalletWithTokenContentLoader( - private val userWallet: UserWallet.Cold, - private val clickIntents: WalletClickIntents, - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, +internal class SingleWalletWithTokenContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory, + private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return buildList { - SingleWalletWithTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ).let(::add) - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ).let(::add) - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + override fun create(): List = listOf( + singleWalletWithTokenSubscriberFactory.create(userWallet), + multiWalletWarningsSubscriberFactory.create(userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoader } } \ No newline at end of file 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 deleted file mode 100644 index a0fcbbd771..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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 -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -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 javax.inject.Inject - -// TODO: Refactor -@Suppress("LongParameterList") -@Deprecated("Use SingleWalletWithTokenContentLoaderV2.Factory instead") -@ModelScoped -internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) { - - fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { - return SingleWalletWithTokenContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - stateHolder = stateHolder, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - getStoryContentUseCase = getStoryContentUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt deleted file mode 100644 index f11999ca24..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.domain.models.wallet.UserWallet -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.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class SingleWalletWithTokenContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Cold, - private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory, - private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val clickIntents: WalletClickIntents, - private val stateController: WalletStateController, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - singleWalletWithTokenSubscriberFactory.create(userWallet), - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ), - checkWalletWithFundsSubscriberFactory.create(userWallet), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoaderV2 - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index a39274838f..b1ddbd67b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -1,12 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer @@ -25,7 +23,9 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class WalletStateController @Inject constructor() { +internal class WalletStateController @Inject constructor( + private val designFeatureToggles: DesignFeatureToggles, +) { val uiState: StateFlow get() = mutableUiState @@ -53,6 +53,10 @@ internal class WalletStateController @Inject constructor() { return value.wallets.firstOrNull { it.walletCardState.id == userWalletId } } + fun getWalletUM(userWalletId: UserWalletId): WalletUM? { + return value.wallets2.firstOrNull { it.walletsBalanceUM.id == userWalletId } + } + fun getWalletStateIfSelected(walletId: UserWalletId): WalletState? { val selectedWalletId = getSelectedWalletId() @@ -61,16 +65,40 @@ internal class WalletStateController @Inject constructor() { } } + fun getWalletUMIfSelected(walletId: UserWalletId): WalletUM? { + val selectedWalletId = getSelectedWalletId() + + return value.wallets2.firstOrNull { + it.walletsBalanceUM.id == walletId && it.walletsBalanceUM.id == selectedWalletId + } + } + fun getSelectedWallet(): WalletState { return with(value) { wallets[selectedWalletIndex] } } + fun getSelectedWalletUM(): WalletUM { + return with(value) { wallets2[selectedWalletIndex] } + } + fun getSelectedWalletId(): UserWalletId { - return with(value) { wallets[selectedWalletIndex].walletCardState.id } + return with(value) { + if (designFeatureToggles.isRedesignEnabled) { + wallets2[selectedWalletIndex].walletsBalanceUM.id + } else { + wallets[selectedWalletIndex].walletCardState.id + } + } } fun getWalletIndexByWalletId(userWalletId: UserWalletId): Int? { - return with(value) { wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } } + return with(value) { + if (designFeatureToggles.isRedesignEnabled) { + wallets2.indexOfFirstOrNull { it.walletsBalanceUM.id == userWalletId } + } else { + wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } + } + } } fun showBottomSheet( @@ -105,12 +133,12 @@ internal class WalletStateController @Inject constructor() { topBarConfig = WalletTopBarConfig(onDetailsClick = {}), selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, wallets = persistentListOf(), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, - isNewMarketEnabled = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt new file mode 100644 index 0000000000..cdbe103b75 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Represents the state of the wallet balance in the UI. + * + * The sealed interface has three implementations: + * - [Content]: Represents the state when the wallet balance is successfully loaded. + * - [Error]: Represents the state when there was an error loading the wallet balance. + * - [Loading]: Represents the state when the wallet balance is currently being loaded. + * + * @property id The unique identifier of the wallet. + * @property name The name of the wallet. + */ +@Immutable +internal sealed interface WalletBalanceUM { + + /** Wallet Id */ + val id: UserWalletId + + /** Wallet Name */ + val name: String + + /** + * Wallet card content state + * + * @property id wallet id + * @property name wallet name + * @property balance wallet balance + */ + data class Content( + override val id: UserWalletId, + override val name: String, + val balance: TextReference, + val isBalanceFlickering: Boolean, + val isZeroBalance: Boolean?, + ) : WalletBalanceUM + + /** + * Wallet card error state + * + * @property id wallet id + * @property name wallet name + */ + data class Error( + override val id: UserWalletId, + override val name: String, + ) : WalletBalanceUM + + /** + * Wallet card loading state + * + * @property id wallet id + * @property name wallet name + */ + data class Loading( + override val id: UserWalletId, + override val name: String, + ) : WalletBalanceUM + + fun copySealed(name: String): WalletBalanceUM { + return when (this) { + is Content -> copy(name = name) + is Error -> copy(name = name) + is Loading -> copy(name = name) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt deleted file mode 100644 index adfede4a1c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import androidx.annotation.DrawableRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.feature.wallet.impl.R - -/** - * Wallet bottom sheet config - * -[REDACTED_AUTHOR] - */ -sealed class WalletBottomSheetConfig( - open val title: TextReference, - open val subtitle: TextReference, - @DrawableRes open val iconResId: Int, - val primaryButtonConfig: ButtonConfig, - val secondaryButtonConfig: ButtonConfig, -) : TangemBottomSheetConfigContent { - - data class ButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - @DrawableRes val iconResId: Int? = null, - ) - - data class UnlockWallets(val onUnlockClick: () -> Unit, val onScanClick: () -> Unit) : WalletBottomSheetConfig( - title = resourceReference(id = R.string.common_access_denied), - subtitle = resourceReference( - id = R.string.unlock_wallet_description_full, - formatArgs = wrappedList( - resourceReference(R.string.common_biometrics), - ), - ), - iconResId = R.drawable.ic_locked_24, - primaryButtonConfig = ButtonConfig( - text = resourceReference( - id = R.string.user_wallet_list_unlock_all_with, - formatArgs = wrappedList(resourceReference(R.string.common_biometrics)), - ), - onClick = onUnlockClick, - ), - secondaryButtonConfig = ButtonConfig( - text = resourceReference(id = R.string.welcome_unlock_card), - onClick = onScanClick, - iconResId = R.drawable.ic_tangem_24, - ), - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt new file mode 100644 index 0000000000..9d24a0c3da --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -0,0 +1,496 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.TangemMessageButtonUM +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.pluralReference +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.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.persistentListOf + +/** + * Wallet notification types + */ +internal enum class WalletNotificationType { + Status, + Critical, + Warning, + Promo, + Survey, + Informational, +} + +/** + * Wallet notification UI model + * + * @property messageUM - message to show in notification + * @property type - type of notification, affects design and priority + */ +internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val type: WalletNotificationType) { + + // region Status + data object SomeNetworksUnreachable : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SomeNetworksUnreachableNotification", + title = resourceReference(id = R.string.warning_some_networks_unreachable_title), + subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, + ) + + data object UsedOutdatedData : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UsedOutdatedDataNotification", + title = stringReference("Missing some token balances"), // todo redesign main lokalise + subtitle = stringReference("Will be updated as soon as possible"), // todo redesign main lokalise + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, + ) + + data object FailedCardValidation : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FailedCardValidationNotification", + title = resourceReference(id = R.string.warning_failed_to_verify_card_title), + subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + messageEffect = TangemMessageEffect.Warning, + ), + type = WalletNotificationType.Status, + ) + + data object DevCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DevCardNotification", + title = resourceReference(id = R.string.warning_developer_card_title), + subtitle = resourceReference(id = R.string.warning_developer_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object TestnetCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TestnetCardNotification", + title = resourceReference(id = R.string.warning_testnet_card_title), + subtitle = resourceReference(id = R.string.warning_testnet_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object DemoCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DemoCardNotification", + title = resourceReference(id = R.string.warning_demo_mode_title), + subtitle = resourceReference(id = R.string.warning_demo_mode_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + // endregion + + // region Critical + data class BackupError(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "BackupErrorNotification", + title = resourceReference(id = R.string.warning_backup_errors_title), + subtitle = resourceReference(id = R.string.warning_backup_errors_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_contact_support), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data class SeedPhraseNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SeedPhraseIssueNotification", + title = resourceReference(id = R.string.warning_seedphrase_issue_title), + subtitle = resourceReference(id = R.string.warning_seedphrase_issue_message), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_no), + type = TangemButtonType.PrimaryInverse, + onClick = onDeclineClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_yes), + type = TangemButtonType.PrimaryInverse, + onClick = onConfirmClick, + ), + ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Critical, + ) + + data class SeedPhraseSecondNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SeedPhraseSecondIssueNotification", + title = resourceReference(id = R.string.warning_seedphrase_action_required_title), + subtitle = resourceReference(id = R.string.warning_seedphrase_contacted_support), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.seed_warning_no), + type = TangemButtonType.PrimaryInverse, + onClick = onDeclineClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.seed_warning_yes), + type = TangemButtonType.PrimaryInverse, + onClick = onConfirmClick, + ), + ), + + ), + type = WalletNotificationType.Critical, + ) + + data class MissingBackup(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "MissingBackupNotification", + title = resourceReference(id = R.string.warning_no_backup_title), + subtitle = resourceReference(id = R.string.warning_no_backup_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.button_start_backup_process), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data class LowSignatures(val count: Int) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "LowSignaturesNotification", + title = resourceReference(id = R.string.warning_low_signatures_title), + subtitle = resourceReference( + id = R.string.warning_low_signatures_message, + formatArgs = wrappedList(count.toString()), + ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Critical, + ) + + data class FinishWalletActivation( + val messageEffect: TangemMessageEffect, + val isBackupExists: Boolean, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FinishWalletActivationNotification", + title = resourceReference(R.string.hw_activation_need_title), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { + when (messageEffect) { + TangemMessageEffect.Warning -> TangemTheme.colors2.graphic.neutral.primary + else -> TangemTheme.colors2.graphic.status.attention + } + }, + ), + messageEffect = messageEffect, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.hw_activation_need_finish), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = when (messageEffect) { + TangemMessageEffect.Warning -> WalletNotificationType.Critical + else -> WalletNotificationType.Warning + }, + ) + + data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NumberOfSignedHashesIncorrectNotification", + title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), + subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Critical, + ) + // endregion + + // region Warning + data class MissingAddresses( + @DrawableRes val tangemIcon: Int?, + val missingAddressesCount: Int, + val onGenerateClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "MissingAddressesNotification", + title = resourceReference(id = R.string.warning_missing_derivation_title), + subtitle = pluralReference( + id = R.plurals.warning_missing_derivation_message, + count = missingAddressesCount, + formatArgs = wrappedList(missingAddressesCount), + ), + isCentered = true, + messageEffect = TangemMessageEffect.Card, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_generate_addresses), + type = TangemButtonType.Primary, + iconRes = tangemIcon, + onClick = onGenerateClick, + ), + ), + ), + type = WalletNotificationType.Warning, + ) + + data class NoAccount(val network: String, val symbol: String, val amount: String) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoAccountNotification", + title = resourceReference(id = R.string.warning_no_account_title), + subtitle = resourceReference( + id = R.string.no_account_generic, + wrappedList(network, amount, symbol), + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Warning, + ) + + data class UnlockWallets(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UnlockWalletsNotification", + title = resourceReference(id = R.string.common_access_denied), + subtitle = resourceReference( + id = R.string.warning_access_denied_message, + formatArgs = wrappedList( + resourceReference(R.string.common_biometrics), + ), + ), + onClick = onClick, + messageEffect = TangemMessageEffect.Card, + isCentered = true, + ), + type = WalletNotificationType.Warning, + ) + // endregion + + // region Promo + data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoteMigrationNotification", + title = resourceReference(R.string.wallet_promo_banner_title), + subtitle = resourceReference(R.string.wallet_promo_banner_description), + messageEffect = TangemMessageEffect.Magic, + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.wallet_promo_banner_button_title), + onClick = onClick, + type = TangemButtonType.Primary, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class OnePlusOnePromo( + val onCloseClick: () -> Unit, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "OnePlusOnePromoNotification", + title = resourceReference(R.string.notification_one_plus_one_title), + subtitle = resourceReference(R.string.notification_one_plus_one_text), + messageEffect = TangemMessageEffect.Magic, + onCloseClick = onCloseClick, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_one_plus_one_button), + type = TangemButtonType.Primary, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class YieldPromo( + val onCloseClick: () -> Unit, + val onTermsAndConditionsClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "YieldPromoNotification", + title = resourceReference(R.string.notification_yield_promo_title), + subtitle = resourceReference(R.string.notification_yield_promo_text), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_yield_promo_button), + type = TangemButtonType.Primary, + onClick = onTermsAndConditionsClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + // endregion + + // region Survey + data class RateApp( + val onLikeClick: () -> Unit, + val onDislikeClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "RateAppNotification", + title = resourceReference(id = R.string.warning_rate_app_title), + subtitle = resourceReference(id = R.string.warning_rate_app_message), + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.warning_button_could_be_better), + type = TangemButtonType.PrimaryInverse, + onClick = onDislikeClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.warning_button_like_it), + type = TangemButtonType.Primary, + onClick = onLikeClick, + ), + ), + messageEffect = TangemMessageEffect.None, + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Survey, + ) + // endregion + + // region Informational + data class PushNotifications( + val onCloseClick: () -> Unit, + val onEnabledClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "PushNotificationsNotification", + title = resourceReference(R.string.user_push_notification_banner_title), + subtitle = resourceReference(R.string.user_push_notification_banner_subtitle), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.common_later), + type = TangemButtonType.PrimaryInverse, + onClick = onCloseClick, + ), + TangemMessageButtonUM( + text = resourceReference(R.string.common_enable), + type = TangemButtonType.Primary, + onClick = onEnabledClick, + ), + ), + ), + type = WalletNotificationType.Informational, + ) + + data class CloreMigration( + val onStartMigrationClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "CloreMigrationNotification", + title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title), + subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_description), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button), + onClick = onStartMigrationClick, + type = TangemButtonType.PrimaryInverse, + ), + ), + ), + type = WalletNotificationType.Informational, + ) + // endregion +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 9c6f2658ae..fe7cb111e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -9,10 +9,10 @@ internal data class WalletScreenState( val topBarConfig: WalletTopBarConfig, val selectedWalletIndex: Int, val wallets: ImmutableList, + val wallets2: ImmutableList, val onWalletChange: (index: Int, onlyState: Boolean) -> Unit, val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, - val isNewMarketEnabled: Boolean, val onDismissMarketsTooltip: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 20023e6233..12261f3059 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -55,11 +55,6 @@ internal sealed interface WalletState : WalletStateHolder { override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val tangemPayState: TangemPayState = TangemPayState.Empty } - - enum class WalletType { - Hot, - Cold, - } } sealed class SingleCurrency : WalletState, TxHistoryStateHolder { @@ -96,4 +91,9 @@ internal sealed interface WalletState : WalletStateHolder { override val marketPriceBlockState: MarketPriceBlockState? = null } } +} + +enum class WalletType { + Hot, + Cold, } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt new file mode 100644 index 0000000000..91ad3be6ab --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.row.TangemRowUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * State of the tokens list in the wallet screen + * + * @property tokenList list of tokens to display + * @property organizeButtonUM configuration for the "Organize Tokens" button, if it should + */ +@Immutable +internal sealed class WalletTokensListUM { + + abstract val tokenList: ImmutableList + abstract val organizeButtonUM: TangemButtonUM? + + data object Empty : WalletTokensListUM() { + override val tokenList: ImmutableList = persistentListOf() + override val organizeButtonUM: TangemButtonUM? = null + } + + data object Loading : WalletTokensListUM() { + override val tokenList: ImmutableList = persistentListOf( + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "0"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "1"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "2"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + ) + override val organizeButtonUM: TangemButtonUM? = null + } + + data class Content( + override val tokenList: ImmutableList, + override val organizeButtonUM: TangemButtonUM?, + ) : WalletTokensListUM() +} + +/** + * State of token list item in the wallet screen + */ +@Immutable +internal sealed interface TokensListItemUM2 { + val tokenRowUM: TangemRowUM + + data class GroupTitle( + override val tokenRowUM: TangemHeaderRowUM, + ) : TokensListItemUM2 + + data class Token( + override val tokenRowUM: TangemTokenRowUM, + ) : TokensListItemUM2 + + data class Portfolio( + override val tokenRowUM: TangemTokenRowUM, + val tokenList: ImmutableList, + val isExpanded: Boolean, + val isCollapsable: Boolean, + ) : TokensListItemUM2 +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt new file mode 100644 index 0000000000..d7b3a5af2d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.ds.button.TangemButtonUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal sealed interface WalletUM { + + val pullToRefreshConfig: PullToRefreshConfig + val walletsBalanceUM: WalletBalanceUM + + val buttons: PersistentList + val notifications: ImmutableList + val notificationsCarousel: ImmutableList + + val tokensListUM: WalletTokensListUM + + val nftState: WalletNFTItemUM + + val type: WalletType + + val tangemPayState: TangemPayState + + data class Content( + override val pullToRefreshConfig: PullToRefreshConfig, + override val walletsBalanceUM: WalletBalanceUM, + override val buttons: PersistentList, + override val notifications: ImmutableList, + override val notificationsCarousel: ImmutableList, + override val tokensListUM: WalletTokensListUM, + override val nftState: WalletNFTItemUM, + override val type: WalletType, + override val tangemPayState: TangemPayState, + ) : WalletUM + + data class Locked( + override val walletsBalanceUM: WalletBalanceUM, + override val buttons: PersistentList, + override val type: WalletType, + override val notifications: ImmutableList = persistentListOf(), + ) : WalletUM { + override val notificationsCarousel: ImmutableList = persistentListOf() + override val pullToRefreshConfig = PullToRefreshConfig(false, {}) + override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Empty // todo redesign main locked state + override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden + override val tangemPayState: TangemPayState = TangemPayState.Empty + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt index a76c101e75..3380c53a2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { @@ -22,6 +23,10 @@ internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletS } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun updateConfig(prevState: WalletState) = prevState.bottomSheetConfig?.copy( isShown = false, ) 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 559a45db9d..b7594959d3 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 @@ -7,7 +7,6 @@ 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 import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState.MultiCurrency.WalletType import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType import kotlinx.collections.immutable.PersistentList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt index 74aaa23e95..b0bf9f08c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class OpenBottomSheetTransformer( userWalletId: UserWalletId, @@ -28,6 +29,10 @@ internal class OpenBottomSheetTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun updateConfig() = TangemBottomSheetConfig( isShown = true, onDismissRequest = onDismissBottomSheet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 4459476877..bcfae039a0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory /** @@ -26,6 +27,10 @@ internal class ReinitializeWalletTransformer( ) } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + override fun transform(prevState: WalletState): WalletState { return walletLoadingStateFactory.create( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt index 0ccb1652e4..ae06ec59bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class RemoveNFTCollectionsTransformer( userWalletId: UserWalletId, @@ -17,4 +18,13 @@ internal class RemoveNFTCollectionsTransformer( is WalletState.SingleCurrency.Locked, -> prevState } + + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + nftState = WalletNFTItemUM.Hidden, + ) + is WalletUM.Locked -> walletUM + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt index f414b696e9..df4942ff07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetCryptoCurrencyActionsTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TokenActionsState.toManageButtons(): PersistentList { return states .filterIfS2C() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index d41cf131aa..9d0afb1eff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @@ -56,6 +57,10 @@ internal class SetExpressStatusesTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TangemBottomSheetConfig.updateStateWithExpressStatusBottomSheet( expressState: ExpressTransactionStateUM?, ): TangemBottomSheetConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt index 4d73f70ed2..caa9d7ec25 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.allLoadedCollectionsEmpty import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toPersistentList internal class SetNFTCollectionsTransformer( @@ -28,6 +29,19 @@ internal class SetNFTCollectionsTransformer( -> prevState } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + nftState = when { + nftCollections.allLoadedCollectionsEmpty() -> + WalletNFTItemUM.Empty(onItemClick) + else -> createContentNFTItemUM(onItemClick) + }, + ) + is WalletUM.Locked -> walletUM + } + } + private fun createContentNFTItemUM(onItemClick: () -> Unit): WalletNFTItemUM.Content { val collectionsContent = nftCollections .map { it.content } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index b09dc47d03..ab69b7a4ce 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletMarketPriceConverter import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetPrimaryCurrencyTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toLoadedSingleCurrencyState(): WalletCardState { return SingleWalletCardStateConverter(status.value, userWallet, appCurrency).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 1ad7d9cb18..985e877682 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -33,6 +34,10 @@ internal class SetRefreshStateTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun PullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): PullToRefreshConfig { return copy(isRefreshing = isRefreshing) } 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 41d72e9aeb..6efe15284e 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 @@ -7,9 +7,7 @@ import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.error.TokenListError 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 -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons import timber.log.Timber import java.math.BigDecimal @@ -51,6 +49,20 @@ internal class SetTokenListErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> { + walletUM.copy( + tokensListUM = WalletTokensListUM.Empty, + ) + } + is WalletUM.Locked -> { + Timber.w("Impossible to load tokens list for locked wallet") + walletUM + } + } + } + private fun WalletCardState.toLoadedState(): WalletCardState { return WalletCardState.Content( id = id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 8a7a452474..d50fe9e0bb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -5,11 +5,10 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber import java.math.BigDecimal @@ -22,6 +21,7 @@ internal class SetTokenListTransformer( private val yieldSupplyApyMap: Map = emptyMap(), private val stakingAvailabilityMap: Map = emptyMap(), private val shouldShowMainPromo: Boolean, + private val isAccountsModeEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -45,6 +45,20 @@ internal class SetTokenListTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> { + walletUM.copy( + tokensListUM = toLoadedState(), + ) + } + is WalletUM.Locked -> { + Timber.w("Impossible to load tokens list for locked wallet") + walletUM + } + } + } + private fun WalletCardState.toLoadedState(): WalletCardState { val fiatBalance = when (params) { is TokenConverterParams.Account -> params.accountList.totalFiatBalance @@ -68,4 +82,19 @@ internal class SetTokenListTransformer( shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } + + private fun toLoadedState(): WalletTokensListUM { + if (params !is TokenConverterParams.Account) return WalletTokensListUM.Empty + + return WalletTokensListUMTransformer( + selectedWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + yieldModuleApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = isAccountsModeEnabled, + expandedAccounts = params.expandedAccounts, + ).convert(value = params.accountList) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index f4fcbf57fc..9a2b1164f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetTxHistoryCountErrorTransformer( ) } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.SingleCurrency.Content -> prevState.copy(txHistoryState = createErrorState()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt index f89b8af13e..cba6ac874b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import timber.log.Timber @@ -33,6 +34,10 @@ internal class SetTxHistoryCountTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TxHistoryState.toLoadingState(): TxHistoryState { return if (this is TxHistoryState.Content) { Timber.d("Load transactions history: $transactionsCount") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt index d16f26c066..b8af717c52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import timber.log.Timber internal class SetTxHistoryItemsErrorTransformer( @@ -27,6 +28,10 @@ internal class SetTxHistoryItemsErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createErrorState(): TxHistoryState.Error = when (error) { is TxHistoryListError.DataError -> { TxHistoryState.Error( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt index d94f18179b..289daa4514 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemFlowConverter import kotlinx.coroutines.flow.Flow import timber.log.Timber @@ -32,6 +33,10 @@ internal class SetTxHistoryItemsTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TxHistoryState.toContentState(): TxHistoryState { val converter = TxHistoryItemFlowConverter( currentState = this, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index 45b0aabd50..fde5efd497 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -2,13 +2,18 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import timber.log.Timber internal class SetWarningsTransformer( userWalletId: UserWalletId, private val warnings: ImmutableList, + private val notifications: ImmutableList = persistentListOf(), + private val notificationsCarousel: ImmutableList = persistentListOf(), ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -23,4 +28,17 @@ internal class SetWarningsTransformer( } } } + + override fun transform(walletUM: WalletUM): WalletUM { + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + notifications = notifications, + notificationsCarousel = notificationsCarousel, + ) + is WalletUM.Locked -> { + Timber.w("Impossible to update notifications for locked wallet") + walletUM + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt index b8a735a4f4..cd344f9f4f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayExposedDeviceTransformer( userWalletId: UserWalletId, @@ -14,4 +15,8 @@ internal class TangemPayExposedDeviceTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt index 171cb8aaed..bbb5035c07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayHiddenStateTransformer( userWalletId: UserWalletId, @@ -15,4 +16,8 @@ internal class TangemPayHiddenStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt index 9e4e6bafd7..8c1f641c38 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayHideOnboardingStateTransformer( userWalletId: UserWalletId, @@ -15,4 +16,8 @@ internal class TangemPayHideOnboardingStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt index f731cb119d..6404796e7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -12,4 +13,8 @@ internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : Wa prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt index 15b2ad4dee..4659e5f486 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayOnboardingBannerStateTransformer( userWalletId: UserWalletId, @@ -22,4 +23,8 @@ internal class TangemPayOnboardingBannerStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index 98a3ee0caa..e2c4d1a6c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayRefreshNeededStateTransformer( userWalletId: UserWalletId, @@ -32,4 +33,8 @@ internal class TangemPayRefreshNeededStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt index cd5885b406..b37a6f916f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayRefreshShowProgressTransformer( userWalletId: UserWalletId, @@ -21,4 +22,8 @@ internal class TangemPayRefreshShowProgressTransformer( ), ) } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt index 5cdc1926c9..5b2a7765a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayUnavailableStateTransformer( userWalletId: UserWalletId, @@ -20,4 +21,8 @@ internal class TangemPayUnavailableStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index c995e3dd44..2839523857 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.model.CustomerInfo.CardInfo @@ -16,8 +17,7 @@ import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import java.util.Currency /** @@ -42,6 +42,10 @@ internal class TangemPayUpdateInfoStateTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createInitialState(): TangemPayState { val cardInfo = value.info.cardInfo val productInstance = value.info.productInstance @@ -50,7 +54,7 @@ internal class TangemPayUpdateInfoStateTransformer( // when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing. return when { value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId) - value.info.kycStatus != APPROVED && !value.info.customerId.isNullOrEmpty() -> + value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() -> createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId) cardInfo != null && productInstance != null -> getCardInfoState(customerId, cardInfo, productInstance) @@ -74,7 +78,6 @@ internal class TangemPayUpdateInfoStateTransformer( cardId = productInstance.cardId, isPinSet = cardInfo.isPinSet, cardFrozenState = cardFrozenState, - customerWalletAddress = cardInfo.customerWalletAddress, cardNumberEnd = cardInfo.lastFourDigits, chainId = POLYGON_CHAIN_ID, ), @@ -89,25 +92,24 @@ internal class TangemPayUpdateInfoStateTransformer( } } - private fun createKycInProgressState(kycStatus: CustomerInfo.KycStatus, customerId: String): TangemPayState = - Progress( - title = TextReference.Res(R.string.tangempay_payment_account), - description = when (kycStatus) { - CustomerInfo.KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) - else -> TextReference.Res(R.string.tangempay_kyc_in_progress) - }, - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = { - when (kycStatus) { - CustomerInfo.KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( - userWalletId = userWalletId, - customerId = customerId, - ) - else -> tangemPayClickIntents.onKycProgressClicked(userWalletId) - } - }, - ) + private fun createKycInProgressState(kycStatus: KycStatus, customerId: String): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_payment_account), + description = when (kycStatus) { + KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) + else -> TextReference.Res(R.string.tangempay_kyc_in_progress) + }, + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = { + when (kycStatus) { + KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( + userWalletId = userWalletId, + customerId = customerId, + ) + else -> tangemPayClickIntents.onKycProgressClicked(userWalletId) + } + }, + ) private fun createIssueProgressState(): TangemPayState = Progress( title = TextReference.Res(R.string.tangempay_payment_account), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt deleted file mode 100644 index bb5c7ca1cf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.transformers - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import kotlin.reflect.KClass - -internal abstract class TypedWalletStateTransformer( - userWalletId: UserWalletId, - protected val targetStateClass: KClass, -) : WalletStateTransformer(userWalletId) { - - abstract fun transformTyped(prevState: S): WalletState - - @Suppress("UNCHECKED_CAST") - final override fun transform(prevState: WalletState): WalletState { - return if (prevState::class == targetStateClass) { - transformTyped(prevState as S) - } else { - prevState - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt index b723034b32..b6ad62458f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.showSwapBadge internal class UpdateMultiWalletActionButtonBadgeTransformer( @@ -16,4 +17,8 @@ internal class UpdateMultiWalletActionButtonBadgeTransformer( else -> prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 3d29e45a4a..72c403ae19 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import timber.log.Timber internal class UpdateWalletCardsCountTransformer( @@ -30,6 +31,10 @@ internal class UpdateWalletCardsCountTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toUpdatedState(): WalletCardState { return when (this) { is WalletCardState.Content -> copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index be06139bf9..5aa3c34900 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList internal abstract class WalletStateTransformer( @@ -11,6 +12,8 @@ internal abstract class WalletStateTransformer( abstract fun transform(prevState: WalletState): WalletState + abstract fun transform(walletUM: WalletUM): WalletUM + final override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = prevState.wallets @@ -18,6 +21,11 @@ internal abstract class WalletStateTransformer( if (state.walletCardState.id == userWalletId) transform(state) else state } .toImmutableList(), + wallets2 = prevState.wallets2 + .map { walletUM -> + if (walletUM.walletsBalanceUM.id == userWalletId) transform(walletUM) else walletUM + } + .toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt new file mode 100644 index 0000000000..25f9f9df7b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.R +import com.tangem.common.ui.tokens.TokenItemStateConverter +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.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingOption +import com.tangem.domain.staking.model.common.RewardInfo +import com.tangem.domain.staking.model.common.RewardType +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.EarnApyConverter.EarnApyInfo +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class EarnApyConverter( + val yieldModuleApyMap: Map, + val stakingApyMap: Map, +) : Converter { + + override fun convert(value: CryptoCurrencyStatus): EarnApyInfo? { + val token = value.currency as? CryptoCurrency.Token + if (token != null && yieldModuleApyMap.isNotEmpty()) { + val yieldSupplyApy = yieldModuleApyMap.entries.firstOrNull { apy -> + apy.key.equals( + other = token.yieldSupplyKey(), + ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), + ) + }?.value + if (yieldSupplyApy != null) { + val isActive = value.value.yieldSupplyStatus?.isActive == false + return EarnApyInfo( + text = resourceReference( + R.string.yield_module_earn_badge, + wrappedList(yieldSupplyApy), + ), + isActive = isActive, + apy = yieldSupplyApy.toString(), + source = TokenItemStateConverter.ApySource.YIELD_SUPPLY, + ) + } + } + + if (stakingApyMap.isNotEmpty()) { + val stakingInfo = findStakingRate( + currencyStatus = value, + stakingApyMap = stakingApyMap, + ) + val rewardTypeRes = when (stakingInfo.rewardType) { + RewardType.APR -> R.string.staking_apr_earn_badge + RewardType.UNKNOWN, + RewardType.APY, + null, + -> R.string.yield_module_earn_badge + } + if (stakingInfo.rate != null) { + val apyString = stakingInfo.rate.format { percent(withPercentSign = false) } + return EarnApyInfo( + text = resourceReference( + rewardTypeRes, + wrappedList(apyString), + ), + isActive = stakingInfo.isActive, + apy = apyString, + source = TokenItemStateConverter.ApySource.STAKING, + ) + } + } + + return null + } + + private fun findStakingRate( + currencyStatus: CryptoCurrencyStatus, + stakingApyMap: Map, + ): StakingLocalInfo { + val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available + ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) + + val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data + val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit + val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool + + val rateInfo = when (val stakingOptions = stakingAvailability.option) { + is StakingOption.P2PEthPool -> { + RewardInfo( + rate = stakingOptions.apy, + type = RewardType.APY, + ) + } + is StakingOption.StakeKit -> if (stakeKitBalance != null) { + val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address } + stakeKitBalance.balance.items + .mapNotNull { it.validatorAddress } + .firstNotNullOfOrNull { address -> + validatorsByAddress[address]?.rewardInfo + } ?: stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo + } + .maxByOrNull { it.rate } + } else { + stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo + } + .maxByOrNull { it.rate } + } + } + + return StakingLocalInfo( + rate = rateInfo?.rate, + isActive = stakeKitBalance != null || p2pEthPoolBalance != null, + rewardType = rateInfo?.type, + ) + } + + data class StakingLocalInfo( + val rate: BigDecimal?, + val isActive: Boolean, + val rewardType: RewardType?, + ) + + data class EarnApyInfo( + val text: TextReference?, + val isActive: Boolean, + val apy: String?, + val source: TokenItemStateConverter.ApySource, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt new file mode 100644 index 0000000000..652591e155 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt @@ -0,0 +1,521 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount +import com.tangem.common.ui.account.AccountIconItemStateConverter +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM.EndContentUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +@Suppress("LargeClass", "LongParameterList") +internal class WalletTokensListUMTransformer( + private val appCurrency: AppCurrency, + private val selectedWallet: UserWallet, + private val clickIntents: WalletClickIntents, + private val yieldModuleApyMap: Map, + private val isAccountsModeEnabled: Boolean, + private val expandedAccounts: Set, + stakingAvailabilityMap: Map, + shouldShowMainPromo: Boolean, +) : Converter { + + private val yieldSupplyPromoBannerConverter = YieldSupplyPromoBannerConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + private val currencyToIconStateConverter = CryptoCurrencyToIconStateConverter() + private val earnApyConverter = EarnApyConverter( + yieldModuleApyMap = yieldModuleApyMap, + stakingApyMap = stakingAvailabilityMap, + ) + + override fun convert(value: AccountStatusList): WalletTokensListUM { + val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value) + return if (value.accountStatuses.isEmpty()) { + WalletTokensListUM.Empty + } else { + val isCollapsable = value.accountStatuses.count { + it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0 + } > 1 + + val tokenListUM = value.accountStatuses + .filterIsInstance() + .asSequence() + .flatMap { accountStatus -> + if (isAccountsModeEnabled) { + val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) + sequenceOf( + TokensListItemUM2.Portfolio( + tokenRowUM = toAccountRow(accountStatus, isExpanded), + isExpanded = isExpanded || !isCollapsable, + isCollapsable = isCollapsable, + tokenList = getTokenListItems( + accountStatus.tokenList, + promoCryptoCurrency, + ).toPersistentList(), + ), + ) + } else { + getTokenListItems(accountStatus.tokenList, promoCryptoCurrency) + } + }.toPersistentList() + + WalletTokensListUM.Content( + tokenList = tokenListUM, + organizeButtonUM = getOrganizeButtonUM(value), + ) + } + } + + private fun getTokenListItems( + tokenList: TokenList, + promoCryptoCurrency: CryptoCurrencyStatus?, + ): Sequence { + return when (tokenList) { + TokenList.Empty -> emptySequence() + is TokenList.GroupedByNetwork -> { + tokenList.groups.asSequence().flatMap { (network, currencies) -> + buildList { + add( + TokensListItemUM2.GroupTitle( + tokenRowUM = toGroupRow(network), + ), + ) + addAll( + currencies.asSequence().map { currencyStatus -> + val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id + TokensListItemUM2.Token( + tokenRowUM = toCurrencyRow( + currencyStatus = currencyStatus, + shouldShowPromo = shouldShowPromo, + ), + ) + }.toList(), + ) + } + } + } + is TokenList.Ungrouped -> { + tokenList.currencies.asSequence().map { currencyStatus -> + TokensListItemUM2.Token( + toCurrencyRow( + currencyStatus = currencyStatus, + shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id, + ), + ) + } + } + } + } + + private fun toAccountRow(accountStatus: AccountStatus.CryptoPortfolio, isExpanded: Boolean): TangemTokenRowUM { + val account = accountStatus.account + + val (topEndContent, bottomEndContent) = when (val accountBalance = accountStatus.tokenList.totalFiatBalance) { + TotalFiatBalance.Failed -> toFailedAccountRow() + is TotalFiatBalance.Loaded -> toLoadedAccountRow(accountStatus, accountBalance) + TotalFiatBalance.Loading -> EndContentUM.Loading to EndContentUM.Loading + } + + return TangemTokenRowUM.Content( + id = accountStatus.account.accountId.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account), + ), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = account.accountName.toUM().value, + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = pluralReference( + R.plurals.common_tokens_count, + count = account.tokensCount, + formatArgs = wrappedList(account.tokensCount), + ), + ), + topEndContentUM = topEndContent, + bottomEndContentUM = bottomEndContent, + onItemClick = { + if (isExpanded) { + clickIntents.onAccountCollapseClick(account) + } else { + clickIntents.onAccountExpandClick(account) + } + }, + onItemLongClick = null, + ) + } + + private fun toFailedAccountRow(): Pair { + return EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) to EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + } + + private fun toLoadedAccountRow( + accountStatus: AccountStatus.CryptoPortfolio, + accountBalance: TotalFiatBalance.Loaded, + ): Pair { + val priceChange = accountStatus.priceChangeLce.getOrNull() + + return EndContentUM.Content( + text = accountBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + ) to if (priceChange != null) { + val priceChangeType = PriceChangeType.fromBigDecimal(priceChange.value) + + EndContentUM.Content( + text = stringReference( + priceChange.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = priceChangeType, + valueInPercent = priceChange.value.format { percent() }, + ), + ) + } else { + EndContentUM.Empty + } + } + + private fun toGroupRow(network: Network): TangemHeaderRowUM { + return TangemHeaderRowUM( + id = network.hashCode().toString(), + title = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(network.name), + ), + ) + } + + private fun toCurrencyRow(currencyStatus: CryptoCurrencyStatus, shouldShowPromo: Boolean): TangemTokenRowUM { + val earnApyInfo = earnApyConverter.convert(currencyStatus) + + return TangemTokenRowUM.Content( + id = currencyStatus.currency.id.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = currencyToIconStateConverter.convert(currencyStatus), + ), + titleUM = toCurrencyRowTitle(currencyStatus, earnApyInfo), + subtitleUM = toCurrencyRowSubtitle(currencyStatus), + topEndContentUM = toCurrencyRowTopEnd(currencyStatus), + bottomEndContentUM = toCurrencyRowBottomEnd(currencyStatus), + promoBannerUM = toPromoBannerUM( + currencyStatus, + earnApyInfo.takeIf { shouldShowPromo }, + ), + onItemClick = when (currencyStatus.value) { + CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + -> null + else -> { + { + clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) + } + } + }, + onItemLongClick = when (currencyStatus.value) { + CryptoCurrencyStatus.Loading -> null + else -> { + { + clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) + } + } + }, + ) + } + + private fun toCurrencyRowTitle( + currencyStatus: CryptoCurrencyStatus, + earnApyInfo: EarnApyConverter.EarnApyInfo?, + ): TangemTokenRowUM.TitleUM = when (val value = currencyStatus.value) { + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> { + TangemTokenRowUM.TitleUM.Content( + text = stringReference(currencyStatus.currency.name), + ) + } + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TangemTokenRowUM.TitleUM.Content( + text = stringReference(currencyStatus.currency.name), + hasPending = value.hasCurrentNetworkTransactions, + badge = if (earnApyInfo != null && earnApyInfo.text != null) { + TangemBadgeUM( + type = TangemBadgeType.Solid, + color = when { + earnApyInfo.isActive -> TangemBadgeColor.Blue + else -> TangemBadgeColor.Gray + }, + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X4, + text = earnApyInfo.text, + onClick = if (earnApyInfo.apy != null) { + { + clickIntents.onApyLabelClick( + userWalletId = selectedWallet.walletId, + currencyStatus = currencyStatus, + apySource = earnApyInfo.source, + apy = earnApyInfo.apy, + ) + } + } else { + null + }, + ) + } else { + null + }, + ) + } + } + + private fun toCurrencyRowSubtitle(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.SubtitleUM { + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.SubtitleUM.Loading + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> TangemTokenRowUM.SubtitleUM.Content( + text = stringReference( + currencyStatus.value.fiatRate.format { + fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = PriceChangeType.fromBigDecimal(currencyStatus.value.priceChange.orZero()), + valueInPercent = currencyStatus.value.priceChange.format { percent() }, + ), + isFlickering = currencyStatus.value.isFlickering(), + ) + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> TangemTokenRowUM.SubtitleUM.Empty + } + } + + private fun toCurrencyRowTopEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM { + val yieldSupply = currencyStatus.value.yieldSupplyStatus + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + EndContentUM.Content( + text = currencyStatus.getTotalFiatAmount().formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + isFlickering = currencyStatus.value.isFlickering(), + startIcons = buildList { + addIf( + element = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + condition = yieldSupply?.isActive == true && !yieldSupply.isAllowedToSpend, + ) + addIf( + element = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.tertiary }, + ), + condition = currencyStatus.value.sources.total == StatusSource.ONLY_CACHE, + ) + }.toImmutableList(), + ) + } + is CryptoCurrencyStatus.Loading -> EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) + is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.NoAmount, + -> EndContentUM.Empty + } + } + + private fun toCurrencyRowBottomEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM { + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> EndContentUM.Content( + text = stringReference( + currencyStatus.getTotalCryptoAmount().format { + crypto(cryptoCurrency = currencyStatus.currency) + }, + ), + isFlickering = currencyStatus.value.isFlickering(), + ) + is CryptoCurrencyStatus.Loading -> EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_no_address, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.NoAmount, + -> EndContentUM.Empty + } + } + + private fun toPromoBannerUM( + currencyStatus: CryptoCurrencyStatus, + earnApyInfo: EarnApyConverter.EarnApyInfo?, + ): TangemTokenRowUM.PromoBannerUM { + val currency = currencyStatus.currency + val isTokenCurrency = currency is CryptoCurrency.Token + val isCurrencyStatusLoaded = currencyStatus.value is CryptoCurrencyStatus.Loaded + val isApyInfoNotNull = earnApyInfo != null && earnApyInfo.apy != null + + if (!isTokenCurrency || !isCurrencyStatusLoaded || !isApyInfoNotNull) { + return TangemTokenRowUM.PromoBannerUM.Empty + } + + return TangemTokenRowUM.PromoBannerUM.Content( + title = resourceReference( + R.string.yield_module_main_screen_promo_banner_message, + wrappedList(earnApyInfo.apy), + ), + onPromoBannerClick = { + clickIntents.onYieldPromoClicked(currency) + clickIntents.onApyLabelClick( + userWalletId = selectedWallet.walletId, + currencyStatus = currencyStatus, + apySource = earnApyInfo.source, + apy = earnApyInfo.apy, + ) + }, + onCloseClick = clickIntents::onYieldPromoCloseClick, + onPromoShown = { + clickIntents.onYieldPromoShown(currency) + }, + ) + } + + private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? { + return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) { + TangemButtonUM( + text = resourceReference(R.string.organize_tokens_title), + isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + type = TangemButtonType.PrimaryInverse, + iconRes = R.drawable.ic_filter_default_24, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } + + private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE + + private fun isSingleCurrencyWalletWithToken(): Boolean { + return selectedWallet is UserWallet.Cold && + selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt index 26c9df46a3..a94d6b5b78 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey @@ -28,6 +29,35 @@ internal class YieldSupplyPromoBannerConverter( if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null if (yieldModuleApyMap.isEmpty()) return null + val max = cryptoCurrencyStatuses.asSequence() + .mapNotNull { status -> + val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null + val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) + val tokenKey = "${token.network.rawId}_${token.contractAddress}" + + val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey -> + mapKey.equals(tokenKey, shouldIgnoreCase) + } ?: return@mapNotNull null + + status to matchedKey + } + .maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO } + + return max?.first + } + + fun convert2(value: AccountStatusList): CryptoCurrencyStatus? { + if (!shouldShowMainPromo) return null + + val currencies = value.flattenCurrencies().filter { status -> + status.value is CryptoCurrencyStatus.Loaded + } + + val cryptoCurrencyStatuses = currencies.filter { it.currency is CryptoCurrency.Token } + + if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (yieldModuleApyMap.isEmpty()) return null + val max = cryptoCurrencyStatuses.asSequence() .mapNotNull { status -> val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null 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 b38da828a2..a7c1027dab 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 @@ -52,7 +52,7 @@ internal class WalletLoadingStateFactory( bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, nftState = WalletNFTItemUM.Hidden, - type = WalletState.MultiCurrency.WalletType.Hot, + type = WalletType.Hot, tangemPayState = TangemPayState.Empty, ) } @@ -66,7 +66,7 @@ internal class WalletLoadingStateFactory( bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, nftState = WalletNFTItemUM.Hidden, - type = WalletState.MultiCurrency.WalletType.Cold, + type = WalletType.Cold, tangemPayState = TangemPayState.Empty, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index cf83e0d256..2d7ba38899 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -37,6 +38,7 @@ internal class AccountListSubscriber @AssistedInject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, + private val designFeatureToggles: DesignFeatureToggles, ) : BasicAccountListSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( @@ -51,15 +53,27 @@ internal class AccountListSubscriber @AssistedInject constructor( accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap, -> - updateState( - accountList = accountList, - appCurrency = appCurrency, - expandedAccounts = expandedAccounts, - isAccountMode = isAccountMode, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ) + if (designFeatureToggles.isRedesignEnabled) { + updateState2( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } else { + updateState( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } } private fun stakingAvailabilityFlow(): Flow> = getAccountStatusListFlow() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index f4b38a9290..5c944d4473 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -85,6 +85,29 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } } + protected fun updateState2( + accountList: AccountStatusList, + appCurrency: AppCurrency, + expandedAccounts: Set, + isAccountMode: Boolean, + yieldSupplyApyMap: Map = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), + shouldShowMainPromo: Boolean = false, + ) { + stateController.update( + SetTokenListTransformer( + params = TokenConverterParams.Account(accountList, expandedAccounts), + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = isAccountMode, + ), + ) + } + private fun singleAccountTransform( maybeTokenList: Lce, appCurrency: AppCurrency, @@ -141,6 +164,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = false, ), ) } 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 deleted file mode 100644 index 3fbfb3592d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.getOrElse -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import java.math.BigDecimal - -@Deprecated("Use AccountListSubscriber instead") -@Suppress("LongParameterList") -internal abstract class BasicTokenListSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : WalletSubscriber() { - - private val sendAnalyticsJobHolder = JobHolder() - private val onTokenListReceivedJobHolder = JobHolder() - - 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) - .onEach { maybeTokenList -> - coroutineScope.launch { - sendTokenListAnalytics( - flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(), - totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance, - ) - }.saveIn(sendAnalyticsJobHolder) - } - .distinctUntilChanged() - .onEach { maybeTokenList -> - coroutineScope.launch { - onTokenListReceived(maybeTokenList) - }.saveIn(onTokenListReceivedJobHolder) - }, - flow2 = appCurrencyFlow(), - flow3 = yieldSupplyApyFlow(), - flow4 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, shouldShowMainPromo -> - val tokenList = maybeTokenList.getOrElse( - ifLoading = { maybeContent -> - val isRefreshing = stateHolder.getWalletState(userWallet.walletId) - ?.pullToRefreshConfig - ?.isRefreshing == true - - maybeContent - ?.takeIf { !isRefreshing } - ?: return@combine - }, - ifError = { e -> - Timber.e("Failed to load token list: $e") - stateHolder.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = e, - appCurrency = appCurrency, - ), - ) - return@combine - }, - ) - - updateContent( - params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList), - appCurrency = appCurrency, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( - userWalletId = userWallet.walletId, - cryptoCurrencyList = tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), - ), - shouldShowMainPromo = shouldShowMainPromo, - ) - - walletWithFundsChecker.check(tokenList) - }, - ) - } - - private suspend fun sendTokenListAnalytics( - flattenCurrencies: List?, - totalFiatBalance: TotalFiatBalance?, - ) { - val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) - - tokenListAnalyticsSender.send( - displayedUiState = displayedState, - userWallet = userWallet, - flattenCurrencies = flattenCurrencies ?: return, - totalFiatBalance = totalFiatBalance ?: return, - ) - } - - private fun updateContent( - params: TokenConverterParams, - appCurrency: AppCurrency, - yieldSupplyApyMap: Map, - stakingAvailabilityMap: Map, - shouldShowMainPromo: Boolean, - ) { - stateHolder.update( - SetTokenListTransformer( - params = params, - userWallet = userWallet, - appCurrency = appCurrency, - clickIntents = clickIntents, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ), - ) - } - - private fun appCurrencyFlow(): Flow = getSelectedAppCurrencyUseCase() - .map { - it.getOrElse { e -> - Timber.e("Failed to load app currency: $e") - AppCurrency.Default - } - } - .distinctUntilChanged() - - private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() - .distinctUntilChanged() - - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() - .distinctUntilChanged() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt index 426cac1ace..8ef8a70601 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt @@ -1,28 +1,37 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionButtonBadgeTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -internal class MultiWalletActionButtonsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class MultiWalletActionButtonsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val getStoryContentUseCase: GetStoryContentUseCase, ) : WalletSubscriber() { + override fun create(coroutineScope: CoroutineScope): Flow<*> = getStoryContentUseCase( id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, ).map { maybeSwapStories -> val isSwapStoriesNotNull = maybeSwapStories.getOrNull() != null - stateHolder.update( + stateController.update( UpdateMultiWalletActionButtonBadgeTransformer( userWalletId = userWallet.walletId, showSwapBadge = isSwapStoriesNotNull, ), ) } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletActionButtonsSubscriber + } } \ No newline at end of file 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 deleted file mode 100644 index c3159ba551..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import kotlinx.coroutines.CoroutineScope - -@Deprecated("Use AccountListSubscriber instead") -@Suppress("LongParameterList") -internal class MultiWalletTokenListSubscriber( - private val userWallet: UserWallet, - private val tokenListStore: MultiWalletTokenListStore, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - stateHolder: WalletStateController, - clickIntents: WalletClickIntents, - tokenListAnalyticsSender: TokenListAnalyticsSender, - walletWithFundsChecker: WalletWithFundsChecker, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : BasicTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, -) { - - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { - tokenListStore.addIfNot(userWallet.walletId, coroutineScope) - - return tokenListStore.getOrThrow(userWallet.walletId) - } - - override suspend fun onTokenListReceived(maybeTokenList: Lce) { - updateSortingIfNeeded(maybeTokenList) - } - - private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) { - val tokenList = getTokenList(maybeTokenList) ?: return - - applyTokenListSortingUseCase( - userWalletId = userWallet.walletId, - sortedTokensIds = getCurrenciesIds(tokenList), - isGroupedByNetwork = tokenList is TokenList.GroupedByNetwork, - isSortedByBalance = tokenList.sortedBy == TokensSortType.BALANCE, - ) - } - - private fun getTokenList(lce: Lce<*, TokenList>): TokenList? { - val tokenList = lce.getOrNull(isPartialContentAccepted = false) - ?: return null - - return tokenList.takeIf { - tokenList.totalFiatBalance is TotalFiatBalance.Loaded && - tokenList.sortedBy == TokensSortType.BALANCE - } - } - - private fun getCurrenciesIds(tokenList: TokenList): List { - return tokenList.flattenCurrencies().map { it.currency.id } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index a8c3a91d43..8cc069470a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -8,17 +8,17 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* -internal class MultiWalletWarningsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class MultiWalletWarningsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, @@ -30,14 +30,20 @@ internal class MultiWalletWarningsSubscriber( .conflate() .distinctUntilChanged() .onEach { warnings -> - val displayedState = stateHolder.getWalletState(userWallet.walletId) + val displayedState = stateController.getWalletState(userWallet.walletId) // Wait until the wallet appears in the list - stateHolder.uiState.first { + stateController.uiState.first { it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } } - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateController.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = warnings, + notifications = persistentListOf(), + ), + ) walletWarningsAnalyticsSender.send(displayedState, warnings) walletWarningsSingleEventSender.send( userWalletId = userWallet.walletId, @@ -46,4 +52,9 @@ internal class MultiWalletWarningsSubscriber( ) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletWarningsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt new file mode 100644 index 0000000000..f7e10a9ecf --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.models.wallet.UserWallet +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.analytics.utils.WalletWarningsSingleEventSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsCarouselFactory +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class MultiWalletWarningsSubscriberV2( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val clickIntents: WalletClickIntents, + private val getWalletWarningsFactory: GetWalletWarningsFactory, + private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory, + private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow> { + return combine( + flow = getWalletWarningsFactory.create(userWallet, clickIntents).conflate().distinctUntilChanged(), + flow2 = getWalletNotificationsCarouselFactory.create(userWallet, clickIntents).conflate() + .distinctUntilChanged(), + ) { notifications, notificationsCarousel -> + val displayedWalletUM = stateHolder.getWalletUM(userWallet.walletId) + + // Wait until the wallet appears in the list + stateHolder.uiState.first { + it.wallets2.any { walletUM -> walletUM.walletsBalanceUM.id == userWallet.walletId } + } + + // If there are notifications, we need to filter out the RateApp notification from stackable notifications, + // because it should not be shown together with other notifications. + val alteredNotificationsCarousel = if (notifications.isNotEmpty()) { + notificationsCarousel.filterNot { it is WalletNotificationUM.RateApp } + } else { + notificationsCarousel + }.toPersistentList() + + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = persistentListOf(), + notifications = notifications, + notificationsCarousel = alteredNotificationsCarousel, + ), + ) + + val totalNotifications = (notifications + alteredNotificationsCarousel).toPersistentList() + + walletWarningsAnalyticsSender.send(displayedWalletUM, totalNotifications) + walletWarningsSingleEventSender.send( + userWalletId = userWallet.walletId, + displayedWalletUM = displayedWalletUM, + newNotifications = totalNotifications, + ) + + totalNotifications + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index 6f564c38a2..bd063d00a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -1,62 +1,47 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.common.extensions.isZero import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import timber.log.Timber +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import java.math.BigDecimal -@Deprecated("Use PrimaryCurrencySubscriberV2 instead") -internal class PrimaryCurrencySubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, +internal class PrimaryCurrencySubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val stateController: WalletStateController, private val analyticsEventHandler: AnalyticsEventHandler, -) : WalletSubscriber() { +) : BasicSingleWalletSubscriber() { - override fun create( - coroutineScope: CoroutineScope, - ): Flow, AppCurrency>> { + override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( - flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .conflate() - .distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase() - .conflate() - .distinctUntilChanged() - .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, - transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, + flow = getPrimaryCurrencyStatusFlow(), + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + transform = ::Pair, ) - .onEach { maybeCurrencyStatusAndAppCurrency -> - val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { - Timber.e("Unable to get primary currency status: $it") - return@onEach - } - - updateContent(status, maybeCurrencyStatusAndAppCurrency.second) + .onEach { (status, appCurrency) -> + updateContent(status, appCurrency) sendAnalyticsEvent(status) - checkWalletWithFunds(status) } } private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) { - stateHolder.update( + stateController.update( SetPrimaryCurrencyTransformer( status = status, userWallet = userWallet, @@ -81,11 +66,11 @@ internal class PrimaryCurrencySubscriber( -> null } - cardBalanceState?.let { + cardBalanceState?.let { balanceState -> // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( - balance = it, + balance = balanceState, tokensCount = null, ), ) @@ -100,7 +85,8 @@ internal class PrimaryCurrencySubscriber( } } - private suspend fun checkWalletWithFunds(status: CryptoCurrencyStatus) { - if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase() + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): PrimaryCurrencySubscriber } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt deleted file mode 100644 index 3aa5c39bb4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.common.extensions.isZero -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onEach -import java.math.BigDecimal - -internal class PrimaryCurrencySubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val stateController: WalletStateController, - private val analyticsEventHandler: AnalyticsEventHandler, -) : BasicSingleWalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow<*> { - return combine( - flow = getPrimaryCurrencyStatusFlow(), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - transform = ::Pair, - ) - .onEach { (status, appCurrency) -> - updateContent(status, appCurrency) - sendAnalyticsEvent(status) - } - } - - private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) { - stateController.update( - SetPrimaryCurrencyTransformer( - status = status, - userWallet = userWallet, - appCurrency = appCurrency, - ), - ) - } - - private fun sendAnalyticsEvent(status: CryptoCurrencyStatus) { - val fiatAmount = status.value.fiatAmount - val cardBalanceState = when (status.value) { - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.NoAccount, - is CryptoCurrencyStatus.NoAmount, - -> createCardBalanceState(fiatAmount) - is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate - is CryptoCurrencyStatus.Unreachable, - -> AnalyticsParam.CardBalanceState.BlockchainError - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Loading, - is CryptoCurrencyStatus.Custom, - -> null - } - - cardBalanceState?.let { - // do not send tokens count for single currency wallet - analyticsEventHandler.send( - event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( - balance = it, - tokensCount = null, - ), - ) - } - } - - private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? { - return when { - fiatAmount == null -> null - fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty - else -> AnalyticsParam.CardBalanceState.Full - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt index af71e2d23f..f477c4d790 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -1,44 +1,43 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.TokenActionsState 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 import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onEach -@Deprecated("Use SingleWalletButtonsSubscriberV2 instead") -internal class SingleWalletButtonsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletButtonsSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) : WalletSubscriber() { + private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, +) : BasicSingleWalletSubscriber() { + @OptIn(ExperimentalCoroutinesApi::class) override fun create(coroutineScope: CoroutineScope): Flow { - return channelFlow { - getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> - getCryptoCurrencyActionsUseCase(userWallet = userWallet, status = status) - ?.let { send(it) } + return getPrimaryCurrencyStatusFlow() + .flatMapLatest { + getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) } - } - .onEach { actions -> - updateContent( - tokenActionsState = actions, - portfolioId = PortfolioId(userWallet.walletId), - ) + .onEach { + updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId)) } } private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { - stateHolder.update( + stateController.update( SetCryptoCurrencyActionsTransformer( tokenActionsState = tokenActionsState, userWallet = userWallet, @@ -48,9 +47,8 @@ internal class SingleWalletButtonsSubscriber( ) } - private suspend fun getCryptoCurrencyActionsUseCase(userWallet: UserWallet, status: CryptoCurrencyStatus) = - this.getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status) - .conflate() - .distinctUntilChanged() - .firstOrNull() + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletButtonsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt deleted file mode 100644 index 85d65823fb..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.onEach - -internal class SingleWalletButtonsSubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, -) : BasicSingleWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow { - return getPrimaryCurrencyStatusFlow() - .flatMapLatest { - getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) - } - .onEach { - updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId)) - } - } - - private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { - stateController.update( - SetCryptoCurrencyActionsTransformer( - tokenActionsState = tokenActionsState, - userWallet = userWallet, - clickIntents = clickIntents, - portfolioId = portfolioId, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index d417654a2f..6a8d7f382b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -1,93 +1,92 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import timber.log.Timber @Suppress("LongParameterList") -@Deprecated("Use SingleWalletExpressStatusesSubscriberV2 instead") -internal class SingleWalletExpressStatusesSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletExpressStatusesSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, -) : WalletSubscriber() { - - override fun create( - coroutineScope: CoroutineScope, - ): Flow, AppCurrency>> { - return combine( - flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWallet.walletId) - .conflate() - .distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase() - .conflate() - .distinctUntilChanged() - .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, - transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, - ).onEach { maybeCurrencyStatusAndAppCurrency -> - val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { - Timber.e("Unable to get primary currency status: $it") - return@onEach - } +) : BasicSingleWalletSubscriber() { + @OptIn(ExperimentalCoroutinesApi::class) + override fun create(coroutineScope: CoroutineScope): Flow<*> { + val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus -> getOnrampTransactionsUseCase( userWalletId = userWallet.walletId, - cryptoCurrencyId = status.currency.id, - ).onEach { maybeTransaction -> + cryptoCurrencyId = currencyStatus.currency.id, + ) + .map { currencyStatus to it } + } + + return combine( + flow = getOnrampTransactionsFlow, + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + transform = ::toTriple, + ) + .onEach { (status, maybeTransaction, appCurrency) -> maybeTransaction.fold( ifRight = { onrampTxs -> onrampTxs.clearHiddenTerminal() - stateHolder.update( + stateController.update( SetExpressStatusesTransformer( userWalletId = userWallet.walletId, onrampTxs = onrampTxs, clickIntents = clickIntents, cryptoCurrencyStatus = status, - appCurrency = maybeCurrencyStatusAndAppCurrency.second, + appCurrency = appCurrency, analyticsEventHandler = analyticsEventHandler, ), ) }, ifLeft = { - stateHolder.update( + stateController.update( SetExpressStatusesTransformer( userWalletId = userWallet.walletId, - onrampTxs = listOf(), + onrampTxs = emptyList(), clickIntents = clickIntents, cryptoCurrencyStatus = status, - appCurrency = maybeCurrencyStatusAndAppCurrency.second, + appCurrency = appCurrency, analyticsEventHandler = analyticsEventHandler, ), ) }, ) } - .launchIn(coroutineScope) - } + } + + private fun toTriple(firstPair: Pair, second: C): Triple { + return Triple(firstPair.first, firstPair.second, second) } private suspend fun List.clearHiddenTerminal() { - this.filter { it.status.isHidden && it.status.isTerminal } + this + .filter { it.status.isHidden && it.status.isTerminal } .forEach { onrampRemoveTransactionUseCase(txId = it.txId) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletExpressStatusesSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt deleted file mode 100644 index 59581e98ce..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -@Suppress("LongParameterList") -internal class SingleWalletExpressStatusesSubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val analyticsEventHandler: AnalyticsEventHandler, -) : BasicSingleWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> { - val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus -> - getOnrampTransactionsUseCase( - userWalletId = userWallet.walletId, - cryptoCurrencyId = currencyStatus.currency.id, - ) - .map { currencyStatus to it } - } - - return combine( - flow = getOnrampTransactionsFlow, - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - transform = ::toTriple, - ) - .onEach { (status, maybeTransaction, appCurrency) -> - maybeTransaction.fold( - ifRight = { onrampTxs -> - onrampTxs.clearHiddenTerminal() - stateController.update( - SetExpressStatusesTransformer( - userWalletId = userWallet.walletId, - onrampTxs = onrampTxs, - clickIntents = clickIntents, - cryptoCurrencyStatus = status, - appCurrency = appCurrency, - analyticsEventHandler = analyticsEventHandler, - ), - ) - }, - ifLeft = { - stateController.update( - SetExpressStatusesTransformer( - userWalletId = userWallet.walletId, - onrampTxs = listOf(), - clickIntents = clickIntents, - cryptoCurrencyStatus = status, - appCurrency = appCurrency, - analyticsEventHandler = analyticsEventHandler, - ), - ) - }, - ) - } - } - - private fun toTriple(firstPair: Pair, second: C): Triple { - return Triple(firstPair.first, firstPair.second, second) - } - - private suspend fun List.clearHiddenTerminal() { - this - .filter { it.status.isHidden && it.status.isTerminal } - .forEach { onrampRemoveTransactionUseCase(txId = it.txId) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index d815a9c21c..42839a3e87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -7,7 +7,11 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarni import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate @@ -17,9 +21,9 @@ import kotlinx.coroutines.flow.onEach /** [REDACTED_AUTHOR] */ -internal class SingleWalletNotificationsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletNotificationsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val clickIntents: WalletClickIntents, @@ -30,10 +34,15 @@ internal class SingleWalletNotificationsSubscriber( .conflate() .distinctUntilChanged() .onEach { warnings -> - val displayedState = stateHolder.getWalletState(userWallet.walletId) + val displayedState = stateController.getWalletState(userWallet.walletId) - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateController.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) walletWarningsAnalyticsSender.send(displayedState, warnings) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletNotificationsSubscriber + } } \ No newline at end of file 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 deleted file mode 100644 index af21e47dc4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -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.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import kotlinx.coroutines.CoroutineScope - -@Deprecated("Use SingleWalletWithTokenSubscriber instead") -@Suppress("LongParameterList") -internal class SingleWalletWithTokenListSubscriber( - private val userWallet: UserWallet.Cold, - private val tokenListStore: MultiWalletTokenListStore, - stateHolder: WalletStateController, - clickIntents: WalletClickIntents, - tokenListAnalyticsSender: TokenListAnalyticsSender, - walletWithFundsChecker: WalletWithFundsChecker, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : BasicTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, -) { - - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { - tokenListStore.addIfNot(userWallet.walletId, coroutineScope) - - 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 bef7b6defb..3bd4244ab3 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 @@ -4,47 +4,46 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase 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 import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -typealias MaybeTxHistoryCount = Either -typealias MaybeTxHistoryItems = Either>> - @Suppress("LongParameterList") -@Deprecated("Use TxHistorySubscriberV2 instead") -internal class TxHistorySubscriber( - private val userWallet: UserWallet.Cold, - private val isRefresh: Boolean, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, +internal class TxHistorySubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, -) : WalletSubscriber() { + private val stateController: WalletStateController, + private val clickIntents: WalletClickIntents, +) : BasicSingleWalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow> { return flow { - getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + getPrimaryCurrencyStatusFlow().collectLatest { status -> val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( userWalletId = userWallet.walletId, currency = status.currency, @@ -52,29 +51,32 @@ internal class TxHistorySubscriber( setLoadingTxHistoryState(maybeTxHistoryItemCount, status) - maybeTxHistoryItemCount.onRight { + maybeTxHistoryItemCount.onRight { _ -> val maybeTxHistoryItems = txHistoryItemsUseCase( userWalletId = userWallet.walletId, currency = status.currency, refresh = isRefresh, ).map { it.cachedIn(coroutineScope) } - setLoadedTxHistoryState(maybeTxHistoryItems, status.currency) + setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency) } } } } - private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { - stateHolder.update( + private fun setLoadingTxHistoryState( + maybeTxHistoryItemCount: Either, + status: CryptoCurrencyStatus, + ) { + stateController.update( maybeTxHistoryItemCount.fold( ifLeft = { error -> SetTxHistoryCountErrorTransformer( userWallet = userWallet, error = error, pendingTransactions = status.value.pendingTransactions, - currency = status.currency, clickIntents = clickIntents, + currency = status.currency, ) }, ifRight = { txCount -> @@ -88,23 +90,26 @@ internal class TxHistorySubscriber( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { - stateHolder.update( + private fun setLoadedTxHistoryState( + maybeTxHistoryItems: Either>>, + currency: CryptoCurrency, + ) { + stateController.update( maybeTxHistoryItems.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, - error = it, + error = error, clickIntents = clickIntents, ) }, ifRight = { itemsFlow -> val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() val itemConverter = TxHistoryItemStateConverter( + currency = currency, symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, - currency = currency, ) SetTxHistoryItemsTransformer( @@ -118,4 +123,9 @@ internal class TxHistorySubscriber( ), ) } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): TxHistorySubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt deleted file mode 100644 index 78c388f87f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map - -@Suppress("LongParameterList") -internal class TxHistorySubscriberV2( - override val userWallet: UserWallet.Cold, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val isRefresh: Boolean, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, -) : BasicSingleWalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow> { - return flow { - getPrimaryCurrencyStatusFlow().collectLatest { status -> - val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - ) - - setLoadingTxHistoryState(maybeTxHistoryItemCount, status) - - maybeTxHistoryItemCount.onRight { - val maybeTxHistoryItems = txHistoryItemsUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - refresh = isRefresh, - ).map { it.cachedIn(coroutineScope) } - - setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency) - } - } - } - } - - private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { - stateController.update( - maybeTxHistoryItemCount.fold( - ifLeft = { error -> - SetTxHistoryCountErrorTransformer( - userWallet = userWallet, - error = error, - pendingTransactions = status.value.pendingTransactions, - clickIntents = clickIntents, - currency = status.currency, - ) - }, - ifRight = { txCount -> - SetTxHistoryCountTransformer( - userWalletId = userWallet.walletId, - transactionsCount = txCount, - clickIntents = clickIntents, - ) - }, - ), - ) - } - - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { - stateController.update( - maybeTxHistoryItems.fold( - ifLeft = { - SetTxHistoryItemsErrorTransformer( - userWalletId = userWallet.walletId, - error = it, - clickIntents = clickIntents, - ) - }, - ifRight = { itemsFlow -> - val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - val itemConverter = TxHistoryItemStateConverter( - currency = currency, - symbol = blockchain.currency, - decimals = blockchain.decimals(), - clickIntents = clickIntents, - ) - - SetTxHistoryItemsTransformer( - userWallet = userWallet, - flow = itemsFlow.map { items -> - items.map(itemConverter::convert) - }, - clickIntents = clickIntents, - ) - }, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt deleted file mode 100644 index 34b9a3bc97..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.RemoveNFTCollectionsTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -@Deprecated("Use WalletNFTListSubscriberV2 instead") -internal class WalletNFTListSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val clickIntents: WalletClickIntents, -) : WalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine( - walletsRepository.nftEnabledStatus(userWallet.walletId), - currenciesRepository.getWalletCurrenciesUpdates(userWallet.walletId), - ) { nftEnabled, currencies -> nftEnabled to currencies } - .distinctUntilChanged() - .flatMapLatest { (nftEnabled, currencies) -> - // if NFT is enabled for this wallet and there are currencies, - // then start observing changes from store and apply transformer if need - if (nftEnabled && currencies.isNotEmpty()) { - getNFTCollectionsUseCase(userWallet.walletId) - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - .onEach { - stateHolder.update( - SetNFTCollectionsTransformer( - userWalletId = userWallet.walletId, - nftCollections = it, - onItemClick = { clickIntents.onNFTClick(userWallet) }, - ), - ) - } - } else { - // otherwise, hide NFT from wallet - stateHolder.update( - RemoveNFTCollectionsTransformer(userWallet.walletId), - ) - emptyFlow() - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index ec373aea09..324c26f234 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -304,11 +304,7 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val background = if (state.isNewMarketEnabled) { - TangemTheme.colors.background.tertiary - } else { - TangemTheme.colors.background.primary - } + val background = TangemTheme.colors.background.tertiary val showMarketsHint by remember { derivedStateOf { @@ -720,7 +716,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { when (bottomSheetConfig.content) { - is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt new file mode 100644 index 0000000000..e368898477 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -0,0 +1,440 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.components.rememberIsKeyboardVisible +import com.tangem.core.ui.components.sheetscaffold.* +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.* +import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import dev.chrisbanes.haze.HazeProgressive +import kotlinx.coroutines.launch + +@OptIn(ExperimentalDecomposeApi::class) +@Composable +internal fun WalletScreen2( + state: WalletScreenState, + bottomSheetContent: @Composable (() -> Unit), + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { + // It means that screen is still initializing + if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return + + val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex) + val snackbarHostState = remember(::SnackbarHostState) + val isAutoScroll = remember { mutableStateOf(value = false) } + + WalletContent2( + state = state, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + isAutoScroll = isAutoScroll, + onAutoScrollReset = { isAutoScroll.value = false }, + bottomSheetContent = bottomSheetContent, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, + ) + + WalletEventEffect( + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + event = state.event, + onAutoScrollSet = { isAutoScroll.value = true }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalDecomposeApi::class) +@Suppress("LongMethod", "LongParameterList", "UnusedPrivateMember") +@Composable +private fun WalletContent2( + state: WalletScreenState, + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + bottomSheetContent: @Composable (() -> Unit), +) { + /* + * Don't pass key to remember, because it will brake scroll animation. + * selectedWalletIndex will be changed in WalletsListEffects. + */ + // val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } + // val selectedWallet = state.wallets2.getOrElse(selectedWalletIndex) { state.wallets2[state.selectedWalletIndex] } + + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getBottom(this).toDp() } + + val listState = rememberLazyListState() + + val partialCollapsedHeight = 64.dp + statusBarHeight + + val scaffoldContent: @Composable (PaddingValues?) -> Unit = { _ -> + Box(Modifier.fillMaxSize()) { + NorthernLightsBackground(Modifier.matchParentSize()) + } + + val pagerState = rememberPagerState( + initialPage = state.selectedWalletIndex, + pageCount = { state.wallets2.size }, + ) + + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage != state.selectedWalletIndex) { + state.onWalletChange(pagerState.currentPage, false) + } + } + } + + BaseScaffoldWithMarkets( + state = state, + listState = listState, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, + bottomSheetContent = bottomSheetContent, + content = scaffoldContent, + ) +} + +@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod", "UnusedPrivateMember") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private inline fun BaseScaffoldWithMarkets( + state: WalletScreenState, + snackbarHostState: SnackbarHostState, + listState: LazyListState, + bottomSheetHeaderHeightProvider: () -> Dp, + modifier: Modifier = Modifier, + noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, + crossinline bottomSheetContent: @Composable () -> Unit, + crossinline content: @Composable (PaddingValues) -> Unit, +) { + val bottomSheetState = rememberTangemStandardBottomSheetState() + val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle() + + val isKeyboardVisible by rememberIsKeyboardVisible() + + val scaffoldState = rememberTangemBottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState, + ) + + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() } + val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() } + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val maxHeight = LocalWindowSize.current.height + + val coroutineScope = rememberCoroutineScope() + val background = TangemTheme.colors2.surface.level2 + + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) }, + ) { + val backgroundColor = LocalMainBottomSheetColor.current + var isSearchFieldFocused by remember { mutableStateOf(false) } + val isNavBarVisible = remember { mutableStateOf(true) } + + BottomSheetStateEffects( + bottomSheetState = bottomSheetState, + onBottomSheetStateChange = onBottomSheetStateChange, + navigationBarVisible = isNavBarVisible, + isSearchFieldFocused = isSearchFieldFocused, + ) + + Box(modifier = modifier) { + TangemBottomSheetScaffold( + modifier = Modifier.background( + brush = Brush.verticalGradient( + listOf( + TangemTheme.colors2.surface.level1, + TangemTheme.colors2.surface.level2, + ), + ), + ), + snackbarHost = { snackbarHostState -> + WalletSnackbarHost( + snackbarHostState = snackbarHostState, + event = state.event, + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing4) + .navigationBarsPadding(), + ) + }, + containerColor = Color.Unspecified, + sheetContainerColor = backgroundColor.value, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetShape = TangemTheme.shapes.bottomSheetLarge, + sheetContent = { + // hide bottom sheet when back pressed + BackHandler( + isKeyboardVisible.not() && + bottomSheetState.currentValue == TangemSheetValue.Expanded, + ) { + coroutineScope.launch { bottomSheetState.partialExpand() } + } + + Column( + modifier = Modifier + // expand bottom sheet when clicked on the header + .clickable( + enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded, + indication = null, + interactionSource = null, + ) { + coroutineScope.launch { bottomSheetState.expand() } + } + .sizeIn(maxHeight = maxHeight - statusBarHeight), + ) { + Hand(Modifier.drawBehind { drawRect(backgroundColor.value) }) + + Box( + modifier = Modifier + .onFocusChanged { + isSearchFieldFocused = it.isFocused + }, + ) { + bottomSheetContent() + } + } + }, + content = { paddingValues -> + Box { + Column( + modifier = Modifier.hazeSourceTangem(-1f), + ) { + content(paddingValues) + } + + Surface( + color = Color.Unspecified, + contentColor = Color.Unspecified, + modifier = Modifier + .hazeEffectTangem { + progressive = + HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) + }, + ) { + TangemTopBar( + title = stringReference(""), // todo balance + startIconRes = R.drawable.ic_tangem_24, + endIconRes = R.drawable.ic_more_default_24, + onEndContentClick = state.topBarConfig.onDetailsClick, + isGhostButtons = !isPowerSaving, + modifier = Modifier + .testTag(MainScreenTestTags.TOP_BAR), + ) + } + + BottomSheetScrim( + color = if (state.showMarketsOnboarding) { + Color.Black.copy(alpha = .65f) + } else { + Color.Black.copy(alpha = .40f) + }, + visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || + state.showMarketsOnboarding, + onDismissRequest = { + coroutineScope.launch { bottomSheetState.partialExpand() } + state.onDismissMarketsTooltip() + }, + ) + } + }, + ) + + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), + visible = isNavBarVisible.value, + ) { + Box( + Modifier + .align(Alignment.BottomCenter) + .background(backgroundColor.value) + .height(bottomBarHeight) + .fillMaxWidth(), + ) + } + } + + LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { + if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) { + state.onDismissMarketsTooltip() + } + } + } +} + +@Composable +private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = tween(), + label = "scrim", + ) + val dismissSheet = if (visible) { + Modifier + .pointerInput(onDismissRequest) { + detectTapGestures { + onDismissRequest() + } + } + .clearAndSetSemantics {} + } else { + Modifier + } + Canvas( + Modifier + .fillMaxSize() + .then(dismissSheet), + ) { + drawRect(color = color, alpha = alpha) + } +} + +@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod") +@Composable +private fun BottomSheetStateEffects( + bottomSheetState: TangemSheetState, + navigationBarVisible: MutableState, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + isSearchFieldFocused: Boolean, +) { + LaunchedEffect(bottomSheetState.targetValue) { + when (bottomSheetState.targetValue) { + TangemSheetValue.Hidden, + TangemSheetValue.Expanded, + -> navigationBarVisible.value = false + TangemSheetValue.PartiallyExpanded, + -> navigationBarVisible.value = true + } + } + + // expand bottom sheet when keyboard appears + val isKeyboardVisible by rememberIsKeyboardVisible() + + LaunchedEffect(isKeyboardVisible) { + if (isKeyboardVisible && isSearchFieldFocused) { + bottomSheetState.expand() + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + // hide keyboard when bottom sheet is about to be hidden + LaunchedEffect(Unit) { + snapshotFlow { + bottomSheetState.currentValue == TangemSheetValue.Expanded && + bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded + }.collect { sheetHasBeenHidden -> + if (sheetHasBeenHidden) { + keyboardController?.hide() + } + } + } + + val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded + LaunchedEffect(isSheetHidden) { + onBottomSheetStateChange( + if (isSheetHidden) { + BottomSheetState.COLLAPSED + } else { + BottomSheetState.EXPANDED + }, + ) + } +} + +@Composable +private fun WalletSnackbarHost( + snackbarHostState: SnackbarHostState, + event: StateEvent, + modifier: Modifier = Modifier, +) { + SnackbarHost(hostState = snackbarHostState, modifier = modifier) { data -> + if (event is StateEvent.Triggered && event.data is WalletEvent.CopyAddress) { + CopiedTextSnackbar(data) + } else { + TangemSnackbar(data) + } + } +} + +// region Preview +@OptIn(ExperimentalDecomposeApi::class) +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider::class) data: WalletScreenState) { + TangemThemePreviewRedesign { + WalletScreen2( + state = data, + bottomSheetContent = { + Text("Markets Content") + }, + bottomSheetHeaderHeightProvider = { 10.dp }, + onBottomSheetStateChange = {}, + ) + } +} + +private class WalletScreen2PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + walletScreenState, + walletScreenState.copy(selectedWalletIndex = 1), + accountScreenState.copy(selectedWalletIndex = 1), + accountScreenWithEmptyTokensState.copy(selectedWalletIndex = 1), + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt new file mode 100644 index 0000000000..8759ecdf11 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt @@ -0,0 +1,422 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.SubcomposeAsyncImage +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM.Content.CollectionPreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletNFTItem2(state: WalletNFTItemUM, modifier: Modifier = Modifier) { + val nftModifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(TangemTheme.colors2.surface.level3) + when (state) { + is WalletNFTItemUM.Hidden -> Unit + is WalletNFTItemUM.Empty -> WalletNFTItemEmpty( + modifier = nftModifier, + onClick = state.onItemClick, + ) + is WalletNFTItemUM.Failed -> WalletNFTItemFailed(modifier = nftModifier) + is WalletNFTItemUM.Loading -> WalletNFTItemLoading(modifier = nftModifier) + + is WalletNFTItemUM.Content -> WalletNFTItemContent( + state = state, + onClick = state.onItemClick, + modifier = nftModifier, + ) + } +} + +@Composable +private fun WalletNFTItemEmpty(onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier.clickableSingle( + onClick = onClick, + ), + ) { + Image( + painter = painterResource(R.drawable.img_nft_empty_collection), + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .layoutId(TangemRowLayoutId.HEAD), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_receive_nft), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.layoutId(TangemRowLayoutId.TAIL), + ) + } +} + +@Composable +private fun WalletNFTItemContent(state: WalletNFTItemUM.Content, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier.clickableSingle( + onClick = onClick, + ), + ) { + Box(modifier = Modifier.layoutId(TangemRowLayoutId.HEAD)) { + CollectionsPreviews( + previews = state.previews, + ) + } + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + isEnabled = state.isFlickering, + textColor = TangemTheme.colors2.text.neutral.primary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(horizontal = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe( + id = R.string.nft_wallet_count, + state.allAssetsCount, + state.collectionsCount, + ), + style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( + isEnabled = state.isFlickering, + textColor = TangemTheme.colors2.text.neutral.secondary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(horizontal = TangemTheme.dimens2.x2), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.layoutId(TangemRowLayoutId.TAIL), + ) + } +} + +@Composable +private fun WalletNFTItemFailed(modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier, + ) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)) + .background(TangemTheme.colors2.skeleton.backgroundPrimary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x5), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_unable_to_load), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2), + ) + } +} + +@Composable +private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)) + .background(TangemTheme.colors2.skeleton.backgroundPrimary) + .layoutId(TangemRowLayoutId.HEAD), + ) + TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + radius = TangemTheme.dimens2.x25, + + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2) + .width(TangemTheme.dimens.size110), + ) + TextShimmer( + style = TangemTheme.typography2.captionSemibold12, + radius = TangemTheme.dimens2.x25, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2) + .width(TangemTheme.dimens.size80), + ) + } +} + +@Composable +@Suppress("MagicNumber", "ReusedModifierInstance") +private fun BoxScope.CollectionsPreviews(previews: ImmutableList, modifier: Modifier = Modifier) { + val modifiers = when (previews.size) { + 1 -> previews1Modifiers() + 2 -> previews2Modifiers() + 3 -> previews3Modifiers() + else -> previews4Modifiers() + } + Box( + modifier = modifier + .size(TangemTheme.dimens2.x10), + ) { + previews.take(modifiers.size).forEachIndexed { index, s -> + val previewModifier = modifiers[index] + when (s) { + is CollectionPreview.Image -> { + SubcomposeAsyncImage( + modifier = previewModifier, + model = s.url, + loading = { + RectangleShimmer() + }, + error = { + Box( + modifier = previewModifier.background(TangemTheme.colors2.surface.level2), + ) + }, + contentDescription = null, + ) + } + is CollectionPreview.More -> { + Icon( + modifier = previewModifier + .background(TangemTheme.colors2.surface.level2), + imageVector = ImageVector.vectorResource(R.drawable.ic_nft_preview_more_16), + tint = TangemTheme.colors2.text.neutral.secondary, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun previews1Modifiers(): List = listOf( + Modifier + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)), +) + +@Composable +private fun BoxScope.previews2Modifiers(): List = listOf( + Modifier + .padding(start = TangemTheme.dimens2.x0_5, top = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x6) + .clip(RoundedCornerShape(TangemTheme.dimens2.x2)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding(TangemTheme.dimens2.x0_5) + .clip(RoundedCornerShape(topStart = 10.dp)) + .background(TangemTheme.colors2.surface.level3) + .padding(start = TangemTheme.dimens2.x0_5, top = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x6) + .clip(RoundedCornerShape(TangemTheme.dimens2.x2)) + .align(Alignment.BottomEnd), +) + +@Composable +private fun BoxScope.previews3Modifiers(): List = listOf( + Modifier + .padding(start = 3.dp, top = 3.dp) + .size(17.8.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding(top = 10.dp, end = 1.dp) + .clip(RoundedCornerShape(8.dp)) + .background(TangemTheme.colors2.surface.level3) + .padding(TangemTheme.dimens2.x0_5) + .size(18.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.TopEnd), + Modifier + .padding(start = 9.dp, top = 2.dp) + .size(14.dp) + .clip(RoundedCornerShape(4.dp)) + .align(Alignment.BottomStart), +) + +@Composable +private fun BoxScope.previews4Modifiers(): List = listOf( + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.TopStart), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.TopEnd), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.BottomStart), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.BottomEnd), +) + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider2::class) state: WalletNFTItemUM) { + TangemThemePreviewRedesign { + WalletNFTItem2( + state = state, + modifier = Modifier + .background(TangemTheme.colors2.surface.level1), + ) + } +} + +private class WalletNFTItemProvider2 : CollectionPreviewParameterProvider( + collection = listOf( + WalletNFTItemUM.Empty( + onItemClick = { }, + ), + WalletNFTItemUM.Loading, + WalletNFTItemUM.Failed, + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = true, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.Image("img4"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.More, + ), + allAssetsCount = 125, + collectionsCount = 11, + isFlickering = true, + noCollectionAssetsCount = 0, + onItemClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt deleted file mode 100644 index ff2d45177d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.common - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig - -/** - * Wallet bottom sheet with detail notification information - * - * @param config component config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun WalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: WalletBottomSheetConfig -> - BottomSheetContent(config = content) - } -} - -@Composable -private fun BottomSheetContent(config: WalletBottomSheetConfig) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size48), - tint = when (config) { - is WalletBottomSheetConfig.UnlockWallets -> TangemTheme.colors.icon.primary1 - }, - ) - - Column( - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = config.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - Text( - text = config.subtitle.resolveReference(), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body2, - ) - } - - Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { - val buttonModifier = Modifier.fillMaxWidth() - - PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier) - - SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) - } - } -} - -@Composable -private fun PrimaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { - if (config.iconResId == null) { - PrimaryButton( - text = config.text.resolveReference(), - onClick = config.onClick, - modifier = modifier, - ) - } else { - PrimaryButtonIconStart( - text = config.text.resolveReference(), - iconResId = config.iconResId, - onClick = config.onClick, - modifier = modifier, - ) - } -} - -@Composable -private fun SecondaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { - if (config.iconResId == null) { - SecondaryButton( - text = config.text.resolveReference(), - onClick = config.onClick, - modifier = modifier, - ) - } else { - SecondaryButtonIconStart( - text = config.text.resolveReference(), - iconResId = config.iconResId, - onClick = config.onClick, - modifier = modifier, - ) - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun WalletBottomSheetContent_Preview( - @PreviewParameter(WalletBottomSheetConfigProvider::class) - config: WalletBottomSheetConfig, -) { - TangemThemePreview { - // Use preview of content because ModalBottomSheet isn't supported in Preview mode - BottomSheetContent(config = config) - } -} - -private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewData.bottomSheet.content as WalletBottomSheetConfig), -) -// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 1ddf7dd2cf..29616c5969 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -163,7 +163,7 @@ private fun LazyListScope.portfolioItem( @Suppress("MagicNumber") @Composable -private fun SlideInItemVisibility( +internal fun SlideInItemVisibility( visible: Boolean, currentIndex: Int, lastIndex: Int, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 7d1be99d66..c3755c12bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -1,5 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency +import androidx.compose.animation.* +import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -8,22 +12,37 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.lerp import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.row.header.TangemHeaderRow +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.core.ui.utils.ProvideSharedTransitionScope import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM import kotlinx.collections.immutable.ImmutableList internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" @@ -59,6 +78,180 @@ internal fun LazyListScope.tokensListItems( } } +/** + * LazyList extension for [WalletTokensListState] + * + * @param walletTokensListUM state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.tokensListItems2( + walletTokensListUM: WalletTokensListUM, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + when (walletTokensListUM) { + is WalletTokensListUM.Loading, + is WalletTokensListUM.Content, + -> { + walletTokensListUM.tokenList.fastForEachIndexed { index, listItem -> + when (listItem) { + is TokensListItemUM2.GroupTitle, + is TokensListItemUM2.Token, + -> tokenItem( + listItem = listItem, + index = index, + lastIndex = walletTokensListUM.tokenList.lastIndex, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + is TokensListItemUM2.Portfolio -> portfolioItem( + listItem = listItem, + index = index, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + } + } + WalletTokensListUM.Empty -> nonContentItem(modifier = modifier) + } +} + +private fun LazyListScope.tokenItem( + listItem: TokensListItemUM2, + index: Int, + lastIndex: Int, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + item( + key = listItem.tokenRowUM.id, + contentType = listItem.tokenRowUM::class.java, + ) { + val itemModifier = modifier + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + .padding(top = if (index == 0) TangemTheme.dimens2.x3 else 0.dp) + .roundedShapeItemDecoration( + radius = 18.dp, + currentIndex = index, + addDefaultPadding = false, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + ) + + when (val tokenRowUM = listItem.tokenRowUM) { + is TangemTokenRowUM -> TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableTokenListState = null, + modifier = itemModifier, + ) + is TangemHeaderRowUM -> TangemHeaderRow( + headerRowUM = tokenRowUM, + modifier = itemModifier, + ) + } + } +} + +private fun LazyListScope.portfolioItem( + listItem: TokensListItemUM2.Portfolio, + index: Int, + isBalanceHidden: Boolean, + modifier: Modifier, +) { + val lastIndex = listItem.tokenList.lastIndex + 1 + + accountItem( + listItem = listItem, + modifier = modifier, + index = index, + lastIndex = lastIndex, + isBalanceHidden = isBalanceHidden, + ) + itemsIndexed( + items = listItem.tokenList, + key = { _, item -> item.tokenRowUM.id }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, item -> + SlideInItemVisibility( + currentIndex = tokenIndex + 1, + lastIndex = lastIndex, + modifier = modifier + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = 18.dp, + currentIndex = tokenIndex + 1, + addDefaultPadding = false, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + ), + visible = listItem.isExpanded, + ) { + val itemModifier = Modifier + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = tokenIndex + 1 } + + when (val tokenRowUM = item.tokenRowUM) { + is TangemTokenRowUM -> TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableTokenListState = null, + modifier = itemModifier, + ) + is TangemHeaderRowUM -> TangemHeaderRow( + headerRowUM = tokenRowUM, + modifier = itemModifier, + ) + } + } + }, + ) +} + +private fun LazyListScope.accountItem( + listItem: TokensListItemUM2.Portfolio, + modifier: Modifier, + index: Int, + lastIndex: Int, + isBalanceHidden: Boolean, +) { + item( + key = listItem.tokenRowUM.id, + contentType = listItem.tokenRowUM::class.java, + ) { + val portfolioModifier = modifier + .padding(top = if (index != 0) TangemTheme.dimens2.x2 else TangemTheme.dimens2.x3) + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + .roundedShapeItemDecoration( + currentIndex = 0, + radius = 18.dp, + addDefaultPadding = false, + lastIndex = if (listItem.isExpanded) lastIndex else 0, + backgroundColor = TangemTheme.colors2.surface.level3, + ) + if (listItem.isCollapsable) { + PortfolioRowItem( + item = listItem, + isBalanceHidden = isBalanceHidden, + modifier = portfolioModifier, + ) + } else { + TangemHeaderRow( + title = (listItem.tokenRowUM.titleUM as? TangemTokenRowUM.TitleUM.Content)?.text.orEmpty(), + subtitle = (listItem.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content) + ?.text?.orMaskWithStars(isBalanceHidden), + headTangemIconUM = listItem.tokenRowUM.headIconUM, + modifier = portfolioModifier, + ) + } + } +} + private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -77,6 +270,7 @@ private fun LazyListScope.contentItems( currentIndex = index, lastIndex = items.lastIndex, backgroundColor = TangemTheme.colors.background.primary, + radius = 18.dp, ) .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) .semantics { lazyListItemPosition = index }, @@ -85,6 +279,117 @@ private fun LazyListScope.contentItems( ) } +@Suppress("MagicNumber", "ReusedModifierInstance", "LongMethod") +@Composable +internal fun PortfolioRowItem( + item: TokensListItemUM2.Portfolio, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + // TangemSharedTransitionLayout { + ProvideSharedTransitionScope(modifier) { + val iconSharedContentState = rememberSharedContentState(key = "icon") + val titleSharedContentState = rememberSharedContentState(key = "title") + val boundsTransform = BoundsTransform { _, _ -> tween(250) } + + AnimatedContent( + item.isExpanded, + transitionSpec = { + fadeIn(animationSpec = tween(350, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(350))) + }, + ) { isExpandedWrapped -> + val animatedContentScope = this + + val composables = remember { + SharedTokenRowComposables( + icon = { modifier -> + val size = if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default + val currencyIconState = + when (val currencyIconState = item.tokenRowUM.headIconUM.currencyIconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> + currencyIconState.copy(size = size) + is CurrencyIconState.CryptoPortfolio.Letter -> + currencyIconState.copy(size = size) + else -> currencyIconState + } + + TangemIcon( + tangemIconUM = item.tokenRowUM.headIconUM.copy(currencyIconState = currencyIconState), + modifier = modifier.sharedBounds( + sharedContentState = iconSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + ), + ) + }, + title = { modifier -> + val targetAnimationFraction = if (isExpandedWrapped) 0f else 1f + + val animationFraction = animateFloatAsState( + targetValue = targetAnimationFraction, + animationSpec = tween(durationMillis = 350), + ) + + val startStyle = TangemTheme.typography2.captionSemibold12 + val stopStyle = TangemTheme.typography2.bodySemibold16 + + val textStyle by remember(animationFraction.value) { + derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) } + } + + val resizedTitle = when (val titleUM = item.tokenRowUM.titleUM) { + is TangemTokenRowUM.TitleUM.Content -> titleUM.copy( + text = styledStringReference( + titleUM.text.resolveReference(), + { textStyle.toSpanStyle() }, + ), + ) + else -> titleUM + } + + TokenRowTitle( + titleUM = resizedTitle, + modifier = modifier.sharedBounds( + sharedContentState = titleSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart), + ), + ) + }, + ) + } + + if (isExpandedWrapped) { + TangemHeaderRow( + subtitle = (item.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content) + ?.text?.orMaskWithStars(isBalanceHidden), + titleContent = composables.title, + headContent = composables.icon, + footerTangemIconRes = R.drawable.ic_minimize_24, + onItemClick = item.tokenRowUM.onItemClick, + ) + } else { + TangemTokenRow( + tokenRowUM = item.tokenRowUM, + headComponent = composables.icon, + titleComponent = composables.title, + isBalanceHidden = isBalanceHidden, + reorderableTokenListState = null, + ) + } + } + // } + } +} + +@Stable +class SharedTokenRowComposables( + val title: @Composable (Modifier) -> Unit, + val icon: @Composable (Modifier) -> Unit, +) + private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( key = NON_CONTENT_TOKENS_LIST_KEY, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index b8f7e9a587..fbf1117370 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @Composable @@ -36,6 +38,17 @@ private fun TangemPayMainScreenBlockPreview() { TangemThemePreview { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false) + TangemPayMainScreenBlock( + state = TangemPayState.RefreshNeeded( + TangemPayRefreshNeeded( + tangemIcon = R.drawable.ic_tangem_24, + buttonText = resourceReference(id = R.string.home_button_scan), + onRefreshClick = {}, + shouldShowProgress = false, + ), + ), + isBalanceHidden = false, + ) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) 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 b163d627d6..0242c1bf72 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 @@ -6,7 +6,6 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetWalletTotalBalanceUseCaseV2 import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError @@ -20,7 +19,6 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R @@ -39,8 +37,6 @@ import kotlinx.coroutines.flow.* @Suppress("LongParameterList") internal class DefaultUserWalletsFetcher @AssistedInject constructor( getWalletsUseCase: GetWalletsUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getWalletTotalBalanceUseCaseV2: GetWalletTotalBalanceUseCaseV2, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -103,11 +99,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( // We should not load balances in auth mode flowOf(Lce.Loading(walletIds.associateWith { TotalFiatBalance.Loading })) } else { - if (accountsFeatureToggles.isFeatureEnabled) { - getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds) - } else { - getWalletTotalBalanceUseCase(walletIds).distinctUntilChanged() - } + getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds) } } diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt deleted file mode 100644 index 37c2e71379..0000000000 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.yield.supply.api - -interface YieldSupplyFeatureToggles { - - val isYieldSupplyFeatureEnabled: Boolean - val isYieldSupplyPendingTransactionsEnabled: Boolean -} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt deleted file mode 100644 index 4f0a442329..0000000000 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.yield.supply.impl - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles - -internal class DefaultYieldSupplyFeatureToggles( - private val featureToggles: FeatureTogglesManager, -) : YieldSupplyFeatureToggles { - override val isYieldSupplyFeatureEnabled: Boolean - get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED") - - override val isYieldSupplyPendingTransactionsEnabled: Boolean - get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED") -} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt index 00958d2ec1..078a18f3e9 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt @@ -1,7 +1,6 @@ package com.tangem.features.yield.supply.impl.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -24,6 +23,7 @@ class YieldSupplyAlertFactory @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -41,35 +41,17 @@ class YieldSupplyAlertFactory @Inject constructor( } fun getSendTransactionErrorState( - error: SendTransactionError?, + error: SendTransactionError, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - uiMessageSender.send( - DialogMessage.Companion( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + uiMessageSender.send(errorDialog) } suspend fun onFailedTxEmailClick(userWallet: UserWallet, cryptoCurrency: CryptoCurrency?, errorMessage: String?) { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt deleted file mode 100644 index 45b8806bd6..0000000000 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.yield.supply.impl.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.yield.supply.impl.DefaultYieldSupplyFeatureToggles -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles -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 YieldSupplyFeatureModule { - - @Singleton - @Provides - fun provideYieldFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldSupplyFeatureToggles { - return DefaultYieldSupplyFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4c87fdffe7..68f64e61cf 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -102,6 +102,7 @@ markdownComposeView = "0.5.4" usedesk = "4.4.0" sumsub = "1.38.0" haze = "1.7.1" +kotlinpoet = "1.18.1" # endregion Other libraries # region Tools @@ -149,6 +150,7 @@ agconnect = { id = "com.huawei.agconnect", version.ref = "agconnect" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradle-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } # end region Classpath # region AndroidX diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ae62f1e18f..6934faad61 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.34-1430" +tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-578" +tangemCardSdk = "develop-582" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index f4ca59e975..a812c22529 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -1,8 +1,8 @@ package com.tangem.blockchainsdk +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -15,16 +15,16 @@ internal typealias BlockchainProvidersResponse = Map /** * Implementation of Blockchain SDK components factory * + * @property blockchainSdkConfig blockchain SDK config * @property blockchainProvidersTypesManager blockchain providers types manager - * @property environmentConfigStorage environment config storage * @property walletManagerFactoryCreator wallet manager factory creator * @param dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultBlockchainSDKFactory( + private val blockchainSdkConfig: BlockchainSdkConfig, private val blockchainProvidersTypesManager: BlockchainProvidersTypesManager, - private val environmentConfigStorage: EnvironmentConfigStorage, private val walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ) : BlockchainSDKFactory { @@ -43,7 +43,7 @@ internal class DefaultBlockchainSDKFactory( private fun createWalletManagerFactory(): Flow { return combine( - flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig }, + flow = flowOf(blockchainSdkConfig), flow2 = blockchainProvidersTypesManager.get(), // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index a4f5188846..6c0fb03aca 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -7,7 +7,6 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchainsdk.providers.BlockchainProviderTypes -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import timber.log.Timber import javax.inject.Inject @@ -24,7 +23,6 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val featureTogglesManager: FeatureTogglesManager, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -35,10 +33,8 @@ internal class WalletManagerFactoryCreator @Inject constructor( blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( - isYieldSupplyEnabled = featureTogglesManager.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED"), - isPendingTransactionsEnabled = featureTogglesManager.isFeatureEnabled( - "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", - ), + isYieldSupplyEnabled = true, + isPendingTransactionsEnabled = true, ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 9abd481655..a8c5f4b3f5 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -17,10 +17,9 @@ import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.libs.blockchain_sdk.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -40,14 +39,14 @@ internal object BlockchainSDKFactoryModule { @Provides @Singleton fun provideBlockchainSDKFactory( + environmentConfig: EnvironmentConfig, blockchainProvidersTypesManager: BlockchainProvidersTypesManager, - environmentConfigStorage: EnvironmentConfigStorage, walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ): BlockchainSDKFactory { return DefaultBlockchainSDKFactory( + blockchainSdkConfig = environmentConfig.blockchainSdkConfig, blockchainProvidersTypesManager = blockchainProvidersTypesManager, - environmentConfigStorage = environmentConfigStorage, walletManagerFactoryCreator = walletManagerFactoryCreator, dispatchers = dispatchers, ) @@ -91,13 +90,11 @@ internal object BlockchainSDKFactoryModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, blockchainSDKLogger: BlockchainSDKLogger, - featureTogglesManager: FeatureTogglesManager, ): WalletManagerFactoryCreator { return WalletManagerFactoryCreator( accountCreator = DefaultAccountCreator(tangemTechApi), blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), blockchainSDKLogger = blockchainSDKLogger, - featureTogglesManager = featureTogglesManager, ) } } \ No newline at end of file diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index a8bc7da8a0..4d9ccbce70 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -17,6 +17,15 @@ dependencies { implementation(deps.gradle.kotlin) implementation(deps.gradle.android) implementation(deps.gradle.detekt) + implementation(deps.gradle.kotlinpoet) + implementation(deps.kotlin.serialization) + + testImplementation(deps.test.junit5) + testImplementation(deps.test.truth) +} + +tasks.withType { + useJUnitPlatform() } gradlePlugin { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt new file mode 100644 index 0000000000..89206fda87 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt @@ -0,0 +1,165 @@ +package com.tangem.plugin.configuration.configurations + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlinx.serialization.json.* +import java.io.File + +/** + * Generator for environment configuration Kotlin object from JSON file. + * Automatically parses JSON structure and generates corresponding Kotlin code. + * +[REDACTED_AUTHOR] + */ +object EnvironmentConfigGenerator { + + private const val PACKAGE_NAME = "com.tangem.datasource.local.config.environment.generated" + private const val CLASS_NAME = "GeneratedEnvironmentConfig" + + /** + * Generates GeneratedEnvironmentConfig object from JSON file. + * + * @param inputFile JSON configuration file + * @param outputDir Output directory for generated Kotlin file + */ + fun generate(inputFile: File, outputDir: File) { + val jsonText = inputFile.readText() + val json = Json.parseToJsonElement(jsonText).jsonObject + + val objectBuilder = TypeSpec.objectBuilder(CLASS_NAME) + .addKdoc("Generated from ${inputFile.name}\nAuto-generated - do not edit manually.") + + // Iterate over all JSON keys and generate properties + json.entries.forEach { (key, value) -> + addPropertyFromJsonValue(objectBuilder, key, value) + } + + val fileSpec = FileSpec.builder(PACKAGE_NAME, CLASS_NAME) + .indent(" ") // Use 4 spaces for indentation + .addType(objectBuilder.build()) + .build() + + outputDir.mkdirs() + fileSpec.writeTo(outputDir) + + // Post-process generated file + val generatedFile = File(outputDir, PACKAGE_NAME.replace('.', '/') + "/$CLASS_NAME.kt") + if (generatedFile.exists()) { + val content = generatedFile.readText() + val fixedContent = content + // Add suppress annotation at file level + .replaceFirst( + "package $PACKAGE_NAME", + "@file:Suppress(\n" + + " \"MaximumLineLength\",\n" + + " \"MaxLineLength\",\n" + + " \"Indentation\",\n" + + ")\n\npackage $PACKAGE_NAME" + ) + // Remove redundant public modifiers + .replace("public object ", "object ") + .replace("public val ", "val ") + .replace("public const val ", "const val ") + generatedFile.writeText(fixedContent) + } + } + + /** + * Adds a property to the TypeSpec based on the JSON value type + */ + private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) { + when (value) { + is JsonPrimitive -> { + when { + value.isString -> { + val stringValue = value.content + val isNullable = stringValue.isEmpty() + val propertySpec = PropertySpec.builder(name, STRING.copy(nullable = isNullable)) + .initializer(if (isNullable) "null" else "%S", stringValue) + + // Add const modifier for non-nullable strings + if (!isNullable) { + propertySpec.addModifiers(KModifier.CONST) + } + + builder.addProperty(propertySpec.build()) + } + value.booleanOrNull != null -> { + builder.addProperty( + PropertySpec.builder(name, BOOLEAN) + .addModifiers(KModifier.CONST) + .initializer("%L", value.boolean) + .build() + ) + } + value.longOrNull != null -> { + builder.addProperty( + PropertySpec.builder(name, LONG) + .addModifiers(KModifier.CONST) + .initializer("%L", value.long) + .build() + ) + } + value.doubleOrNull != null -> { + builder.addProperty( + PropertySpec.builder(name, DOUBLE) + .addModifiers(KModifier.CONST) + .initializer("%L", value.double) + .build() + ) + } + else -> { + // Null value + builder.addProperty( + PropertySpec.builder(name, STRING.copy(nullable = true)) + .initializer("null") + .build() + ) + } + } + } + is JsonArray -> { + val listType = LIST.parameterizedBy(STRING) + val values = value.map { it.jsonPrimitive.content } + builder.addProperty( + PropertySpec.builder(name, listType) + .initializer( + CodeBlock.builder() + .add("listOf(\n") + .apply { + values.forEach { v -> + add(" %S,\n", v) + } + } + .add(")") + .build() + ) + .build() + ) + } + is JsonObject -> { + // Generate nested object with proper naming (convert dashes to camelCase) + val nestedClassName = name.toPascalCase() + val nestedObjectBuilder = TypeSpec.objectBuilder(nestedClassName) + + value.entries.forEach { (nestedKey, nestedValue) -> + addPropertyFromJsonValue(nestedObjectBuilder, nestedKey, nestedValue) + } + + builder.addType(nestedObjectBuilder.build()) + } + } + } + + /** + * Converts a string to PascalCase, handling dashes and underscores. + * Examples: "cosmos-hub" -> "CosmosHub", "polygon-zkevm" -> "PolygonZkevm" + */ + private fun String.toPascalCase(): String { + return this.split("-", "_") + .filter { it.isNotEmpty() } + .joinToString("") { part -> + part.replaceFirstChar { it.uppercase() } + } + } +} diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt index 644b1eaead..20b67b7b6d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt @@ -5,4 +5,5 @@ import org.gradle.api.Project internal fun Project.configure() { configureKotlinCompilerOptions() configureDetektRules() + configureTestLogging() } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt new file mode 100644 index 0000000000..5f5c3d5d9c --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt @@ -0,0 +1,48 @@ +package com.tangem.plugin.configuration.configurations + +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.TestDescriptor +import org.gradle.api.tasks.testing.TestListener +import org.gradle.api.tasks.testing.TestResult +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import java.io.Serializable + +internal fun Project.configureTestLogging() { + tasks.withType(Test::class.java).configureEach { + println("Test task scheduled: $path") + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } + addTestListener(TestSuiteLogger(path)) + } +} + +private class TestSuiteLogger(private val taskPath: String) : TestListener, Serializable { + override fun beforeSuite(suite: TestDescriptor) {} + + override fun afterSuite(suite: TestDescriptor, result: TestResult) { + if (suite.parent == null) { + val output = + "$taskPath - Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" + val startItem = "| " + val endItem = " |" + val repeatLength = startItem.length + output.length + endItem.length + println( + "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( + repeatLength, + ), + ) + } + } + + override fun beforeTest(testDescriptor: TestDescriptor) {} + override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {} + + companion object { + private const val serialVersionUID = 1L + } +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 0ab5e8f747..c15761c0d9 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -1,11 +1,11 @@ package com.tangem.plugin.configuration.model -internal enum class BuildType( +enum class BuildType( val id: String, - val appIdSuffix: String? = null, - val versionSuffix: String? = null, - val obfuscating: Boolean = false, - val configFields: List, + internal val appIdSuffix: String? = null, + internal val versionSuffix: String? = null, + internal val obfuscating: Boolean = false, + internal val configFields: List, ) { /** @@ -117,4 +117,19 @@ internal enum class BuildType( BuildConfigField.ABTestsEnabled(isEnabled = false), ), ), + ; + + /** Returns the environment value (dev/prod) for this build type */ + val environment: String + get() { + val environmentField = configFields + .filterIsInstance() + .firstOrNull() + + requireNotNull(environmentField) { + "BuildType '$id' must have a BuildConfigField.Environment in configFields" + } + + return environmentField.value.removeSurrounding("\"") + } } \ No newline at end of file diff --git a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt new file mode 100644 index 0000000000..bfcccb71a8 --- /dev/null +++ b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt @@ -0,0 +1,473 @@ +package com.tangem.plugin.configuration.configurations + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +/** + * Tests for [EnvironmentConfigGenerator] covering JSON parsing edge cases. + */ +class EnvironmentConfigGeneratorTest { + + @TempDir + lateinit var tempDir: File + + private lateinit var outputDir: File + + @BeforeEach + fun setup() { + outputDir = File(tempDir, "output") + } + + @Test + fun `generate handles string values correctly`() { + // Arrange + val json = """ + { + "apiKey": "test-api-key", + "baseUrl": "https://example.com" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("""const val apiKey: String = "test-api-key"""") + assertThat(generatedCode).contains("""const val baseUrl: String = "https://example.com"""") + } + + @Test + fun `generate handles empty string as nullable`() { + // Arrange + val json = """ + { + "emptyValue": "" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val emptyValue: String? = null") + } + + @Test + fun `generate handles null values`() { + // Arrange + val json = """ + { + "nullValue": null + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val nullValue: String? = null") + } + + @Test + fun `generate handles boolean values`() { + // Arrange + val json = """ + { + "isEnabled": true, + "isDisabled": false + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val isEnabled: Boolean = true") + assertThat(generatedCode).contains("const val isDisabled: Boolean = false") + } + + @Test + fun `generate handles integer values as Long`() { + // Arrange + val json = """ + { + "count": 42, + "negativeNumber": -100, + "largeNumber": 9223372036854775807 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val count: Long = 42") + assertThat(generatedCode).contains("const val negativeNumber: Long = -100") + // KotlinPoet formats large numbers with underscores + assertThat(generatedCode).contains("const val largeNumber: Long = 9_223_372_036_854_775_807") + } + + @Test + fun `generate handles double values`() { + // Arrange + val json = """ + { + "ratio": 3.14, + "negativeDouble": -2.5 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val ratio: Double = 3.14") + assertThat(generatedCode).contains("const val negativeDouble: Double = -2.5") + } + + @Test + fun `generate handles string arrays`() { + // Arrange + val json = """ + { + "items": ["one", "two", "three"] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val items: List = listOf(") + assertThat(generatedCode).contains(""""one",""") + assertThat(generatedCode).contains(""""two",""") + assertThat(generatedCode).contains(""""three",""") + } + + @Test + fun `generate handles empty arrays`() { + // Arrange + val json = """ + { + "emptyList": [] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val emptyList: List = listOf(") + } + + @Test + fun `generate handles nested objects`() { + // Arrange + val json = """ + { + "database": { + "host": "localhost", + "port": 5432 + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Database {") + assertThat(generatedCode).contains("""const val host: String = "localhost"""") + // KotlinPoet formats numbers >= 1000 with underscores + assertThat(generatedCode).contains("const val port: Long = 5_432") + } + + @Test + fun `generate handles deeply nested objects`() { + // Arrange + val json = """ + { + "level1": { + "level2": { + "level3": { + "deepValue": "deep" + } + } + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Level1 {") + assertThat(generatedCode).contains("object Level2 {") + assertThat(generatedCode).contains("object Level3 {") + assertThat(generatedCode).contains("""const val deepValue: String = "deep"""") + } + + @Test + fun `generate converts dash-separated names to PascalCase`() { + // Arrange + val json = """ + { + "cosmos-hub": { + "chainId": "cosmoshub-4" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CosmosHub {") + } + + @Test + fun `generate converts underscore-separated names to PascalCase`() { + // Arrange + val json = """ + { + "api_config": { + "timeout": 30 + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object ApiConfig {") + } + + @Test + fun `generate handles consecutive dashes in names`() { + // Arrange + val json = """ + { + "cosmos--hub": { + "testValue": "test" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CosmosHub {") + } + + @Test + fun `generate handles trailing dash in names`() { + // Arrange + val json = """ + { + "config-": { + "testValue": "test" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Config {") + } + + @Test + fun `generate handles special characters in string values`() { + // Arrange + val json = """ + { + "query": "SELECT * FROM users WHERE name = 'John'", + "path": "C:\\Users\\test", + "newline": "line1\nline2", + "unicode": "Hello 世界" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val query: String") + assertThat(generatedCode).contains("const val path: String") + assertThat(generatedCode).contains("const val newline: String") + assertThat(generatedCode).contains("const val unicode: String") + } + + @Test + fun `generate handles arrays with special characters`() { + // Arrange + val json = """ + { + "urls": [ + "https://api.example.com/v1", + "https://api.example.com/v2?key=value&other=1" + ] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val urls: List = listOf(") + assertThat(generatedCode).contains(""""https://api.example.com/v1",""") + } + + @Test + fun `generate adds file suppress annotations`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("@file:Suppress(") + assertThat(generatedCode).contains(""""MaximumLineLength"""") + assertThat(generatedCode).contains(""""MaxLineLength"""") + assertThat(generatedCode).contains(""""Indentation"""") + } + + @Test + fun `generate creates proper package declaration`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("package com.tangem.datasource.local.config.environment.generated") + } + + @Test + fun `generate creates object with correct name`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object GeneratedEnvironmentConfig {") + } + + @Test + fun `generate adds kdoc with source file reference`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("Generated from") + assertThat(generatedCode).contains("Auto-generated - do not edit manually") + } + + @Test + fun `generate removes public modifiers`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).doesNotContain("public object") + assertThat(generatedCode).doesNotContain("public val") + assertThat(generatedCode).doesNotContain("public const val") + } + + @Test + fun `generate handles complex real-world config`() { + // Arrange + val json = """ + { + "tangemComApiKey": "api-key-123", + "moonPayApiKey": "moon-pay-key", + "moonPayApiSecretKey": "secret-key", + "mercuryoWidgetId": "", + "blockchainSdkConfig": { + "blockchairApiKey": "blockchair-key", + "blockcypherTokens": ["token1", "token2"], + "quickNodeSolanaCredentials": { + "apiKey": "solana-key", + "subdomain": "solana-node" + } + }, + "isFeatureEnabled": true, + "maxRetryCount": 3 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + // Top-level properties + assertThat(generatedCode).contains("""const val tangemComApiKey: String = "api-key-123"""") + assertThat(generatedCode).contains("val mercuryoWidgetId: String? = null") + assertThat(generatedCode).contains("const val isFeatureEnabled: Boolean = true") + assertThat(generatedCode).contains("const val maxRetryCount: Long = 3") + + // Nested object + assertThat(generatedCode).contains("object BlockchainSdkConfig {") + assertThat(generatedCode).contains("""const val blockchairApiKey: String = "blockchair-key"""") + assertThat(generatedCode).contains("val blockcypherTokens: List") + + // Deeply nested object + assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {") + } + + private fun generateAndReadOutput(jsonContent: String): String { + val inputFile = File(tempDir, "config.json").apply { + writeText(jsonContent) + } + + EnvironmentConfigGenerator.generate(inputFile, outputDir) + + val generatedFile = File( + outputDir, + "com/tangem/datasource/local/config/environment/generated/GeneratedEnvironmentConfig.kt" + ) + + assertThat(generatedFile.exists()).isTrue() + return generatedFile.readText() + } +} + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index d71fbf5bea..f77aab113a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,6 +300,9 @@ include(":features:token-recieve:impl") include(":features:yield-supply:api") include(":features:yield-supply:impl") +include(":features:approval:api") +include(":features:approval:impl") + include(":features:feed:api") include(":features:feed:impl") // endregion Feature modules @@ -349,6 +352,7 @@ include(":domain:manage-tokens") include(":domain:manage-tokens:models") include(":domain:onramp") include(":domain:onramp:models") +include(":domain:offramp") include(":domain:promo") include(":domain:promo:models") include(":domain:nft")