diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1514e89d5..7b732b629d 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) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index a7aa7666e3..ca62baa8b2 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -146,9 +146,7 @@ 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..da98e59b92 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -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 @@ -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..de3252872f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -73,10 +73,8 @@ 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 @@ -89,7 +87,6 @@ import timber.log.Timber lateinit var store: Store val foregroundActivityObserver = ForegroundActivityObserver -internal lateinit var derivationsFinder: DerivationsFinder open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider { @@ -190,9 +187,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() @@ -348,11 +342,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } - derivationsFinder = DerivationsFinder( - userTokensResponseStore = userTokensResponseStore, - dispatchers = dispatchers, - ) - appStateHolder.mainStore = store wcInitializeUseCase.init( 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/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..bc3f3f9376 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,8 @@ package com.tangem.tap.di.domain -import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.account.supplier.MultiAccountListSupplier 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 +18,13 @@ object EarnDomainModule { } @Provides - fun provideManageEarnNetworksUseCase( + fun provideGetEarnNetworksUseCase( earnRepository: EarnRepository, - userWalletsListRepository: UserWalletsListRepository, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + multiAccountListSupplier: MultiAccountListSupplier, ): GetEarnNetworksUseCase { return GetEarnNetworksUseCase( earnRepository = earnRepository, - userWalletsListRepository = userWalletsListRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, + multiAccountListSupplier = multiAccountListSupplier, ) } 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/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 11a3ffb59b..39cae1ec11 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 93a1c1f2be..e33ebc491c 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 @@ -44,11 +42,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?, @@ -79,7 +76,7 @@ internal class ScanProductTask( readVisaCard( session = session, cardDto = cardDto, - scanWalletProcessor = ScanWalletProcessor(derivationsFinder), + scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder), callback = callback, ) return @@ -87,7 +84,7 @@ internal class ScanProductTask( val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() - else -> ScanWalletProcessor(derivationsFinder) + else -> ScanWalletProcessor(blockchainToDeriveFinder) } commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { @@ -160,7 +157,7 @@ internal class ScanProductTask( } private class ScanWalletProcessor( - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null @@ -283,7 +280,6 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit, ) { val productType = getWalletProductType(card) - val config = CardConfig.createConfig(card) scope.launch { val scanResponse = ScanResponse( card = card, @@ -291,8 +287,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 @@ -322,32 +317,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/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 2b20696ee9..792b932178 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 @@ -25,7 +25,7 @@ class FinalizeTwinTask( is CompletionResult.Success -> ScanProductTask( card = readResult.data, - derivationsFinder = null, + blockchainToDeriveFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, onboardingV2FeatureToggles = null, 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/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/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..91e45c1573 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 @@ -177,14 +177,6 @@ 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") 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/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/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/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/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..23de2f59f4 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,22 +24,6 @@ "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" 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/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/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt index 3591920330..86d955f2d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R -import com.tangem.core.ui.components.flicker import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -58,7 +58,15 @@ fun TangemMessage( if (messageUM.iconUM != null) { TangemIcon( tangemIconUM = messageUM.iconUM, - modifier = Modifier.size(TangemTheme.dimens2.x8), + modifier = Modifier + .align( + if (messageUM.buttonsUM.isEmpty()) { + Alignment.CenterVertically + } else { + Alignment.Top + }, + ) + .size(TangemTheme.dimens2.x7), ) } }, @@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider @@ -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..150d6586d3 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 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..b838ad7601 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, @@ -270,8 +270,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, ) 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 626219cb64..60747a0d0c 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 @@ -7,7 +7,6 @@ import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency 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 @@ -16,7 +15,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 @@ -34,7 +32,6 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, - private val walletManagersFacade: WalletManagersFacade, ) : StakingRepository { override fun getStakingAvailability( @@ -42,7 +39,7 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, ): Flow { return channelFlow { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { send(StakingAvailability.Unavailable) return@channelFlow } @@ -78,7 +75,7 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { return StakingAvailability.Unavailable } @@ -118,31 +115,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..2c2683c14e 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -41,8 +41,6 @@ dependencies { /** 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) 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/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..4fc006ec5c 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,26 @@ package com.tangem.domain.earn.usecase import arrow.core.Either -import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier 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 +import kotlinx.coroutines.flow.map /** - * 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 userWalletsListRepository: UserWalletsListRepository, - private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + private val multiAccountListSupplier: MultiAccountListSupplier, ) { operator fun invoke(): Flow { @@ -36,24 +36,13 @@ 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 multiAccountListSupplier() + .map { accountLists -> + accountLists + .flatMap(AccountList::flattenCurrencies) + .map { it.network.backendId } + .toSet() } } } \ 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/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/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 559774d3e2..bc0e47c44b 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -26,8 +26,6 @@ dependencies { 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/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/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index bb91b6a704..c9385e437b 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,20 +114,23 @@ 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) -> stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) + error?.let(::handleBestOpportunitiesErrorAnalytics) }.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/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/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 0b7845919d..4939a58ede 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 @@ -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) @@ -218,17 +215,14 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: @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 +301,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 +311,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 +326,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 +492,7 @@ private fun EarnContentLoadingPreview() { ) { EarnContent( state = previewEarnUM( - mostlyUsed = EarnListUM.Loading, + mostlyUsed = EarnListUM.Error(onRetryClicked = {}), bestOpportunities = EarnBestOpportunitiesUM.Loading, ), ) @@ -588,8 +584,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/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/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..ff2921118b 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 @@ -8,8 +8,6 @@ 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.graphics.Brush @@ -19,6 +17,7 @@ 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 @@ -65,12 +64,6 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend @Composable private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { val listState = rememberLazyListState() - val articlesReadStatus = remember(news.content) { - news.content.map { it.isViewed } - } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) - } Column { Header( title = { @@ -104,10 +97,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) @@ -133,7 +130,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, ) { itemsIndexed( items = news.content, - key = { _, article -> article.id }, + key = { index, _ -> index }, contentType = { _, _ -> "article" }, ) { index, article -> val articleModifier = if (index == FOURTH_ITEM_INDEX) { @@ -181,10 +178,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/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/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/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 66ff85116c..3db29adf59 100644 --- 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 @@ -2,9 +2,12 @@ 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.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.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData @@ -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/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/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..fedeec4e55 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 @@ -7,10 +7,12 @@ 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.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -19,9 +21,8 @@ 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 +44,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 +121,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) + } } } } 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 deleted file mode 100644 index 393e589bce..0000000000 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.tangempay - -interface TangemPayFeatureToggles { - val isTangemPayEnabled: 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 deleted file mode 100644 index a51c11a3bc..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.tangempay - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -internal class DefaultTangemPayFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : TangemPayFeatureToggles { - override val isTangemPayEnabled - get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt deleted file mode 100644 index a6ea142d28..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.tangempay.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles -import com.tangem.features.tangempay.TangemPayFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object TangemPayDetailsModule { - - @Provides - @Singleton - fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { - return DefaultTangemPayFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt index 8c9400fe8e..08c9604ba6 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt @@ -3,7 +3,6 @@ package com.tangem.features.tangempay.deeplink import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.features.tangempay.TangemPayFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -11,18 +10,13 @@ import dagger.assisted.AssistedInject internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor( @Assisted uri: Uri, appRouter: AppRouter, - tangemPayFeatureToggles: TangemPayFeatureToggles, ) : OnboardVisaDeepLinkHandler { init { - if (tangemPayFeatureToggles.isTangemPayEnabled) { - val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink( - deeplink = uri.toString(), - ) - appRouter.push(AppRoute.TangemPayOnboarding(mode)) - } else { - appRouter.push(AppRoute.Home()) - } + val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink( + deeplink = uri.toString(), + ) + appRouter.push(AppRoute.TangemPayOnboarding(mode)) } @AssistedFactory diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 10cb869ad1..642d83b272 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) 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/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/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..199094b424 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,6 +25,7 @@ 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 @@ -38,6 +41,7 @@ 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, @@ -50,6 +54,7 @@ internal class WalletComponent @AssistedInject constructor( 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() @@ -148,18 +153,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() 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..7a6c21dbc3 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 @@ -45,9 +45,7 @@ import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvid 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.* @@ -90,10 +88,8 @@ 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, @@ -166,10 +162,8 @@ internal class WalletModel @Inject constructor( } private fun updateYieldSupplyApy() { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { - modelScope.launch(dispatchers.default) { - yieldSupplyApyUpdateUseCase() - } + modelScope.launch(dispatchers.default) { + yieldSupplyApyUpdateUseCase() } } @@ -394,7 +388,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, 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/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index e6baa06b02..f42ebbb24e 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 @@ -36,13 +38,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 +64,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,14 +136,14 @@ 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, @@ -335,12 +335,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 +473,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 +662,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/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index ba99940e19..be88fcbccd 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,6 +1,5 @@ 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 @@ -186,17 +185,6 @@ internal object WalletPreviewData { ) } - 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..78b73ebfa5 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,6 +218,7 @@ internal object WalletScreenPreviewData { singleWalletLockedState, multiWalletState, ), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, 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/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/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/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index c71fb95879..498fdd3e0d 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 @@ -20,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.tangempay.TangemPayFeatureToggles @Suppress("LongParameterList") @Deprecated("Use MultiWalletContentLoaderV2 instead") @@ -44,7 +43,6 @@ internal class MultiWalletContentLoader( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { @@ -88,9 +86,7 @@ internal class MultiWalletContentLoader( getStoryContentUseCase = getStoryContentUseCase, ).let(::add) - if (tangemPayFeatureToggles.isTangemPayEnabled) { - add(tangemPayMainSubscriberFactory.create(userWallet)) - } + add(tangemPayMainSubscriberFactory.create(userWallet)) } } } \ 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 index 525899d92e..1d26373427 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -20,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.TangemPayMainSubscriber -import com.tangem.features.tangempay.TangemPayFeatureToggles import javax.inject.Inject @Suppress("LongParameterList") @@ -43,7 +42,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) { @@ -66,7 +64,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( currenciesRepository = currenciesRepository, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) 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 index fea4a02329..e6d8b5a984 100644 --- 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 @@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarni 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 @@ -25,11 +24,10 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( 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( + override fun create(): List = listOf( accountListSubscriberFactory.create(userWallet = userWallet), walletNFTListSubscriberV2Factory.create(userWallet = userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), @@ -46,12 +44,7 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( stateHolder = stateController, getStoryContentUseCase = getStoryContentUseCase, ), - - if (tangemPayFeatureToggles.isTangemPayEnabled) { - tangemPayMainSubscriberFactory.create(userWallet) - } else { - null - }, + tangemPayMainSubscriberFactory.create(userWallet), ) @AssistedFactory 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..8f877c0751 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,6 +133,7 @@ internal class WalletStateController @Inject constructor() { topBarConfig = WalletTopBarConfig(onDetailsClick = {}), selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, wallets = persistentListOf(), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, 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..fa655d1daf 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,6 +9,7 @@ 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, 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..9308e8370f 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,8 @@ internal class RemoveNFTCollectionsTransformer( is WalletState.SingleCurrency.Locked, -> 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/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..10a816e576 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,10 @@ internal class SetNFTCollectionsTransformer( -> prevState } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + 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..9ecb89cabb 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 @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo 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.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons import timber.log.Timber import java.math.BigDecimal @@ -51,6 +52,10 @@ internal class SetTokenListErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + 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..27a1171ace 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 @@ -8,6 +8,7 @@ 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.WalletUM 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.utils.enableButtons @@ -45,6 +46,10 @@ internal class SetTokenListTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toLoadedState(): WalletCardState { val fiatBalance = when (params) { is TokenConverterParams.Account -> params.accountList.totalFiatBalance 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..c07d25bdfc 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 @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState. 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 +43,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 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/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/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index a8c3a91d43..577da2f275 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 @@ -9,12 +9,9 @@ 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 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, @@ -37,7 +34,13 @@ internal class MultiWalletWarningsSubscriber( it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } } - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = warnings, + notifications = persistentListOf(), + ), + ) walletWarningsAnalyticsSender.send(displayedState, warnings) walletWarningsSingleEventSender.send( userWalletId = userWallet.walletId, 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/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index d815a9c21c..349dce0765 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 @@ -8,6 +8,7 @@ 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 kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate @@ -32,7 +33,7 @@ internal class SingleWalletNotificationsSubscriber( .onEach { warnings -> val displayedState = stateHolder.getWalletState(userWallet.walletId) - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) walletWarningsAnalyticsSender.send(displayedState, warnings) } } 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..6bdd528c1b 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 @@ -720,7 +720,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..8a878d9111 --- /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.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 = { _ -> + + 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 = if (state.isNewMarketEnabled) { + TangemTheme.colors.background.tertiary + } else { + TangemTheme.colors.background.primary + } + + 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/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/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/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/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a0d719fc6b..ae62f1e18f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-579" +tangemCardSdk = "develop-578" #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/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..cce9a8c942 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,7 +17,6 @@ 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 @@ -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/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/settings.gradle.kts b/settings.gradle.kts index d71fbf5bea..88b32b89bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -349,6 +349,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")